一、业务幂等本质
2000年,Eric Brewer教授提出CAP猜想,2年后,被Seth Gilbert和Nancy Lynch从理论上证明
Consistency(强一致性)、Availability(可用性)、Partition tolerance(网络分区容忍性)
分布式系统三者同时满足二者
数据库更新案例
- C:数据库主节点更新,从节点也要更新
- A:必须保证两个数据节点都是可用的
- P:当主从节点出现网络分区,必须保证系统对外可用
- 只要主从出现网络分区,A就无法满足,所以要么是AP要么是CP
二、业务场景
业务场景一:交易商品库锁定
- 防止用户重复下单
业务场景二:MQ消息去重
- 防止消息重复消费
- 发送端去重
- 消费端去重
业务场景三:订单操作变更协同
- 在用户对商品下单后,订单状态为待支付,在某一时刻用户正在对该订单做支付操作,商家对该订单进行改 价操作
- 状态的修改行为需要做串行处理,避免出现数据不一性
解决方案
- 共享资源互斥
- 共享资源串行化
问题转化
- 锁的问题
- 本地锁弊端
- 分布式锁
分布式锁主要是为了解决分布式系统中多个节点之间对共享资源的访问控制问题。在分布式系统中,多个节点可能同时需要对同一个资源进行操作,因此需要一种机制来确保同一时刻只有一个节点可以访问该共享资源,以避免数据的不一致或冲突问题。
最常见的应用场景就是分布式环境下的数据分段写入,以避免数据冲突和覆盖的问题。在分布式环境中,每个节点都有自己的本地存储,如果多个节点同时要对同一个数据进行写操作,就需要通过分布式锁来实现对该数据共享资源的控制,从而保证数据的正确性。
此外,分布式锁还可以用来控制分布式事务的一致性,防止分布式事务过程中数据的不一致性和死锁发生。在分布式事务中,多个服务节点需要互相协作完成事务,分布式锁保证了同步操作的不冲突。
在总体上,分布式锁通常用于控制共享资源的访问,确保同一时刻只有一个节点可以访问资源,这是分布式系统中的关键问题。
分布式锁的本质
基于Redis的分布式锁实现方案
原理:唯一线程串行处理
实现方式
- Redis SetNX(SET if Not eXists) # 命令在指定的 key 不存在时,为 key 设置指定的值
- SETNX Key Value # 设置成功,返回 1, 设置失败,返回 0
- EXPIRE Key seconds # 设置生存时间
- MULTI;多个执行命令;EXEC;
- SET Key Value NX PX milliseconds # 在指定的 key 不存在时,为 key 设置指值,并设置生存时间
redis单机\ Master-Slave
缺点:锁时间不可控
一切脱离业务场景谈架构都是耍流氓(深层次)
- 分布式锁本质
- 业务场景容忍度
架构设计本质:分布式锁是CP模型,Redis集群是AP模型
CP模型分布式锁
ZooKeeper对锁实现使用创建临时节点和 watch机制,并发执行效率、扩展能力、社区活跃度等方面低于 etcd选择基于etcd实现
设计目标
- 强一致性
- 服务高可用、系统稳健
- 锁自动续约及其自动释放
- 代码高度抽象业务接入极简
- 可视化管理后台,监控及管理
- 业务可重入
etcd分布式锁
-
简单KV
-
强一致
-
高可用、无单点
-
数据高可靠、持久化
分布式锁解决方案
-
分布式Client + etcd
-
Client TTL模式
申请锁
-
业务方申请资源锁,调用时提供key,ttl
-
etcd生成uuid,作为当前锁的唯一凭证,将 (key,uuid,ttl) 写 etcd
-
检查etcd中此key是否存在,如没有,尝试写 入key,写入失败,
-
拿锁失败,写入成功拿到锁l拿锁后,心跳线程启动,心跳线程维持时间为 ttl/3 ,compare and swap uuid , 从而将key值续租
-
相关etcd API
-
申请锁
curl http://127.0.0.1:2379/v2/keys/foo -XPUT -d value=bar -d ttl=5 prevExist=false -
CAS更新锁租约
curl http://127.0.0.1:2379/v2/keys/foo?prevValue=prev_uuid -XPUT -d ttl=5 -d refresh=true -d
prevExist=true -
CAS删除锁
curl http://10.10.0.21:2379/v2/keys/foo?prevValue=prev_uuid -XDELETE
业务方申请资源锁,调用时提供key,ttl
检查etcd中key的存在,若已存在,拿锁失败
清理锁
如果调用方正常结束,通过cas接口掉用delete方法自动清理etcd中的key值
如果调用方异常终止 ,等待原有锁ttl过期后,锁资源释放
分布式锁时序图
业务接入
JDK 7及以上
获取锁案例
释放锁案例
以上代码是非阻塞锁的用法,其中没有显式调 lock.release()释放锁,因为 Lock 对象实现 Closeable 接 口,
在JDK 1.7+ 语法中,try语句执行完成后会自动执行close(),Lock 把close()指向release()。 (语法糖)
获取锁平均耗时
每天有千万级的锁操作,平均耗时 1.34 ms
由于 etcd 强一致性,根据Raft算法,消耗时间稍微长一点
锁可以解决幂等性问题吗?
锁不能解决幂等性,但是解决幂等性问题科技借助分布式锁,幂等性属于业务问题,所以需要在业务上添加业务校验
分布式锁在分布式系统中既有很多优点,也有很多挑战和限制。在实践中需要注意以下几点。
-
锁的粒度。锁的粒度要控制好,如果锁的粒度过大,会影响多个客户端的并发性能,而如果锁的粒度过小,锁的数量也会过多,同样会影响性能。
-
锁的容错性。分布式锁必须要具备容错性,当锁宿主节点故障时,应该选择在哪个节点上重新选举锁,防止资源的重复修改。
-
锁的可重入性。锁应该是可重入的,即同一个线程在持有锁的情况下可以多次获取到锁。
-
锁的过期时间。为了防止死锁,应该设置合理的锁过期时间。如果设置过短,就有可能导致一些合法的客户端无法获取到锁;如果设置过长,会导致一些客户端的等待时间过长,甚至会引发死锁。
-
锁的性能。在分布式系统中,锁的性能通常是系统性能的瓶颈。因此,在设计分布式锁时必须考虑到锁的高性能,以避免锁的性能成为系统的瓶颈。
-
锁的实现方式。在分布式环境中,锁的实现方式也会影响锁的性能和可靠性。常见的锁的实现方式包括数据库锁、内存锁、ZooKeeper锁、Redis锁等。
总之,在实现分布式锁系统时应该充分考虑应用场景、系统架构、容错性、性能等因素,以保证分布式锁的高效性和可靠性。
技术选型
在选择分布式锁的技术时,需要综合考虑系统需求、性能、可靠性、易用性等因素。下面简单介绍Redis、ZooKeeper和etcd三种分布式锁技术的特点和应用场景。
- Redis分布式锁
Redis是一种内存数据库,支持分布式锁的实现。Redis分布式锁通常使用SETNX命令和EXPIRE命令实现锁的获取和释放,并且支持自动过期机制,可以避免锁的死锁问题。Redis分布式锁适用于对性能要求较高,需要快速获取锁的场景,但对于容灾、高可用等要求较高的场景,Redis的解决方案可能不够优秀。
- ZooKeeper分布式锁
ZooKeeper是一个通用的分布式系统协调服务,提供了分布式的锁服务。ZooKeeper分布式锁通常使用顺序节点和watch机制实现,可以自动发现当前持有锁的客户端、故障节点的容错能力较为优秀。ZooKeeper分布式锁适用于需要高可靠性和高可用性,但对于大规模集群可能不是很适合。
- etcd分布式锁
etcd是一个基于Raft协议的分布式系统,同时也是一个分布式键值存储系统。etcd分布式锁通常使用分布式的一致性算法实现,可以提供高度分布式的一致性。etcd分布式锁对于对高可靠性、容灾、高可用等要求很高的场景,是一个很好的选择。
综上所述,Redis分布式锁适用于性能要求高的场景,ZooKeeper分布式锁适用于对高可用性要求高的场景,etcd分布式锁适用于对高可靠性和容灾要求高的场景。在实际应用中,我们应该根据具体业务需求,选择适合自己的分布式锁技术。