【分布式】CAP理论 & 分布式锁
什么是分布式的 CAP 理论?
分布式的 CAP 理论是指在分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)这三个指标无法同时满足的问题。具体来说:
- 一致性(Consistency):指多个副本之间数据保持一致,即在一个副本上的写操作会立即同步到其他所有副本,所有副本的数据都是最新的,保持强一致性。
- 可用性(Availability):指系统在任何时候都能对外提供服务,即系统随时能够响应用户请求,不会因为节点故障或其他原因而导致服务中断。
- 分区容错性(Partition Tolerance):指系统在出现网络分区(节点之间失去联系)时,仍能够继续工作,保证数据的一致性和可用性。
CAP 理论指出,一个分布式系统只能同时满足其中的两个指标,无法同时满足三个。
例如,当出现网络分区时,如果要保证一致性,就必须停止对外服务,从而失去可用性;如果要保证可用性,就必须放弃一致性,从而可能导致不同节点之间数据不一致。
因此,在设计分布式系统时,需要根据具体的场景和需求来选择合适的权衡方案,比如选择 CP(一致性和分区容错性)或者选择 AP(可用性和分区容错性)。
需要注意的是,CAP 理论只是一种理论框架,不能直接应用于实际的分布式系统设计。在实际应用中,还需要考虑系统的具体业务需求、数据访问模式、节点规模和部署环境等因素,综合权衡之后再选择合适的分布式架构和技术方案。
分布式锁介绍
为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。
如何才能实现共享资源的互斥访问呢? 锁是一个比较通用的解决方案,更准确点来说是悲观锁。
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
对于单机多线程来说,在 Java 中,我们通常使用 ReetrantLock
类、synchronized
关键字这类 JDK 自带的 本地锁 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。
举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。
分布式锁应该具备哪些条件?
一个最基本的分布式锁需要满足:
- 互斥:任意一个时刻,锁只能被一个线程持有。
- 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
- 可重入:一个节点获取了锁之后,还可以再次获取锁。
除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件:
- 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
- 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
分布式锁的常见实现方式有哪些?
常见分布式锁实现方案如下:
- 基于关系型数据库比如 MySQL 实现分布式锁。
- 基于分布式协调服务 ZooKeeper 实现分布式锁。
- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。
基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些。
分布式锁常见实现方案总结
基于 Redis 实现分布式锁
如何基于 Redis 实现一个最简易的分布式锁?
不论是本地锁还是分布式锁,核心都在于“互斥”。
在 Redis 中, SETNX
命令是可以帮助我们实现互斥。SETNX
即 SET if Not eXists (对应 Java 中的 setIfAbsent
方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX
啥也不做。
> SETNX lockKey uniqueValue |
释放锁的话,直接通过 DEL
命令删除对应的 key 即可。
> DEL lockKey |
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放 |
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
为什么要给锁设置一个过期时间?
为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 key(也就是锁) 设置一个过期时间 。
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX |
- lockKey:加锁的锁名;
- uniqueValue:能够唯一标示锁的随机字符串;
- NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
- EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。
一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。
代码大概如下:
String KEY = "REQ12343456788";//请求唯一编号 |
这样确实可以解决问题,不过,这种解决办法同样存在漏洞:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。
你或许在想:如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!
如何实现锁的优雅续期?
对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**Redisson** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlock。
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog(看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
如何实现可重入锁?
所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 synchronized
和 ReentrantLock
都属于可重入锁。
不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。
可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。
实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 Redisson ,其内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。
Redis 如何解决集群情况下分布式锁的可靠性?
为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
针对这个问题,Redis 之父 antirez 设计了 Redlock(红锁) 算法 来解决。
Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文(How to do distributed locking - Martin Kleppmann - 2016)怼过 Redlock,他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看Redis 锁从面试连环炮聊到神仙打架这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。
实际项目中不建议使用 Redlock 算法,成本和收益不成正比。
如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠(强一致性)的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。
Redis 实现分布式锁会有什么问题?
2024/08/02 补充
如果使用Redis发布/订阅功能来通知其他客户端锁的释放,以保证其他客户端尝试能够正常获取锁,但这可能存在问题。发布/订阅机制在Redis中是基于“尽力而为”的原则,不保证消息一定会被接收,存在消息丢失的风险。在分布式锁场景下,这种不确定性可能导致锁机制出现错误,比如客户端无法及时获取锁的释放信息,进而无法获取锁。
总结一下:
- 业务未执行完,锁已到期:看门狗/过期时间设置合理(使得在大多数情况下任务能够在锁过期之前完成)
- 单点故障问题:红锁
- 主从问题(因为 Redis 的主从复制过程是异步实现的,如果 Redis 主节点获取到锁之后,还没同步到其他的从节点,此时 Redis 主节点发生宕机了,这个时候新的主节点上没锁的数据,因此其他客户端可以获取锁,就会导致多个应用服务同时获取锁。)
- 时钟漂移:可以让所有节点的系统时钟通过 NTP 服务进行同步,减少时钟漂移的影响。
- 注意原子性!无论是加锁还是释放锁!
基于 ZooKeeper 实现分布式锁
ZooKeeper 简介
ZooKeeper 是一种开源分布式协调服务,用于管理大型分布式系统中的配置、同步以及命名等信息。它通过提供一个简单的原语集合来帮助开发人员设计更加可靠和分布式的系统架构。
ZooKeeper 的主要作用包括:
1)集中配置管理:ZooKeeper 可以用来存储配置信息,多个分布式系统实例可以通过 ZooKeeper 来获取和更新配置,从而保证配置信息的一致性。
2)命名服务:通过提供一个集中化的命名服务,ZooKeeper 使得各个分布式系统组件可以方便地找到对方。
3)集群管理:ZooKeeper 能管理分布式系统中各个节点的状态,比如监控节点的上线、下线,并进行相应的维护。
4)分布式锁服务:用于实现分布式环境下的锁机制,保证多个客户端之间的互斥访问同一资源。
5)领导选举:在分布式系统中,通过 ZooKeeper 来进行节点的领导选举,保证系统的高可用性。
ZooKeeper 在 CAP 问题上的取舍是什么?
ZooKeeper 在 CAP 问题上的取舍是偏向于一致性和分区容忍性(CP),而相对牺牲了一些可用性。也就是说,ZooKeeper 更注重数据的一致性,即便在网络分区的情况下,它也会尽量保证数据的一致和可靠。为了实现这一点,ZooKeeper 采取了一些措施来组织和管理分布式数据。
为什么这么做呢?
1)领袖选举和数据同步机制
ZooKeeper 通过选举一个主节点(Leader)来发布和管理变更,这确保了数据的一致性。任何变更必须通过这个 Leader 来操作,其他从节点(Followers)只是用来读取数据。
2)Quorum 机制
ZooKeeper 采用 Quorum(法定人数)机制,确保数据在写入时得到大多数节点的确认。这也使得它能够容忍一定的网络分区情况,并确保在多数节点确认的情况下,数据是一致的。
这样,ZooKeeper 牺牲了一些可用性。例如,在 Leader 挂掉或者无法联系到多数节点时,ZooKeeper 会暂停对外提供服务,直到新的 Leader 选举出来并同步完数据。
ZooKeeper 和其他分布式锁实现的比较
- 与 Redisson(基于 Redis)实现的分布式锁相比,ZooKeeper 不处理基于内存的数据,它的存储更持久化。然而,ZooKeeper 的性能一般不如基于内存存储的服务。
- 和 Etcd 比较,两者在分布式锁上的思想非常类似,但 ZooKeeper 使用更为广泛,社区支持也更强。