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
浅谈redis缓存在项目中的使用
May 20 Redis
分布式锁为什么要选择Zookeeper而不是Redis?看完这篇你就明白了
May 21 Redis
比较几种Redis集群方案
Jun 21 Redis
在redisCluster中模糊获取key方式
Jul 09 Redis
为什么RedisCluster设计成16384个槽
Sep 25 Redis
详解redis在微服务领域的贡献
Oct 16 Redis
SpringBoot集成Redis的思路详解
Oct 16 Redis
Spring Boot实战解决高并发数据入库之 Redis 缓存+MySQL 批量入库问题
Feb 12 Redis
Redis 中使用 list,streams,pub/sub 几种方式实现消息队列的问题
Mar 16 Redis
高并发下Redis如何保持数据一致性(避免读后写)
Mar 18 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 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根据数据id自动生成编号的实现方法
2016/10/16 PHP
AJAX的使用方法详解
2017/04/29 PHP
CI框架网页缓存简单用法分析
2018/12/26 PHP
基于PHP+Mysql简单实现了图书购物车系统的实例详解
2020/08/06 PHP
仅IE不支持setTimeout/setInterval函数的第三个以上参数
2011/05/25 Javascript
jquery中使用ajax获取远程页面信息
2011/11/13 Javascript
分享我对JS插件开发的一些感想和心得
2016/02/04 Javascript
基于BootStrap Metronic开发框架经验小结【五】Bootstrap File Input文件上传插件的用法详解
2016/05/12 Javascript
JS实现的跨浏览器解析XML文件实例
2016/06/21 Javascript
对Angular.js Controller如何进行单元测试
2016/10/25 Javascript
遍历json获得数据的几种方法小结
2017/01/21 Javascript
JS使用Date对象实时显示当前系统时间简单示例
2018/08/23 Javascript
nodejs npm错误Error:UNKNOWN:unknown error,mkdir 'D:\Develop\nodejs\node_global'at Error
2019/03/02 NodeJs
Jquery+AJAX实现无刷新上传并重命名文件操作示例【PHP后台接收】
2020/05/29 jQuery
ES6 Generator基本使用方法示例
2020/06/06 Javascript
vue使用openlayers实现移动点动画
2020/09/24 Javascript
vue+vant实现购物车全选和反选功能
2020/11/17 Vue.js
[00:50]2014DOTA2国际邀请赛 NEWBEE战队回顾
2014/08/01 DOTA
[01:33]真香警告!DOTA2勇士令状不朽珍藏Ⅱ饰品欣赏
2018/06/26 DOTA
读取本地json文件,解析json(实例讲解)
2017/12/06 Python
python with提前退出遇到的坑与解决方案
2018/01/05 Python
几种实用的pythonic语法实例代码
2018/02/24 Python
解决pycharm py文件运行后停止按钮变成了灰色的问题
2018/11/29 Python
如何通过python画loss曲线的方法
2019/06/26 Python
python实现从本地摄像头和网络摄像头截取图片功能
2019/07/11 Python
python根据多个文件名批量查找文件
2019/08/13 Python
python实现一个函数版的名片管理系统过程解析
2019/08/27 Python
Python cookie的保存与读取、SSL讲解
2020/02/17 Python
python继承threading.Thread实现有返回值的子类实例
2020/05/02 Python
浅谈python出错时traceback的解读
2020/07/15 Python
海淘零差价,宝贝全球购: 宝贝格子
2016/08/24 全球购物
linux下进程间通信的方式
2013/01/23 面试题
我的五年职业生涯规划
2014/01/23 职场文书
给校长的建议书500字
2014/05/15 职场文书
2015年营销工作总结范文
2015/04/23 职场文书
我的收音机情缘
2022/04/05 无线电