Redis分布式锁的7种实现


Posted in Redis onApril 01, 2022

分布式锁介绍

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

一把靠谱的分布式锁应该有如下特征:

  • 互斥性:任意时刻,只有一个客户端能持有锁。
  • 锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
  • 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
  • 高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
  • 安全性:锁只能被持有的客户端删除,不能被其他客户端删除。

方案一:SETNX + EXPIRE

Redis的分布式锁最简单的实现方式为setnx+ expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。

SETNX 是SET IF NOT EXISTS的简写。日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。

假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下:

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
    expire(key_resource_id,100); //设置过期时间
    try {
        do something  //业务请求
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

但是这个方案中,setnx和expire两个命令分开了,不是原子操作。如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash或者要重启维护了,别的线程永远获取不到锁啦

方案二:SETNX + value值是(系统时间+过期时间)

为了解决方案一发生异常锁得不到释放的场景,有小伙伴认为,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。伪代码如下:

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间
String expiresStr = String.valueOf(expires);

// 如果当前锁不存在,返回加锁成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果锁已经存在,获取锁的过期时间
String currentValueStr = jedis.get(key_resource_id);

// 如果获取到的过期时间,小于系统当前时间,表示已经过期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

     // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁
         return true;
    }
}
        
//其他情况,均返回加锁失败
return false;
}

这个方案的优点是,巧妙移除expire单独设置过期时间的操作,把过期时间放到setnx的value值里面来。但是这个方案还有别的缺点:

  • 过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。
  • 如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
  • 该锁没有保存持有者的唯一标识,可能被别的客户端释放/解锁。

方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)

Redis 通过 LUA 脚本创建具有原子性的命令: 当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。

在Redis中执行Lua脚本有两种方法:eval和evalsha。eval命令使用内置的 Lua 解释器,对 Lua 脚本进行求值,例子如下:

//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

因此我们可以使用LUA脚本实现分布式锁,伪代码如下:

//LUA脚本
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

//加锁
 String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
//判断是否成功
return result.equals(1L);

方案四:SET的扩展命令(SET EX PX NX)

Redis的SET指令扩展参数也可以保证指令的原子性!

SET key value[EX seconds][PX milliseconds][NX|XX]
NX:表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
EX seconds:设定key的过期时间,时间单位是秒。
PX milliseconds:设定key的过期时间,单位为毫秒
XX:仅当key存在时设置值

伪代码如下:

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

但是呢,这个方案还是可能存在问题:

  • 锁过期释放了,业务还没执行完。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
  • 锁被别的线程误删。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢。

方案五:SET EX PX NX + 校验唯一随机值,再释放锁

既然锁可能被别的线程误删,那我们给value值设置一个标记当前线程唯一的随机数,在删除的时候,校验一下,不就OK了嘛。伪代码如下:

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       //判断是不是当前线程加的锁,是才释放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
        }
    }
}

在这里,判断是不是当前线程加的锁和释放锁不是一个原子操作。这可能这把锁已经不属于当前客户端,会解除他人加的锁。

为了更严谨,一般也是用lua脚本代替。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

方案六: 开源框架Redisson

方案五还是可能存在锁过期释放但业务没执行完的问题。为了解决这个问题,我们可以给获得锁的线程开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

当前开源框架Redisson就是这样实现的,Redisson底层原理图如下:

Redis分布式锁的7种实现

只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson解决了锁过期释放但业务没执行完的问题

方案七:多机实现的分布式锁Redlock

前面六种方案都只是基于单机版的讨论,还不是很完美。其实Redis一般都是集群部署的:

Redis分布式锁的7种实现

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

为了解决这个问题,Redis提出一种高级的分布式锁算法:Redlock。我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例,如下图所示:

Redis分布式锁的7种实现

则RedLock的实现步骤如下:

  • 按顺序向5个master节点请求加锁。
  • 根据设置的超时时间来判断,是不是要跳过该master节点。
  • 如果大于等于3个节点(N/2+1,这里是5/2+1=3个节点)加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
  • 如果获取锁失败,解锁!

 到此这篇关于Redis分布式锁的7种实现的文章就介绍到这了,更多相关Redis分布式锁内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
redis限流的实际应用
Apr 24 Redis
浅谈redis缓存在项目中的使用
May 20 Redis
redis实现共同好友的思路详解
May 26 Redis
你真的了解redis为什么要提供pipeline功能
Jun 22 Redis
Redis主从配置和底层实现原理解析(实战记录)
Jun 30 Redis
厉害!这是Redis可视化工具最全的横向评测
Jul 15 Redis
Redis Cluster 集群搭建你会吗
Aug 04 Redis
Redis如何实现分布式锁
Aug 23 Redis
redis缓存存储Session原理机制
Nov 20 Redis
Springboot/Springcloud项目集成redis进行存取的过程解析
Dec 04 Redis
Redis 异步机制
May 15 Redis
Redis基本数据类型Set常用操作命令
Jun 01 Redis
Redis 哨兵机制及配置实现
Redis如何使用乐观锁(CAS)保证数据一致性
Mar 25 #Redis
Redis 操作多个数据库的配置的方法实现
Mar 23 #Redis
Redis安装使用RedisJSON模块的方法
Mar 23 #Redis
解决redis批量删除key值的问题
Mar 23 #Redis
源码分析Redis中 set 和 sorted set 的使用方法
Redis监控工具RedisInsight安装与使用
You might like
PHP下打开URL地址的几种方法小结
2010/05/16 PHP
phpMyAdmin 链接表的附加功能尚未激活的问题
2010/08/01 PHP
php输出反斜杠的实例方法
2019/09/19 PHP
用javascript实现画板的代码
2007/09/05 Javascript
JQuery 操作Javascript对象和数组的工具函数小结
2010/01/22 Javascript
js利用与或运算符优先级实现if else条件判断表达式
2010/04/15 Javascript
js获取当前日期代码适用于网页头部
2013/06/27 Javascript
Javascript中查找不以XX字符结尾的单词示例代码
2013/10/15 Javascript
js传参数受特殊字符影响错误的解决方法
2013/10/21 Javascript
Jquery异步提交表单代码分享
2015/03/26 Javascript
深入解析桶排序算法及Node.js上JavaScript的代码实现
2016/07/06 Javascript
Three.js学习之网格
2016/08/10 Javascript
详解Vue 普通对象数据更新与 file 对象数据更新
2017/04/26 Javascript
vue-router单页面路由
2017/06/17 Javascript
jQuery remove()过滤被删除的元素(推荐)
2017/07/18 jQuery
Vue 让元素抖动/摆动起来的实现代码
2018/05/31 Javascript
Vue+axios实现统一接口管理的方法
2018/07/23 Javascript
Vue-cli项目部署到Nginx服务器的方法
2019/11/01 Javascript
微信小程序入门之指南针
2020/10/22 Javascript
NodeJS模块Buffer原理及使用方法解析
2020/11/11 NodeJs
[01:36:17]DOTA2-DPC中国联赛 正赛 Ehome vs iG BO3 第一场 1月31日
2021/03/11 DOTA
python处理Excel xlrd的简单使用
2017/09/12 Python
python中reload(module)的用法示例详解
2017/09/15 Python
python实现一组典型数据格式转换
2018/12/15 Python
pytorch载入预训练模型后,实现训练指定层
2020/01/06 Python
win10下opencv-python特定版本手动安装与pip自动安装教程
2020/03/05 Python
Python如何生成xml文件
2020/06/04 Python
解决python和pycharm安装gmpy2 出现ERROR的问题
2020/08/28 Python
自我评价中英文语句
2013/11/30 职场文书
土木工程专业个人求职信
2013/12/30 职场文书
优秀教师先进个人事迹材料
2014/08/31 职场文书
学校领导四风问题整改措施思想汇报
2014/10/09 职场文书
我的法兰西岁月观后感
2015/06/09 职场文书
python入门之算法学习
2021/04/22 Python
十个Python自动化常用操作,即拿即用
2021/05/10 Python
Windows Server 2019 域控制器安装图文教程
2022/04/28 Servers