孙玄:毕业于浙江大学,现任转转公司首席架构师,技术委员会主席,大中后台技术负责人(交易平台、基础服务、智能客服、基础架构、智能运维、数据库、安全、IT
等方向);前58集团技术委员会主席,高级系统架构师;前百度资深研发工程师;
【架构之美】微信公众号作者;擅长系统架构设计,大数据,运维、机器学习等技术领域;代表公司多次在业界顶级技术大会 CIO
峰会、Artificial、Intelligence、Conference、A2M、QCon、ArchSummit、SACC、SDCC、CCTC、DTCC、Top100、Strata
+、Hadoop World、WOT、GITC、GIAC、TID等发表演讲,并为《程序员》杂志撰稿 2 篇。
1、CAP 定律剖析
2000 年 Eric Brewer 教授提出 CAP 猜想,2 年后 CAP 猜想被 Seth Gilbert 和Nancy Lynch 从理论上证明。CAP 是 Consitency(强一致性)、Availability(可用性)、Partition tolerance(网络分区容忍性)三个不同维度的组合体,如图 1 所示:
在分布式系统中,CAP 定律中的三者只能同时满足二者(如图 1 所示):CP、AP、AC 模型。进一步分析,AC 模型并不真正的存在,脱离 P(分布式环境)谈AC 都是耍流氓。
我们以多机房数据库同步更新的场景来分析下为什么 CAP 定律中三者不能同时满足,如图 2 所示,用户通过机房一的数据访问层写入数据到 MySQL 主库,并通过网络把此数据同步到机房二的 MySQL 从库。
在此场景下,CAP 对应的含义为:C 为机房一的 MySQL 数据库主节点更新,那么机房二的 MySQL 数据从节点也要更新;A 为必须保证 MySQL 主从两个数据节点都是可用的;P 为当机房一和机房二主从节点出现网络分区,必须保证系统对外可用。
对于机房一的写请求,一旦机房一和机房二出现网络分区(即网络断开),此时写请求无法成功写入到机房二的 MySQL 从库,就会导致写请求无法返回,即 A(可用性)无法满足。大家可以思考一个问题,假设机房一和机房二的网络终究会恢复,用户侧也能够容忍机房网络恢复的时间一直等待,那么 CAP 定律是否同时满足?
2、业务场景驱动
我们来看三个典型的业务场景:
业务场景一:在秒杀的场景下,只允许用户购买一件商品;
业务场景二:用户下单成功后会产生下单消息,在订单消息响应应答模式下会发送多条消息到 MQ 中,下游在对 MQ 中订单消息进行消费时,需要对此订单消息进行去重;
业务场景三:在用户对商品下单后,订单状态变为待支付,在某一时刻用户正在对该订单做支付操作,商家对该订单进行改价操作,如何保证操作的数据一致性。
通过业务需求剖析业务背后的本质,是架构师需要具备的核心能力之一。这三个业务场景背后共性的本质是什么?
业务场景一需要对用户进行并发控制,也就是需要对用户 ID 进行串行化操作处理,防止用户重复下单;
业务场景二需要对订单消息中的订单 ID 进行串行化操作处理,防止下游对订单消息重复消费;
业务场景三需要对订单 ID 进行串行化操作处理,防止出现数据的不一致性。
既然三个场景都需要对共享资源进行串行化处理,问题转化为锁处理的问题。如果是单机环境通过本地锁的方式可以优雅解决(如图 3),在分布式的环境下,服务冗余部署多份,不同的请求由不用的冗余服务来处理,本地锁将不能很好的工作,需要分布式锁进行处理(如图 4)。
3、分布式锁本质
提到分布式锁,大家能够想到基于 Redis 来实现,锁的本质是对共享资源串行化处理。Redis 内部采用唯一线程的串行化处理请求恰好满足锁的使用场景。那么基于 Redis 如何具体实现分布式锁?我们从具体命令和架构两个维度进行分析。
在具体命令实现侧,有两种方式,第一采用 Redis SetNX(Set if Not eXsits)命令,此命令在指定的 Key 不存在时,为 Key 设置指定的值。SETNX Key Value 命令设置成功返回 1,设置失败,返回 0。
比如在业务场景一,用户 ID 为 1009,此时 Key 为 1009,Value 可以随意填写,例如 100。当两个服务同时调用调用 SETNT 1009 100 命令申请锁时,Redis 保证只有一个服务能够成功申请到锁。对于锁我们需要加上锁使用的时间,确保锁的公平性,最坏情况下,其他服务能够有机会申请到这把锁。
为了达到这个目的,需要对锁进一步添加过期时间,使用 EXPIRE Key seconds 命令,设置生存时间,比如为用户 ID1009 设置 10S 的生存时间:EXPIRE 1009 10。
由于 SETNX 命令和 EXPIRE 命令是两条命令,需要保证他们同时执行成功。Redis 提供了事务的处理方式:采用【MULTI;多个执行命令;EXEC;】LUA 脚本执行语句组。业务场景一可以如下进行事务处理:
<span style="color:#000000"><code>MULTI;
SETNX 1009 100;
EXPIRE 1009 10;
EXEC;
</code></span>
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
上述具体命令实现较为复杂,在 Redis 2.6.12 及以上的版本,可以采用 Set Key Value NX PX milliseconds 命令方式,此命令在指定的 Key 不存在时,为 Key 设置指定的值,并设置生存时间。
其中 NX 参数表示指定的 Key 不存在时,再设置指定的值;PX 参数表示生存时间,单位为毫秒。业务场景一采用 Set 命令,具体调用命令如下:Set 1009 100 NX PX 10000。
在架构设计侧,第一种方式采用 Redis 单机方式(如图 5),当服务 S1 和服务 S2 同时申请锁(Set 1009 100 NX PX 10000)简称为 L1 时,Redis 单机会按照接受到请求先后顺序的处理方式,保证 S1 申请到锁 L1,S2 申请锁 L1 失败。(假设S1先申请锁L1,S2后申请锁L1)。
Redis 单机模式存在单点隐患,一旦 Redis 宕机,内存中的锁全部丢失,Redis 再次启动,假如此时服务 S1 还在使用锁 L1,服务 S2 又再次申请锁 L1,就会申请成功,此时就会出现同一时刻 2 个服务同时都拿到同一把锁的尴尬局面。
第一种方式问题在于 Redis 的单机模式,通过使用 Redis 集群的主从模式来解决Redis 单机模式的数据丢失问题。
第二种方式如同 6 所示,在业务场景一,当服务 S1 和服务 S2 同时在 Redis 主节点申请锁 L1 时,服务 S1 申请到锁 L1。通过 Redis 主从集群的数据同步机制会异步同步给 Redis 从节点,Redis 从节点也拥有了锁 L1。假设 Redis 主节点挂掉,由于 Redis 集群的 Sentinal 的哨兵监控和主从切换机制,此时 Redis 集群的从节点会提升为新 Redis 集群的主节点,继续对外提供锁申请服务,使得锁申请服务继续正常进行。
大家思考一个极端的场景,如图 7 所示,服务 S1 刚在 Redis 集群主节点申请到锁 L1,锁 L1 还未同步到 Redis 集群从节点,此时 Redis 集群主节点挂掉,根据Redis 集群的 Sentinal 哨兵机制,会把从节点提升为新 Redis 集群的主节点,而服务 S2 继续在新 Redis 集群的主节点申请锁 L1,那么服务 S2 就会成功申请到锁 L1。则再次出现服务 S1 和服务 S2 在此时同时申请到锁 L1 的情况。
那为何会造成这样的情况?我们从架构的本质(CAP 模型)来深入分析下原因。
我们要保证同一把分布式锁的申请在同一时刻只能有一个服务拿到此锁,因此从 CAP 模型底层分析,分布式锁是 CP 模型。
而 Redis 集群的主从模式是 AP 模型。也就是说从架构设计哲学层面来看,分布式锁选用 Redis 集群的主从模式就是不优雅的,从而导致了上述一系列问题的出现。
但是,当在百度里搜索分布式锁,有很多的实现方案是基于 Redis 集群,为什么会是这样?
我们继续深入分析,一切脱离场景谈架构都是耍流氓,特别是脱离业务场景。
业务场景分为 2 类:追求数据强一致性场景、追求数据最终一致性场景。
数据强一致性场景:比如金融、电商交易等,使用分布式锁时需要使用 CP 模型,不然就会出现支付去重失败等重大问题,此时公司离破产只差用户一个大请求;
数据最终一致性场景:比如微信发消息等,在使用分布式锁时使用AP模型较优雅,比如对用户发送消息(今晚有空吗?约个饭)的去重,极端情况下使用分布式锁去重失败,也就是消息发送到对方 2 次,反而会增加彼此之间的感情,本来要拒绝邀请的,由于收到 2 次邀请消息,结果就不好意思拒绝了。
4、分布式锁设计与实践
分布式锁存储选型至关重要,以下对比了 Redis、ZooKeeper、etcd 等存储模型,如图8 所示。
通过以上的分析,对于数据一致性要求高的业务场景需使用 CP 型的存储模型。
Zookeeper 多锁实现使用创建临时节点和 Watch 机制,在执行效率、扩展能力以及社区活跃度等方面低于 etcd,因为选用基于 etcd 作为分布式锁的存储模型。
分布式锁的架构设计如图 8 所示,由 etcd 存储集群、分布式锁客户端、监控平台等三部分构成。
其中 etcd 集群负责锁的申请、续租、释放等操作处理,分布式锁客户端通过 HTTP API对 etcd 集群进行操作,从而使得微服务调用方能够申请锁、对锁进行续租、对锁探活、锁操作的监控等。
在部署层面,etcd 集群至少需要部署 3 台,分布式锁客户端以 SDK 的方式嵌入到微服务中。
5、总结
从架构设计哲学层面分析,分布式锁本质上是 CP 模型。一切脱离场景谈架构设计都是耍流氓,因此我们需要针对业务场景的不同,选用优雅的分布式锁实现,在追求数据强一致性的业务场景中,选用 CP 存储模型,在追求数据最终一致性的业务场景中,选用 AP 存储模型。