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

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 本文介绍了分布式系统中分布式锁的概念、实现方式及其应用场景。分布式锁用于在多个独立的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 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。
相关文章
|
16天前
|
供应链 监控 安全
对话|企业如何构建更完善的容器供应链安全防护体系
阿里云与企业共筑容器供应链安全
171338 13
|
18天前
|
供应链 监控 安全
对话|企业如何构建更完善的容器供应链安全防护体系
随着云计算和DevOps的兴起,容器技术和自动化在软件开发中扮演着愈发重要的角色,但也带来了新的安全挑战。阿里云针对这些挑战,组织了一场关于云上安全的深度访谈,邀请了内部专家穆寰、匡大虎和黄竹刚,深入探讨了容器安全与软件供应链安全的关系,分析了当前的安全隐患及应对策略,并介绍了阿里云提供的安全解决方案,包括容器镜像服务ACR、容器服务ACK、网格服务ASM等,旨在帮助企业构建涵盖整个软件开发生命周期的安全防护体系。通过加强基础设施安全性、技术创新以及倡导协同安全理念,阿里云致力于与客户共同建设更加安全可靠的软件供应链环境。
150296 32
|
26天前
|
弹性计算 人工智能 安全
对话 | ECS如何构筑企业上云的第一道安全防线
随着中小企业加速上云,数据泄露、网络攻击等安全威胁日益严重。阿里云推出深度访谈栏目,汇聚产品技术专家,探讨云上安全问题及应对策略。首期节目聚焦ECS安全性,提出三道防线:数据安全、网络安全和身份认证与权限管理,确保用户在云端的数据主权和业务稳定。此外,阿里云还推出了“ECS 99套餐”,以高性价比提供全面的安全保障,帮助中小企业安全上云。
201962 14
对话 | ECS如何构筑企业上云的第一道安全防线
|
4天前
|
机器学习/深度学习 自然语言处理 PyTorch
深入剖析Transformer架构中的多头注意力机制
多头注意力机制(Multi-Head Attention)是Transformer模型中的核心组件,通过并行运行多个独立的注意力机制,捕捉输入序列中不同子空间的语义关联。每个“头”独立处理Query、Key和Value矩阵,经过缩放点积注意力运算后,所有头的输出被拼接并通过线性层融合,最终生成更全面的表示。多头注意力不仅增强了模型对复杂依赖关系的理解,还在自然语言处理任务如机器翻译和阅读理解中表现出色。通过多头自注意力机制,模型在同一序列内部进行多角度的注意力计算,进一步提升了表达能力和泛化性能。
|
8天前
|
存储 人工智能 安全
对话|无影如何助力企业构建办公安全防护体系
阿里云无影助力企业构建办公安全防护体系
1254 10
|
10天前
|
机器学习/深度学习 自然语言处理 搜索推荐
自注意力机制全解析:从原理到计算细节,一文尽览!
自注意力机制(Self-Attention)最早可追溯至20世纪70年代的神经网络研究,但直到2017年Google Brain团队提出Transformer架构后才广泛应用于深度学习。它通过计算序列内部元素间的相关性,捕捉复杂依赖关系,并支持并行化训练,显著提升了处理长文本和序列数据的能力。相比传统的RNN、LSTM和GRU,自注意力机制在自然语言处理(NLP)、计算机视觉、语音识别及推荐系统等领域展现出卓越性能。其核心步骤包括生成查询(Q)、键(K)和值(V)向量,计算缩放点积注意力得分,应用Softmax归一化,以及加权求和生成输出。自注意力机制提高了模型的表达能力,带来了更精准的服务。
|
9天前
|
人工智能 自然语言处理 程序员
通义灵码2.0全新升级,AI程序员全面开放使用
通义灵码2.0来了,成为全球首个同时上线JetBrains和VSCode的AI 程序员产品!立即下载更新最新插件使用。
1358 24
|
9天前
|
消息中间件 人工智能 运维
1月更文特别场——寻找用云高手,分享云&AI实践
我们寻找你,用云高手,欢迎分享你的真知灼见!
682 28
1月更文特别场——寻找用云高手,分享云&AI实践
|
14天前
|
人工智能 自然语言处理 API
阿里云百炼xWaytoAGI共学课DAY1 - 必须了解的企业级AI应用开发知识点
本课程旨在介绍阿里云百炼大模型平台的核心功能和应用场景,帮助开发者和技术小白快速上手,体验AI的强大能力,并探索企业级AI应用开发的可能性。
|
8天前
|
机器学习/深度学习 人工智能 自然语言处理