基于redis的分布式锁

1 介绍

这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁。会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁。

本篇文章会将分布式锁的实现分为两部分,一个是单机环境,另一个是集群环境下的Redis锁实现。在介绍分布式锁的实现之前,先来了解下分布式锁的一些信息。

2 分布式锁

2.1 什么是分布式锁?

分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。

2.2 分布式锁需要具备哪些条件

  1. 互斥性:在任意一个时刻,只有一个客户端持有锁。
  2. 无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
  3. 容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁

2.4 分布式锁的实现有哪些?

  1. 数据库
  2. Memcached(add命令)
  3. Redis(setnx命令)
  4. Zookeeper(临时节点)
  5. 等等

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
复制代码

注:要保证设置过期时间和设置锁具有原子性

这时又出现一个问题,问题出现的步骤如下

  1. 客户端A获取锁成功,过期时间30秒。
  2. 客户端A在某个操作上阻塞了50秒。
  3. 30秒时间到了,锁自动释放了。
  4. 客户端B获取到了对应同一个资源的锁。
  5. 客户端A从阻塞中恢复过来,释放掉了客户端B持有的锁。

示意图如下

这时会有两个问题

  1. 过期时间如何保证大于业务执行时间?
  2. 如何保证锁不会被误删除?

先来解决如何保证锁不会被误删除这个问题。
这个问题可以通过设置value为当前客户端生成的一个随机字符串,且保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。

版本2的完整代码:Github地址

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++操作,会出现如下情况

  1. i设置值为0
  2. 线程A读到i的值为0
  3. 线程B也读到i的值为0
  4. 线程A执行了+1操作,将结果值1写入到内存
  5. 线程B执行了+1操作,将结果值1写入到内存
  6. 此时i进行了两次i++操作,但是结果却为1

在多线程环境下有什么方式可以避免这类情况发生?
解决方式有很多种,例如用AtomicInteger、CAS、synchronized等等。
这些解决方式的目的都是要确保i++ 操作的原子性。那么回过头来看看解锁,同理我们也是要确保解锁的原子性。我们可以利用Redis的lua脚本来实现解锁操作的原子性。

版本3的完整代码:Github地址

3.5 版本4-具有原子性的释放锁

lua脚本内容如下

if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
复制代码

这段Lua脚本在执行的时候要把的lockValue作为ARGV[1]的值传进去,把lockKey作为KEYS[1]的值传进去。现在来看看解锁的java代码

public void unlock() {
// 使用lua脚本进行原子删除操作
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}
复制代码

好了,解锁操作也确保了原子性了,那么是不是单机Redis环境的分布式锁到此就完成了?
别忘了版本2-设置锁的过期时间还有一个,过期时间如何保证大于业务执行时间问题没有解决。

版本4的完整代码:Github地址

3.6 版本5-确保过期时间大于业务执行时间

抽象类RedisLock增加一个boolean类型的属性isOpenExpirationRenewal,用来标识是否开启定时刷新过期时间。
在增加一个scheduleExpirationRenewal方法用于开启刷新过期时间的线程。

public abstract class RedisLock implements Lock {
//... protected volatile boolean isOpenExpirationRenewal = true; /**
* 开启定时刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
} /**
* 刷新key的过期时间
*/
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
System.out.println("执行延迟失效时间中..."); String checkAndExpireScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 end";
jedis.eval(checkAndExpireScript, 1, lockKey, lockValue, "30"); //休眠10秒
sleepBySencond(10);
}
}
}
}
复制代码

加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal方法,开启刷新过期时间的线程。

public void lock() {
while (true) {
String result = jedis.set(lockKey, lockValue, NOT_EXIST, SECONDS, 30);
if (OK.equals(result)) {
System.out.println("线程id:"+Thread.currentThread().getId() + "加锁成功!时间:"+LocalTime.now()); //开启定时刷新过期时间
isOpenExpirationRenewal = true;
scheduleExpirationRenewal();
break;
}
System.out.println("线程id:"+Thread.currentThread().getId() + "获取锁失败,休眠10秒!时间:"+LocalTime.now());
//休眠10秒
sleepBySencond(10);
}
}
复制代码

解锁代码增加一行代码,将isOpenExpirationRenewal属性置为false,停止刷新过期时间的线程轮询。

public void unlock() {
//...
isOpenExpirationRenewal = false;
} 复制代码

版本5的完整代码:Github地址

3.7 测试

测试代码如下

public void testLockCase5() {
//定义线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(0, 10,
1, TimeUnit.SECONDS,
new SynchronousQueue<>()); //添加10个线程获取锁
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
try {
Jedis jedis = new Jedis("localhost");
LockCase5 lock = new LockCase5(jedis, lockName);
lock.lock(); //模拟业务执行15秒
lock.sleepBySencond(15); lock.unlock();
} catch (Exception e){
e.printStackTrace();
}
});
} //当线程池中的线程数为0时,退出
while (pool.getPoolSize() != 0) {}
}
复制代码

测试结果

或许到这里基于单机Redis环境的分布式就介绍完了。但是使用java的同学有没有发现一个锁的重要特性

那就是锁的重入,那么分布式锁的重入该如何实现呢?这里就留一个坑了

4 集群Redis的分布式锁

在Redis的分布式环境中,Redis 的作者提供了RedLock 的算法来实现一个分布式锁。

4.1 加锁

RedLock算法加锁步骤如下

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

4.2 解锁

向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.


关于RedLock算法,还有一个小插曲,就是Martin Kleppmann 和 RedLock 作者 antirez的对RedLock算法的互怼。 官网原话如下

Martin Kleppmann analyzed Redlock here. I disagree with the analysis and posted my reply to his analysis here.

更多关于RedLock算法这里就不在说明,有兴趣的可以到官网阅读相关文章。

5 总结

这篇文章讲述了一个基于Redis的分布式锁的编写过程及解决问题的思路,但是本篇文章实现的分布式锁并不适合用于生产环境。java环境有 Redisson 可用于生产环境,但是分布式锁还是Zookeeper会比较好一些(可以看Martin Kleppmann 和 RedLock的分析)。

Martin Kleppmann对RedLock的分析:martin.kleppmann.com/2016/02/08/…

RedLock 作者 antirez的回应:antirez.com/news/101

整个项目的地址存放在Github上,有需要的可以看看:Github地址

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 基于redis的分布式锁

基于redis的分布式锁(转)的更多相关文章

  1. 基于redis 实现分布式锁的方案

    在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...

  2. 基于redis的分布式锁

    <?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * ...

  3. 基于Redis的分布式锁真的安全吗?

    说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...

  4. 基于 Redis 的分布式锁

    前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...

  5. 基于redis的分布式锁实现

    1.分布式锁介绍 在计算机系统中,锁作为一种控制并发的机制无处不在. 单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为.而在如今的大型复杂系统中,通常采用的是分布式架构提供服务 ...

  6. 基于redis的分布式锁(不适合用于生产环境)

    基于redis的分布式锁 1 介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分 ...

  7. 基于 redis 的分布式锁实现 Distributed locks with Redis debug 排查错误

    小结: 1. 锁的实现方式,按照应用的实现架构,可能会有以下几种类型: 如果处理程序是单进程多线程的,在 python下,就可以使用 threading 模块的 Lock 对象来限制对共享变量的同步访 ...

  8. 转载:基于Redis实现分布式锁

    转载:基于Redis实现分布式锁  ,出处: http://blog.csdn.net/ugg/article/details/41894947 背景在很多互联网产品应用中,有些场景需要加锁处理,比如 ...

  9. redis系列:基于redis的分布式锁

    一.介绍 这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁.会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁. 本篇文章会将分布式锁的实现分为两部分,一个是单机环境, ...

随机推荐

  1. [js]练习绘制拓扑图

  2. 连接oracle数据库

    一.连接oracle数据库 一.windows环境 oracle windows客户端下载地址:http://www.oracle.com/technetwork/topics/winx64soft- ...

  3. 一HTML基础知识

    网站(前段项目)的目录结构及命名 网站的结构:网站是存放在服务器上的一个文件夹(根目录),是网站所有文件的集合.网站中所有文件按照文件类型或功能分门别类的整理存放. 网站命名规则:网站中的所有文件命名 ...

  4. 牛客(web 1)

    bootstrap(Web框架) 有关换行的知识: http://www.cnblogs.com/wqsbk/p/3493948.html 关于link加载问题: link是同时加载的,script标 ...

  5. python类与对象-如何为创建大量实例节省内存

    如何为创建大量实例节省内存 问题举例 在网络游戏中,定义玩家类Player(id, name, level...), 每个玩家在线将创建一个Player实例,当在线人数很多时,将产生大量实例, 如何降 ...

  6. Repeater 实现 OnSelectedIndexChanged

    在Repeater中使用DropDownList的方法   在Repeater中使用DropDownList的方法 以下代码并不完整,只记录了关键的方法 aspx代码中 假设这是一个用户管理的系统的模 ...

  7. SpringMVC和Struts2的区别及优势

    1.SpringMVC和Struts2的区别比较 1.Struts2是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方 ...

  8. redis 无序集合(set)函数

    sAdd 命令/方法/函数 Adds a value to the set value stored at key. If this value is already in the set, FALS ...

  9. window xshell 连接本地ubuntu虚拟机

    先设置VMware 虚拟机的连接属性 1.桥接,利用真实网卡  设置和window 同一个网段就可以直接通信 2.hostnoly是通过vm8(查看你的所有网络连接) 只能和主机联系 设置和vm8同一 ...

  10. python垃圾回收机制与小整数池

    python垃圾回收机制 当引用计数为0时,python会删除这个值. 引用计数 x = 10 y = x del x print(y) 10 引用计数+1,引用计数+1,引用计数-1,此时引用计数为 ...