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

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
简介: 本文介绍了分布式系统中分布式锁的概念、实现方式及其应用场景。分布式锁用于在多个独立的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 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。
相关文章
|
1月前
|
关系型数据库 Apache 微服务
《聊聊分布式》分布式系统基石:深入理解CAP理论及其工程实践
CAP理论指出分布式系统中一致性、可用性、分区容错性三者不可兼得,必须根据业务需求进行权衡。实际应用中,不同场景选择不同策略:金融系统重一致(CP),社交应用重可用(AP),内网系统可选CA。现代架构更趋向动态调整与混合策略,灵活应对复杂需求。
|
1月前
|
消息中间件 运维 监控
《聊聊分布式》BASE理论 分布式系统可用性与一致性的工程平衡艺术
BASE理论是对CAP定理中可用性与分区容错性的实践延伸,通过“基本可用、软状态、最终一致性”三大核心,解决分布式系统中ACID模型的性能瓶颈。它以业务为导向,在保证系统高可用的同时,合理放宽强一致性要求,并借助补偿机制、消息队列等技术实现数据最终一致,广泛应用于电商、社交、外卖等大规模互联网场景。
|
1月前
|
算法 NoSQL 关系型数据库
《聊聊分布式》分布式系统核心概念
分布式系统由多节点协同工作,突破单机瓶颈,提升可用性与扩展性。CAP定理指出一致性、可用性、分区容错性三者不可兼得,BASE理论通过基本可用、软状态、最终一致性实现工程平衡,共识算法如Raft保障数据一致与系统可靠。
|
2月前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
202 0
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
9月前
|
NoSQL Java 中间件
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
本文介绍了从单机锁到分布式锁的演变,重点探讨了使用Redis实现分布式锁的方法。分布式锁用于控制分布式系统中多个实例对共享资源的同步访问,需满足互斥性、可重入性、锁超时防死锁和锁释放正确防误删等特性。文章通过具体示例展示了如何利用Redis的`setnx`命令实现加锁,并分析了简化版分布式锁存在的问题,如锁超时和误删。为了解决这些问题,文中提出了设置锁过期时间和在解锁前验证持有锁的线程身份的优化方案。最后指出,尽管当前设计已解决部分问题,但仍存在进一步优化的空间,将在后续章节继续探讨。
1235 131
【📕分布式锁通关指南 02】基于Redis实现的分布式锁
|
6月前
|
Apache
分布式锁—7.Curator的分布式锁
本文详细解析了Apache Curator库中多种分布式锁的实现机制,包括可重入锁、非可重入锁、可重入读写锁、MultiLock和Semaphore。可重入锁通过InterProcessMutex实现,支持同一线程多次加锁,锁的获取和释放通过Zookeeper的临时顺序节点实现。非可重入锁InterProcessSemaphoreMutex基于Semaphore实现,确保同一时间只有一个线程获取锁。可重入读写锁InterProcessReadWriteLock通过组合读锁和写锁实现,支持读写分离。Multi
|
9月前
|
NoSQL Java 测试技术
【📕分布式锁通关指南 05】通过redisson实现分布式锁
本文介绍了如何使用Redisson框架在SpringBoot中实现分布式锁,简化了之前通过Redis手动实现分布式锁的复杂性和不完美之处。Redisson作为Redis的高性能客户端,封装了多种锁的实现,使得开发者只需关注业务逻辑。文中详细展示了引入依赖、配置Redisson客户端、实现扣减库存功能的代码示例,并通过JMeter压测验证了其正确性。后续篇章将深入解析Redisson锁实现的源码。
290 0
【📕分布式锁通关指南 05】通过redisson实现分布式锁
|
9月前
|
运维 NoSQL 算法
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
本文深入探讨了基于Redis实现分布式锁时遇到的细节问题及解决方案。首先,针对锁续期问题,提出了通过独立服务、获取锁进程自己续期和异步线程三种方式,并详细介绍了如何利用Lua脚本和守护线程实现自动续期。接着,解决了锁阻塞问题,引入了带超时时间的`tryLock`机制,确保在高并发场景下不会无限等待锁。最后,作为知识扩展,讲解了RedLock算法原理及其在实际业务中的局限性。文章强调,在并发量不高的场景中手写分布式锁可行,但推荐使用更成熟的Redisson框架来实现分布式锁,以保证系统的稳定性和可靠性。
542 0
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
|
10月前
|
消息中间件 算法 调度
分布式系统学习10:分布式事务
本文是小卷关于分布式系统架构学习系列的第13篇,重点探讨了分布式事务的相关知识。随着业务增长,单体架构拆分为微服务后,传统的本地事务无法满足需求,因此需要引入分布式事务来保证数据一致性。文中详细介绍了分布式事务的必要性、实现方案及其优缺点,包括刚性事务(如2PC、3PC)和柔性事务(如TCC、Saga、本地消息表、MQ事务、最大努力通知)。同时,还介绍了Seata框架作为开源的分布式事务解决方案,提供了多种事务模式,简化了分布式事务的实现。
460 5

热门文章

最新文章