分布式系统学习9:分布式锁

本文涉及的产品
可观测监控 Prometheus 版,每月50GB免费额度
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
可观测可视化 Grafana 版,10个用户账号 1个月
简介: 本文介绍了分布式系统中分布式锁的概念、实现方式及其应用场景。分布式锁用于在多个独立的JVM进程间确保资源的互斥访问,具备互斥、高可用、可重入和超时机制等特点。文章详细讲解了三种常见的分布式锁实现方式:基于Redis、Zookeeper和关系型数据库(如MySQL)。其中,Redis适合高性能场景,推荐使用Redisson库;Zookeeper适用于对一致性要求较高的场景,建议基于Curator框架实现;而基于数据库的方式性能较低,实际开发中较少使用。此外,还探讨了乐观锁和悲观锁的区别及适用场景,并介绍了如何通过Lua脚本和Redis的`SET`命令实现原子操作,以及Redisson的自动续期机

这是小卷对分布式系统架构学习的第12篇文章,今天学习面试中高频问题:分布式锁,为什么要做分布式锁,有哪些实现方式,各适用于什么场景等等问题

1. 为什么要用分布式锁?

分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了

分布式锁的特点:

  • 互斥:任意时刻,锁只能被一个线程持有
  • 高可用:锁服务本身是高可用的,一个节点出问题,能自动切换到另一个节点
  • 可重入:获取过锁的节点,可再次获取锁;
  • 超时机制:为了防止锁无法被释放的异常情况,需要设置超时时间,过了超时时间,锁自动释放;
  • 自动续期:如果任务处理时间超过超时时间,会出现任务未处理完成而锁释放的情况。因此可开启一个监听线程,监听任务还未完成就延长锁的超时时间;

2. 乐观锁和悲观锁

  • 悲观锁:认为多线程环境下,每次访问共享资源一定会出现冲突,所以访问资源前就加锁
  • 乐观锁:认为冲突是偶然情况,没有竞争才是普遍情况。一开始就不加锁,在出现冲突时采取补救措施,简单概述:先修改共享资源,再验证有没有发生冲突,如没有,则操作完成。如果有其他线程已经修改过这个资源,就放弃本次操作

使用场景:

  • 乐观锁去除了加锁解锁的操作,但是一旦冲突后的重试成本非常高,只有再冲突概率非常低,且加锁成本比较高的场景,才考虑使用乐观锁

3.分布式锁的实现方式

常见分布式锁实现方案如下:

  • 基于关系型数据库比如 MySQL 实现分布式锁。
  • 基于分布式协调服务 ZooKeeper 实现分布式锁。
  • 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。

3.1基于Redis的实现

setnx + expire组合命令

在redis中,SETNX命令可以实现互斥,即Set if not exist的意思,如果key不存在,才可设置key的值,如果key已存在,SETNX命令啥也做不了

setnx命令不能设置key的超时时间,因此需要通过expire命令来设置key的超时时间

加锁

> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
# 设置过期时间
> expire lockKey seconds

这里常见的问题就是加锁和设置过期时间是两个操作,不是原子操作,可能出现加锁成功,设置超时时间失败,出现锁永远不会释放的问题。为了解决这个问题,Redis从2.6.12之后支持set命令增加过期时间参数:

127.0.0.1:6379> SET lockKey uniqueValue EX 30 NX
OK
127.0.0.1:6379> SET lockKey uniqueValue EX 30 NX
(nil)

关于Redis SET命令的详细说明可以查看Redis官方文档:https://redis.io/docs/latest/commands/set/

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
  EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

参数说明:

  • EX 秒数:设置指定的过期时间,以秒为单位(正整数)。
  • PX 毫秒数:设置指定的过期时间,以毫秒为单位(正整数)。
  • EXAT 时间戳(秒):设置键将在指定的Unix时间戳(以秒为单位)过期(正整数)。
  • PXAT 时间戳(毫秒):设置键将在指定的Unix时间戳(以毫秒为单位)过期(正整数)。
  • NX:仅在键不存在时设置键。
  • XX:仅在键已存在时设置键。
  • KEEPTTL:保留键的生存时间。
  • GET:返回键存储的旧字符串,如果键不存在则返回nil。如果键存储的值不是字符串,则返回错误并终止SET操作。

释放锁

释放锁时通过DEL命令删除key即可,但不能乱删,要保证执行操作的客户端就是加锁的客户端。为了防止误删了其他锁,这里建议使用lua脚本通过key对应的value来判断,使用Lua脚本保证解锁操作的原子性

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

分布式锁1.png

面试题:如何实现锁的优雅续期?

如果任务还没执行完成,锁就过期了,这样就出现锁提前过期的问题了。为了解决这个问题,Java语言已经有了解决方案:Redisson

其他语言的解决方案,可以在Redis官方文档中找到:https://redis.io/docs/latest/develop/use/patterns/distributed-locks/

分布式锁2.png

官方提供了Redlock的算法,用于实现分布式锁管理器。

下面讲讲Redisson的自动续期机制,原理很简单:提供了一个专门用来监控和续期锁的Watch Dog(看门狗)机制,如果操作共享资源的线程还未执行完成的话,Watch Dog会不断延长锁的过期时间

分布式锁3.png

看门狗核心逻辑如下:

  • EXPIRATION_RENEWAL_MAP中获取锁的状态。如果锁已经被释放,则不再续期。
  • 如果锁仍然存在且当前线程持有锁,则异步调用renewExpirationAsync方法来更新锁的过期时间。
  • 如果续期成功,会递归调用renewExpiration方法,重新启动定时任务,继续进行下一次续期;

如何实现可重入锁?

可重入锁指的是一个线程可以多次获取同一把锁,如Java中的synchronizedReentrantLock都是可重入锁

实现可重入锁的核心思路:线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。需要为每个锁关联一个可重入计数器和一个占有它的线程,计数器大于0时,锁被占用,需判断请求获取锁的线程和当前持有锁的线程是否为一个。

Redisson本身已经支持了多种锁:可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)

3.2基于Zookeeper的实现

当前面试比较卷啊,面试官可能会问除了用Redis做分布式锁外,还有没有其他方法,所以还是要多了解一种方法的

前面分布式理论基础时已经了解到Zookeeper是CP模式,提供数据一致性,因此适合作为分布式锁的选型。

ZooKeeper 分布式锁是基于 临时顺序节点Watcher(事件监听器) 实现的。

分布式锁的实现步骤为:

(1)创建锁节点

  • 在Zookeeper中创建一个父节点(如/lock),作为锁的根节点
  • 每个客户端尝试获取锁时,会在/lock下创建一个临时顺序节点(如/lock/lock-0000000001

(2)获取锁

  • 客户端创建完临时顺序节点后,会获取/lock下所有子节点的列表。
  • 客户端检查自己创建的节点是否是当前所有子节点中序号最小的节点:
    • 如果是,则认为获取了锁。
    • 如果不是,客户端会监听比自己序号小的紧邻前一个节点的删除事件(即/lock/lock-0000000001会监听/lock/lock-0000000000的删除事件)

(3)释放锁

  • 当持有锁的客户端完成任务后,它会主动删除自己创建的临时顺序节点
  • 由于Zookeeper的监听机制,下一个等待锁的客户端会收到通知,再次检查自己是否是当前序号最小的节点
  • 如果是,则获取锁并继续执行

分布式锁4.png

实际开发过程中,通常使用Curator来实现Zookeeper的分布式锁,该框架封装了各种API可直接使用,可实现:

  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutex:分布式不可重入排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。

3.3 基于数据库的实现

这里只简单说下基于MySQL数据库实现的分布式锁,实际开发中应该没人用MySQL做分布式锁吧

基于悲观锁的方式

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

示例:

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;

我们使用了select…for update的方式,for update是一种行级锁,也叫排它锁。如果一条select语句后面加上for update,其他事务可以读取,但不能进进行更新操作。这样就通过开启排他锁的方式实现了悲观锁

基于乐观锁的方式

使用版本号,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号

示例:

1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status2
update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

使用场景选择

这里还是使用Redis和Zookeeper的两种方式,MySQL的方式性能较低

  • 如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。推荐优先选择 Redisson 提供的现成分布式锁,而不是自己实现。实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。
  • 如果对一致性要求比较高,建议使用 ZooKeeper 实现分布式锁,推荐基于 Curator 框架来实现。不过,现在很多项目都不会用到 ZooKeeper,如果单纯是因为分布式锁而引入 ZooKeeper 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。
相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
26天前
|
存储 缓存 NoSQL
分布式系统架构8:分布式缓存
本文介绍了分布式缓存的理论知识及Redis集群的应用,探讨了AP与CP的区别,Redis作为AP系统具备高性能和高可用性但不保证强一致性。文章还讲解了透明多级缓存(TMC)的概念及其优缺点,并详细分析了memcached和Redis的分布式实现方案。此外,针对缓存穿透、击穿、雪崩和污染等常见问题提供了应对策略,强调了Cache Aside模式在解决数据一致性方面的作用。最后指出,面试中关于缓存的问题多围绕Redis展开,建议深入学习相关知识点。
166 8
|
20天前
|
消息中间件 算法 调度
分布式系统学习10:分布式事务
本文是小卷关于分布式系统架构学习系列的第13篇,重点探讨了分布式事务的相关知识。随着业务增长,单体架构拆分为微服务后,传统的本地事务无法满足需求,因此需要引入分布式事务来保证数据一致性。文中详细介绍了分布式事务的必要性、实现方案及其优缺点,包括刚性事务(如2PC、3PC)和柔性事务(如TCC、Saga、本地消息表、MQ事务、最大努力通知)。同时,还介绍了Seata框架作为开源的分布式事务解决方案,提供了多种事务模式,简化了分布式事务的实现。
49 5
|
4月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
95 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
4月前
|
分布式计算 NoSQL Java
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
80 2
|
5月前
|
机器学习/深度学习 算法 自动驾驶
深度学习之分布式智能体学习
基于深度学习的分布式智能体学习是一种针对多智能体系统的机器学习方法,旨在通过多个智能体协作、分布式决策和学习来解决复杂任务。这种方法特别适用于具有大规模数据、分散计算资源、或需要智能体彼此交互的应用场景。
282 4
|
4月前
|
SQL NoSQL 安全
分布式环境的分布式锁 - Redlock方案
【10月更文挑战第2天】Redlock方案是一种分布式锁实现,通过在多个独立的Redis实例上加锁来提高容错性和可靠性。客户端需从大多数节点成功加锁且总耗时小于锁的过期时间,才能视为加锁成功。然而,该方案受到分布式专家Martin的质疑,指出其在特定异常情况下(如网络延迟、进程暂停、时钟偏移)可能导致锁失效,影响系统的正确性。Martin建议采用fencing token方案,以确保分布式锁的正确性和安全性。
78 0
|
5月前
|
Java
分布式-Zookeeper-分布式锁
分布式-Zookeeper-分布式锁
|
4月前
|
NoSQL Java Redis
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
8天前
|
缓存 NoSQL 中间件
Redis,分布式缓存演化之路
本文介绍了基于Redis的分布式缓存演化,探讨了分布式锁和缓存一致性问题及其解决方案。首先分析了本地缓存和分布式缓存的区别与优劣,接着深入讲解了分布式远程缓存带来的并发、缓存失效(穿透、雪崩、击穿)等问题及应对策略。文章还详细描述了如何使用Redis实现分布式锁,确保高并发场景下的数据一致性和系统稳定性。最后,通过双写模式和失效模式讨论了缓存一致性问题,并提出了多种解决方案,如引入Canal中间件等。希望这些内容能为读者在设计分布式缓存系统时提供有价值的参考。感谢您的阅读!
Redis,分布式缓存演化之路
|
2月前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
210 5