高可用

关于高可用的一点讨论

高可用

我目前的理解是,高可用必须得基于磁盘的持久性。也就是说,数据存储在项目代码、虚拟机、内存、线程中,通通都是不可靠的。只有放在磁盘,也就是服务器或服务器的数据库中,才是真的。

数据一致性

比如对于分布式架构下的数据一致性问题,在强可用性需求下,我们可以通过在数据库加一张容错表来实现。

就拿一个 ES 和 MySQL 的双写问题来讲,同一份数据既要存到 ES,又要存到 MySQL,这时候就需要考虑数据一致性问题。不能在一边存了,另一边却没存的情况。

我这里是先存 MySQL,再同步到 ES。我会在要存储的表中加一个字段,比如就叫做 sync_status,初始值是 0,表示未同步。

在业务代码中,是先进行 MySQL 的保存(设置 sync_status = 0),然后再开一个 MQ 异步任务,把数据同步到 ES。如果同步成功,则需要回显把 sync_status 改成 1。如果同步失败,也没关系。我们可以开一个定时任务,扫描 sync_status 为 0 的数据,然后再次尝试同步,直到所有数据都同步成功。

  • 这里如果 MySQL 的保存就失败了,怎么办?这个其实很简单,我们只需要利用事务特性,直接回滚即可。毕竟需求是一致性,只要两方数据是一致的,就没问题。

乐观锁

再举一个我今天刚遇到的一个bug,是关于高并发下的资源竞态问题。我这里尝试用基于数据库支持的乐观锁来解决。

讲起来思路也比较简单,就是在数据库表字段中,多加一个 version 字段,用来记录数据的版本号。

所有乐观锁的原理都是基于 CAS 的,也就是 compare-and-swap。也就是说,我们先获取数据的值,对做完操作之后数据的值进行一个预期的估计,如果预期的估计和实际的估计一致,则更新数据,否则就不更新。

那么切换到这个情景当中,我们把这个资源竞态问题抽象成修改数据库表中size字段的值吧。比如说,本来是3,有个线程A要把size设为4,有个线程B要设为6。当然传入参数肯定是一整个实体,实体中的一个字段是size。

我们首先先通过ID获取要修改的那条记录,获取它的version字段的值,假设它是n,那么修改完后的版本应该是n+1。之后我们修改传入实体的版本号为n+1,进行update的更新操作。但是这里注意,update操作是有条件的,就是说,只有对ID字段符合条件并且version字段的值是n才可以执行更新操作。

那么即使有多个线程同时修改同一条记录,也许在查询的时候它们查出来的version值都是一样,有可能会造成竞态。但在实际应用更改的时候,由于版本号的存在,只有一个线程能符合修改的实体版本号刚好比要被修改的数据库表记录的版本号大1,才会被成功执行更新操作。而其他操作都会因为版本号小于或等于要被修改的记录的版本号,而被拒绝。

这样,通过乐观锁,我们就以一种无锁的方式,解决了资源竞态的问题。

限流降级

这个我没有实际做过类似的需求和实现,毕竟什么几亿的请求量一起打过来这种需求,去全国的互联网厂都很少有,就算有,也不可能交给你一个实习生。

这个一般的做法是,在高并发情况下,通过限流和降级,来避免系统被压垮。限流是指限制系统的请求处理速度,降级是指降低系统的处理能力,让系统变得更加可靠。

比如,对于一个接口,我们可以设置一个阈值,比如每秒100次,超过这个阈值就拒绝请求。如果超过了这个阈值,我们可以暂时把请求丢弃,或者返回一个降级的响应。

降级的另一种方式是,根据系统的负载情况,动态调整限流阈值,比如,如果负载过高,可以适当提高限流阈值,以避免系统被压垮。

限流和降级的原理都是通过限制系统的请求处理速度和降低系统的处理能力,来避免系统被压垮。限流和降级的策略可以根据系统的实际情况进行调整。