to be perfected
2023年3月11日大约 5 分钟Redis
简单说一下锁
锁的几个基本条件
- 锁必须是互斥的,即在任何时候只能有一个线程持有锁
- 锁必须是可重入的,即如果一个线程已经持有了锁,那么它可以多次获取锁儿不会发生死锁
- 锁必须是安全的,即如果一个线程获得了锁,那么即使崩溃或失去连接,锁也必须释放
分布式锁的几个条件
- 高性能:分布式锁可能会有很多服务器来获取,所以一定要保证锁能够高效地获取和释放,不然锁又会成为一个瓶颈
- 高可用:不能因为某一个分布式锁获取的服务不可用,导致所有服务都拿不到或者释放锁
- 锁失效机制:假设某个应用获取到锁之后,一直没有释放锁,可能服务本身已经挂掉了。不能一直不释放,导致其他服务一直获取不到锁
- 非阻塞特性:在某个服务来获取锁时,假设该锁已经被另一个服务获取,要能直接返回失败,不能一直等待
常见的分布式锁实现方式,如何进行选型
常见的分布式锁实现方式包括基于数据库、基于缓存、基于 ZooKeeper 等方式
基于 MySQL 实现分布式锁
通过数据库的事务特性来保证锁的正确性,将锁状态存储在服务器中
通过使用数据库的行级锁来保证对统一资源的访问互斥
例如使用 MySQL 的select ... for update
或者update ... where
来实现分布式锁通过使用数据库的唯一索引来实现分布式锁
例如在 MySQL 中可以使用insert ... on duplicate key update
来实现
在此方式下,需要将资源的唯一标识作为唯一索引,每次需要获取锁时,尝试向数据库中插入一条记录,如果已存在,则更新记录,否则插入一条新纪录,插入成功即可获得锁
优缺点分析
- 优点
- 实现简单:相对于基于 ZooKeeper 的实现方式,基于 MySQL 的实现方式较为简单,只需要使用 MySQL 的事务和唯一索引即可实现分布式锁
- 不需要引入额外的依赖:MySQL 较 ZooKeeper 来说常用,对于没有 ZooKeeper 的系统来说,可以方便地使用基于 MySQL 的分布式锁
- 缺点
- 对 MySQL 的可用性和性能要求比较高:需要保证 MySQL 集群的可用性和性能,否则将会影响到整个系统的正常运行
- 不适合高频率的加锁和解锁操作:基于 MySQL 的实现方式使用数据库的事务机制实现锁的管理,因此在高频率的加锁和解锁操作场景下,会产生大量的数据库连接和事务操作,导致性能瓶颈
- 不适合长时间占用锁资源:由于 MySQL 的锁实现方式是使用行锁或表锁,因此不适合长时间占用锁资源,否则会导致锁冲突,进而影响到整个系统的性能和可用行
基于 Redis 的分布式锁实现方式
基于缓存的分布式锁实现方式,通常使用分布式缓存存储锁状态,例如使用 Redis 的 SETNX 操作
- 使用 Redis 的 SETNX 命令获取锁,SETNX 命令可以在指定的键不存在时设置该键的值,如果键已经存在则不做任何操作
- 可以使用 Redis 的带有过期时间的 SETEX 或者类似的命令,保证锁的自动过期,避免死锁
优缺点分析
- 优点
- 实现简单:基于缓存的分布式锁的实现方式比较简单,只需要使用缓存的 CAS 原语和过期时间即可实现
- 支持高并发:缓存系统一般都支持高并发的读写操作,因此可以较好地支持高并发下的分布式锁场景
- 可以较好地解决死锁问题:过期时间机制可以较好地避免死锁问题,一旦某个节点因为故障导致锁没有被释放,那么在过期时间到达之后,该节点的锁会自动失效,从而不会影响整个系统的运行
- 缺点
- 依赖于缓存系统的可用性和性能:如果缓存系统不稳定或者出现性能瓶颈,将会影响到整个系统的正常运行
- 可靠性较低:缓存系统本身就是一个内存存储系统,因此在发生故障时,缓存中的数据可能会丢失,因此需要设计合理的故障恢复机制
基于 ZooKeeper 的分布式锁实现方式
通常使用 ZooKeeper 的节点特性来实现,例如使用 ZooKeeper 的临时节点来实现
- 在 ZooKeeper 上创建临时节点,每个节点对应一把锁,锁的持有者在创建节点时添加自己的标识。当锁的持有者释放锁时,删除对应的节点即可
- 利用 ZooKeeper 提供的 watch 机制,当节点被删除时,可以通过 watch 机制进行通知,从而实现锁的等待机制
优缺点分析
- 优点
- 实现简单:ZooKeeper 已经实现了分布式协调服务的机制,提供了临时节点、watch 机制等原语,可以方便地实现分布式锁
- 具有一定的容错能力:ZooKeeper 是一个高可用的分布式协调服务,能够保证分布式锁在出现故障时的可用性
- 对于分布式环境下的锁管理效果良好
- 缺点
- 对 ZooKeeper 的可用性和性能要求较高
- 依赖性较强