1 介绍
这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁。会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁。
本篇文章会将分布式锁的实现分为两部分,一个是单机环境,另一个是集群环境下的Redis锁实现。在介绍分布式锁的实现之前,先来了解下分布式锁的一些信息。
开始之前,记得点赞收藏加关注哦 ,需要下载PDF版本和获取更多知识点、面试题的朋友可以点一点下方链接免费领取
链接:点这里!!! 799215493 暗号:CSDN
2 分布式锁
2.1 什么是分布式锁?
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
2.2 分布式锁需要具备哪些条件
- 互斥性:在任意一个时刻,只有一个客户端持有锁。
- 无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
- 容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁
2.3 分布式锁的实现有哪些?
- 数据库
- Memcached(add命令)
- Redis(setnx命令)
- Zookeeper(临时节点)
- 等等
3 单机Redis的分布式锁
3.1 准备工作
3.1.1 定义常量类
public class LockConstants {
public static final String OK = "OK";
/** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/
public static final String NOT_EXIST = "NX";
public static final String EXIST = "XX";
/** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/
public static final String SECONDS = "EX";
public static final String MILLISECONDS = "PX";
private LockConstants() {
}
}
3.1.2 定义锁的抽象类
抽象类RedisLock实现java.util.concurrent包下的Lock接口,然后对一些方法提供默认实现,子类只需实现lock方法和unlock方法即可。代码如下
public abstract class RedisLock implements Lock {
protected Jedis jedis;
protected String lockKey;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey);
}
public void sleepBySencond(int sencond){
try {
Thread.sleep(sencond*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void lockInterruptibly(){
}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
}
3.2 最基础的版本1
先来一个最基础的版本,代码如下
public class LockCase1 extends RedisLock {
public LockCase1(Jedis jedis, String name) {
super(jedis, name);
}
@Override
public void lock() {
while(true){
String result = jedis.set(lockKey, "value", NOT_EXIST);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
@Override
public void unlock() {
jedis.del(lockKey);
}
}
LockCase1类提供了lock和unlock方法。
其中lock方法也就是在reids客户端执行如下命令
SET lockKey value NX
而unlock方法就是调用DEL命令将键删除。
好了,方法介绍完了。现在来想想这其中会有什么问题?
假设有两个客户端A和B,A获取到分布式的锁。A执行了一会,突然A所在的服务器断电了(或者其他什么的),也就是客户端A挂了。这时出现一个问题,这个锁一直存在,且不会被释放,其他客户端永远获取不到锁。如下示意图
可以通过设置过期时间来解决这个问题
3.3 版本2-设置锁的过期时间
public void lock() {
while(true){
String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
类似的Redis命令如下
SET lockKey value NX EX 30
注:要保证设置过期时间和设置锁具有原子性
这时又出现一个问题,问题出现的步骤如下
- 客户端A获取锁成功,过期时间30秒。
- 客户端A在某个操作上阻塞了50秒。
- 30秒时间到了,锁自动释放了。
- 客户端B获取到了对应同一个资源的锁。
- 客户端A从阻塞中恢复过来,释放掉了客户端B持有的锁。
示意图如下
这时会有两个问题
- 过期时间如何保证大于业务执行时间?
- 如何保证锁不会被误删除?
先来解决如何保证锁不会被误删除这个问题。
这个问题可以通过设置value为当前客户端生成的一个随机字符串,且保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。
3.4 版本3-设置锁的value
抽象类RedisLock增加lockValue字段,lockValue字段的默认值为UUID随机值假设当前线程ID。
public abstract class RedisLock implements Lock {
//...
protected String lockValue;
public RedisLock(Jedis jedis,String lockKey) {
this(jedis, lockKey, UUID.randomUUID().toString()+Thread.currentThread().getId());
}
public RedisLock(Jedis jedis, String lockKey, String lockValue) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
}
//...
}
加锁代码
public void lock() {
while(true){
String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECONDS,30);
if(OK.equals(result)){
System.out.println(Thread.currentThread().getId()+"加锁成功!");
break;
}
}
}
解锁代码
public void unlock() {
String lockValue = jedis.get(lockKey);
if (lockValue.equals(lockValue)){
jedis.del(lockKey);
}
}
这时看看加锁代码,好像没有什么问题啊。
再来看看解锁的代码,这里的解锁操作包含三步操作:获取值、判断和删除锁。这时你有没有想到在多线程环境下的i++操作?
3.4.1 i++问题
i++操作也可分为三个步骤:读i的值,进行i+1,设置i的值。
如果两个线程同时对i进行i++操作,会出现如下情况
- i设置值为0
- 线程A读到i的值为0
- 线程B也读到i的值为0
- 线程A执行了+1操作,将结果值1写入到内存
- 线程B执行了+1操作,将结果值1写入到内存
- 此时i进行了两次i++操作,但是结果却为1
在多线程环境下有什么方式可以避免这类情况发生?
解决方式有很多种,例如用AtomicInteger、CAS、synchronized等等。
这些解决方式的目的都是要确保i++ 操作的原子性。那么回过头来看看解锁,同理我们也是要确保解锁的原子性。我们可以利用Redis的lua脚本来实现解锁操作的原子性。
3.5 版本4-具有原子性的释放锁
3.6 版本5-确保过期时间大于业务执行时间
3.7 测试
累了,不想写了,需要完整版的朋友点一点下方链接自己领取
链接:点这里!!! 799215493 暗号:CSDN
4 集群Redis的分布式锁
在Redis的分布式环境中,Redis 的作者提供了RedLock 的算法来实现一个分布式锁。
4.1 加锁
RedLock算法加锁步骤如下
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
4.2 解锁
向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.
总结
我这里准备了一线大厂面试资料和我原创的超硬核PDF技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作!
有需要的朋友可以点一点下方链接免费领取
链接:点这里!!! 799215493 暗号:CSDN
来源:oschina
链接:https://my.oschina.net/u/4349518/blog/4743710