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 12 Redis
SpringBoot 集成Redis 过程
Jun 02 Redis
聊一聊Redis与MySQL双写一致性如何保证
Jun 26 Redis
redis客户端实现高可用读写分离的方式详解
Jul 04 Redis
厉害!这是Redis可视化工具最全的横向评测
Jul 15 Redis
Redis Cluster 集群搭建你会吗
Aug 04 Redis
Redis的字符串是如何实现的
Oct 24 Redis
SpringBoot整合Redis入门之缓存数据的方法
Nov 17 Redis
浅谈Redis跟MySQL的双写问题解决方案
Feb 24 Redis
一文搞懂Redis中String数据类型
Apr 03 Redis
基于Redission的分布式锁实战
Aug 14 Redis
Redis配置外网可访问(redis远程连接不上)的方法
Dec 24 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开发规范手册之PHP代码规范详解
2011/01/13 PHP
PHP中调用ASP.NET的WebService的代码
2011/04/22 PHP
php实现多站点共用session实现单点登录的方法详解
2019/09/18 PHP
jQuery 连续列表实现代码
2009/12/21 Javascript
去掉gridPanel表头全选框的小例子
2013/07/18 Javascript
解析JavaScript中instanceof对于不同的构造器或许都返回true
2013/12/03 Javascript
JavaScript实现页面5秒后自动跳转的方法
2015/04/16 Javascript
js计算系统当前日期是星期几的方法
2016/07/14 Javascript
Javascript点击按钮随机改变数字与其颜色
2016/09/01 Javascript
jQuery tagsinput在h5邮件客户端中应用详解
2016/09/26 Javascript
JS高仿抛物线加入购物车特效实现代码
2017/02/20 Javascript
jQuery validata插件实现方法
2017/06/25 jQuery
详解10分钟学会vue滚动行为
2017/09/21 Javascript
详解利用Angular实现多团队模块化SPA开发框架
2017/11/27 Javascript
js计算两个时间差 天 时 分 秒 毫秒的代码
2019/05/21 Javascript
前端Vue项目详解--初始化及导航栏
2019/06/24 Javascript
Vue中遍历数组的新方法实例详解
2019/07/21 Javascript
如何通过javaScript去除字符串两端的空白字符
2020/02/06 Javascript
JS操作Fckeditor的一些常用方法(获取、插入等)
2020/02/19 Javascript
Python变量和数据类型详解
2017/02/15 Python
python 实现数组list 添加、修改、删除的方法
2018/04/04 Python
Flask模拟实现CSRF攻击的方法
2018/07/24 Python
详解python中Numpy的属性与创建矩阵
2018/09/10 Python
python 检查文件mime类型的方法
2018/12/08 Python
Python利用lxml模块爬取豆瓣读书排行榜的方法与分析
2019/04/15 Python
python logging 日志的级别调整方式
2020/02/21 Python
CSS3 实现穿梭星空动画
2020/11/13 HTML / CSS
护理自我鉴定范文
2013/10/06 职场文书
《姥姥的剪纸》教学反思
2014/02/25 职场文书
三年级学生评语
2014/04/23 职场文书
蛋糕店创业计划书范文
2014/09/21 职场文书
2016春季小学开学寄语
2015/12/03 职场文书
Python 高级库15 个让新手爱不释手(推荐)
2021/05/15 Python
SpringBoot工程下使用OpenFeign的坑及解决
2021/07/02 Java/Android
CSS Transition通过改变Height实现展开收起元素
2021/08/07 HTML / CSS
golang操作rocketmq的示例代码
2022/04/06 Golang