「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」
分布式锁的简介
锁 是一种用来解决多个执行线程 访问共享资源 错误或数据不一致问题的工具。
锁的本质:同一时间只允许一个用户操作共享数据。
为什么需要分布式锁
一般情况下,我们使用分布式锁主要有两个场景:
- 避免不同节点重复相同的工作:比如用户执行了某个操作需要输入验证码不同节点没有相互通信会发送多条验证码;
- 避免破坏数据的正确性:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现;
Java 中实现分布式锁的常见方式
- 基于 MySQL 中的锁:
MySQL
本身有自带的悲观锁for update
关键字,也可以自己实现悲观/乐观锁来达到目的; - 基于 Zookeeper 有序节点:
Zookeeper
允许临时创建有序的子节点,这样客户端获取节点列表时,就能够根据当前子节点列表中的序号来判断是否能够获得锁; - 基于 Redis 的单线程:由于
Redis
是单线程,所以命令会以串行的方式执行,并且本身提供了像SETNX(set if not exists)
这样的指令,并且还扩展了SET
命令;
每个方案都有各自的优缺点,例如
MySQL
虽然直观理解容易,但是实现起来却需要额外考虑 锁超时、加事务 等问题,并且性能局限于数据库;这边我们以Redis
为主进行分析。
分布式锁应该具备的特性
- 原子性
- 互斥性
- 独占性:自己的锁只能自己解开
- 可重入性
- 超时与续期
分布式锁特性场景解析
- 超时场景说明:
假如有两个服务 A 和 B,其中服务 A 在 获取锁之后 由于不可抗力因素宕机了(例如:机房停电),因为锁被服务 A 持有,就会导致 B 服务就永远无法获取到锁了,这样显然是不合理的,所以我们需要额外设置一个超时时间,来保证避免这种情况的发生。
2. 独占性场景的说明
延续上面的场景,我们在考虑这种场景,如果在加锁和释放锁之间的逻辑比较复杂,执行时间较长,以至于超出了锁的超时限制,也会出现问题。这时候线程 A 持有锁过期了,而临界区的逻辑还没有执行完,因为锁过期了,所以 Redis
会自动将这个锁对应 key
给删除掉;这个时候线程 B 就可以获得这个分布式锁,当线程 B 刚获取到自己的锁,原本超时的 A 执行到释放自己锁的代码,A 的锁其实已经过期了现在的 key
是 B 的锁,A 现在就会把 B 的锁给释放掉,其实 B 才刚刚获取到锁还没有执行自己的逻辑,所以锁应该有独占性,自己的锁应该只能自己解开。
实现方式:将锁的 value
值设置为一个随机串,释放锁时先匹配随机串是否一致,然后再删除 key
。
3. 续期场景说明
为了避免线程没有处理完自己业务就过期的问题,加锁时,先设置一个过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源处理业务逻辑还没有完成,那么就自动对锁进行 续期操作,重新设置过期时间。 Redisson
使用的就是这种机制,它将其成为看门狗。
4. 原子性方面
匹配 value
和删除 key
在 Redis
中并不是一个原子性的操作,也没有类似保证原子性的指令,所以可能需要使用像 Lua
这样的脚本来处理了,因为 Lua
脚本可以 保证多个指令的原子性执行。
5. 互斥性方面
一个 key
被一个实例获取之后,其他的实例就不能再次获取了。
6. 可重入性方面
同一个线程方法 A 调用方法 B ,A B 都需要获得锁,A 获得锁之后,执行自己逻辑调用 B ,如果不是重入锁的话,就会发生死锁。
在 Java
编程中 synchronized
和 ReentrantLock
都是可重入锁
使用 Redis 实现分布式锁
Redis 相关命令解析
SETEX
:
1 | vbnet复制代码语法:SETEX KEY_NAME TIMEOUT VALUE |
SETNX
: (SET if Not eXists
)
1 | vbnet复制代码语法:SETNX KEY_NAME VALUE |
我们注意到 setnx
是可以满足我们没有值时候设置成功,有值的时候设置失败的需求的,但是了解完上面章节中介绍的分布式锁应该考虑的问题这边应该需要设置一个超时时间,所以在 Redis 2.6.12
之后扩展了 set
命令:
SET
:
1 | scss复制代码# 一条命令保证原子性执行 在 setnx 的基础上 设置超时时间 |
自定义 Redis 分布式锁
上文中提到 SETNX
是原子性操作,但是没有办法同时完成 EXPIRE
操作,不能保证 SETNX
和 EXPIRE
的原子性。这个可以使用 SET
命令来实现并且保证原子性。
1 | java复制代码package com.aha.train.test.lock.distributed; |
测试自定义分布式锁
1 | java复制代码package com.aha.train.test.lock.distributed; |
Redisson 实现分布式锁
导入 Redisson
的依赖
1 | xml复制代码<!-- Redisson --> |
编写配置文件
1 | yaml复制代码server: |
测试分布式锁
1 | Java复制代码package com.aha.distributedlock.redisson; |
思考:Redis 实现的分布式锁当主从切换的时候依旧安全吗
本文转载自: 掘金