redis分布式锁,setnx+lua脚本的java实现
1 前言
在现在工作中,为保障服务的高可用,应对单点故障、负载量过大等单机部署带来的问题,生产环境常用多机部署。为解决多机房部署导致的数据不一致问题,我们常会选择用分布式锁。
目前其他比较常见的实现方案我列举在下面:
- 基于缓存实现分布式锁(本文主要使用redis实现)
- 基于数据库实现分布式锁
- 基于zookeeper实现分布式锁
本文是基于redis缓存实现分布式锁,其中使用了setnx命令加锁,expire命令设置过期时间并lua脚本保证事务一致性。Java实现部分基于JIMDB提供的接口。JIMDB是京东自主研发的基于Redis的分布式缓存与高速键值存储服务。
2 SETNX
基本语法:SETNX KEY VALUE
SETNX 是表示 SET ifNot eXists, 即命令在指定的 key 不存在时,为 key 设置指定的值。
KEY 是表示待设置的key名
VALUE是设置key的对应值
若设置成功,则返回1;若设置失败(key存在),则返回0。
由此,我们会选择用SETNX来进行分布式锁的实现,当Key存在时,会返回加锁失败的信息。
SET 与 SETNX 区别:
SET 如果key已经存在,则会覆盖原值,且无视类型
SETNX 如果key已经存在,则会返回0,表示设置key失败
Redis 2.6.12版本前后对比:
2.6.12版本前:分布式锁并不能只用SETNX实现,需要搭配EXPIRE命令设置过期时间,否则,key将永远有效。其中,为保证SETNX和EXPIRE在同一个事务里,我们需要借助LUA脚本来完成事务实现。(由于在写这篇文章时,JIMDB还未支持SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]语法,故本文依然用lua事务)
2.6.12版本后:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL] 语法糖可用于分布式锁并支持原子操作,无需EXPIRE命令设置过期时间。
3 LUA脚本
什么是LUA脚本?
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序种,从而为程序提供灵活的扩展和定制功能。
为什么需要用到LUA脚本?
本文的锁实现是基于两个Redis命令 - SETNX 和 EXPIRE。 为保证命令的原子性,我们将这两个命令写入LUA脚本,并上传至Redis服务器。Redis服务器会单线程执行LUA脚本,以确保两个命令在执行期间不被其他请求打断。
LUA脚本的优势
- 减少网络开销。若干命令的多次请求,可组合成一个脚本进行一次请求
- 高复用性。脚本编辑一次后,相同代码逻辑可多处使用,只需将不同的参数传入即可。
- 原子性。若期望多个命令执行期间不被其他请求打断,或出现竞争状态,可以用LUA脚本实现,同时保证了事务的一致性。
分布式锁LUA脚本的实现
假设在同一时刻只能创建一个订单,我们可以将orderId作为key值,uuid作为value值。过期时间设置为3秒。
LUA脚本如下,通过Redis的eval/evalsha命令实现:
-- lua加锁脚本
-- KEYS[1],ARGV[1],ARGV[2]分别对应了orderId,uuid,3
-- 如果setnx成功,则继续expire命令逻辑
if redis.call('setnx',KEYS[1],ARGV[1]) == 1
then
-- 则给同一个key设置过期时间
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
-- 如果setnx失败,则返回0
return 0
end
-- lua解锁脚本
-- KEYS[1],ARGV[1]分别对应了orderId,uuid
-- 若无法获取orderId缓存,则认为已经解锁
if redis.call('get',KEYS[1]) == false
then
return 1
-- 若获取到orderId,并value值对应了uuid,则执行删除命令
elseif redis.call('get',KEYS[1]) == ARGV[1]
then
-- 删除缓存中的key
return redis.call('del',KEYS[1])
else
-- 若获取到orderId,且value值与存入时不一致,则返回特殊值,方便进行后续逻辑
return 2
end
【注】根据Redis的版本,在LUA脚本中,当使用redis.call('get',key)判定缓存key不存在时,需要注意对比值为布尔类型的false,还是null。
根据 官方文档 :Lua Boolean -> RESP3 Boolean reply (note that this is a change compared to the RESP2, in which returning a Boolean Lua true returned the number 1 to the Redis client, and returning a false used to return a null .
在RESP3中,redis cli返回的是空值时,lua会用布尔类型false来代替。
RESP3简介
RESP3是Redis6的新特性,是RESP v2的新版本。该协议用于客户端和服务器之间的请求响应通信。由于该协议可以不对称的使用,即客户端发送一个简单的请求,服务器可以将更复杂的并扩充后的相关信息返回到客户端。升级后的协议,引入了13种数据类型,使之更适用于数据库的交互场景。
4 基于JIMDB的Java分布式锁实现
调用类实现代码
SoRedisLock soJimLock = null;
try{
soJimLock = new SoRedisLock("orderId", jimClient);
if (!soJimLock.lock(3)) {
log.error("订单创建加锁失败");
throw new BPLException("订单创建加锁失败");
}
} catch(Exception e) {
throw e;
} finally {
if (null != soJimLock) {
soJimLock.unlock();
}
}
分布式锁实现类代码
public class SoRedisLock{
/** 加锁标志 */
public static final String LOCKED = "TRUE";
/** 锁的关键词 */
private String key;
private Cluster jimClient;
/**
* lock的构造函数
*
* @param key
* key+"_lock" (key使用唯一的业务单号)
* @param
*
*/
public SoRedisLock(String key, Cluster jimClient)
{
this.key = key + "_LOCK";
this.jimClient = jimClient;
}
/**
* 加锁
*
* @param expire
* 锁的持续时间(秒),过期删除
* @return 成功或失败标志
*/
public boolean lock(int expire)
{
try
{
log.info("分布式事务加锁,key:{}", this.key);
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";
String sha = jimClient.scriptLoad(lua_scripts);
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(this.key);
values.add(LOCKED);
values.add(String.valueOf(expire));
this.locked = jimClient.evalsha(sha, keys, values, false).equals(1L);
return this.locked;
} catch (Exception e){
throw new RuntimeException("Locking error", e);
}
}
/**
* 解锁 无论是否加锁成功,都需要调用unlock 建议放在finally 方法块中
*/
public void unlock()
{
if (this.jimClient == null || !this.locked) {
return ;
}
try {
String luaScript = "if redis.call('get',KEYS[1]) == false then return 1 " +
"elseif redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) else return 2 end";
String sha = jimClient.scriptLoad(luaScript);
if(!jimClient.evalsha(sha, Collections.singletonList(this.key), Collections.singletonList(LOCKED), false).equals(1L)){
throw new RuntimeException("解锁失败,key:"+this.key);
}
} catch (Exception e) {
log.error("unLocking error, key:{}", this.key, e);
throw new RuntimeException("unLocking error, key:"+this.key);
}
}
}
由于我们只是使用key-value做一个加锁动作,value并无意义。故,本文key对应的value给定固定值。Jimdb提供了上传脚本的API,我们通过scriptLoad()方法将lua脚本上传至redis服务器中。并利用evalsha()方法来进行脚本的执行。evalsha()返回值即为脚本中的设置的return的返回值。
我们通过list将参数传入脚本中,并对应脚本中的标记位。例如上方的代码中:
“orderId_LOCK”对应了脚本中的KEYS[1]
“TRUE”对应了脚本中的ARGV[1]
“3”对应了脚本中的ARGV[2]
【注】若在一个脚本中存在多个key,需要确保redis中的hashtag被启用,以防分片导致的key不处于同一分片,进而出现“Only support single key or use same hashTag”异常。当然,hashtag启用需要谨慎,否则分片不均导致流量的集中,造成服务器压力过大。
实际使用中的日志截图
5 总结
通过上述介绍我们了解到如何保证Redis多个命令的原子性。当然,Redis事务一致性,也可以选择Redis的事务(Transaction)操作来实现。Jimdb也有API支持事务的multi,discard,exec,watch和unwatch命令。本文之所以选择使用LUA脚本来进行实现,主要是考虑到目前Jimdb在执行事务时,流量只会打到主实例,多实例的负载均衡会失效。更多的可行方案等待大家的探索,我们下个文档见。
6 参考资料
Redis分布式锁: https://www.cnblogs.com/niceyoo/p/13711149.html
Redis中使用Lua脚本:https://zhuanlan.zhihu.com/p/77484377
Redis Eval命令: https://www.redis.net.cn/order/3643.html
LUA API: https://redis.io/docs/interact/programmability/lua-api/
作者:京东物流 牟佳义
来源:京东云开发者社区 自猿其说Tech 转载请注明来源
redis分布式锁,setnx+lua脚本的java实现的更多相关文章
- Redis分布式锁—SETNX+Lua脚本实现篇
前言 平时的工作中,由于生产环境中的项目是需要部署在多台服务器中的,所以经常会面临解决分布式场景下数据一致性的问题,那么就需要引入分布式锁来解决这一问题. 针对分布式锁的实现,目前比较常用的就如下几种 ...
- redis分布式锁-SETNX实现
Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists.这系列的命令非常有用,这里讲使用SETNX来实现分布式锁 ...
- (转)redis分布式锁-SETNX实现
Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists.这系列的命令非常有用,这里讲使用SETNX来实现分布式锁 ...
- Lua脚本在redis分布式锁场景的运用
目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...
- redis集群+JedisCluster+lua脚本实现分布式锁(转)
https://blog.csdn.net/qq_20597727/article/details/85235602 在这片文章中,使用Jedis clien进行lua脚本的相关操作,同时也使用一部分 ...
- Redis实现分布式锁的正确使用方式(java版本)
Redis实现分布式锁的正确使用方式(java版本) 本文使用第三方开源组件Jedis实现Redis客户端,且只考虑Redis服务端单机部署的场景. 分布式锁一般有三种实现方式: 1. 数据库乐观锁: ...
- Redis分布式锁的正确实现方式(Java版)
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- 死磕 java同步系列之redis分布式锁进化史
问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...
- 【转】Redis 分布式锁的正确实现方式( Java 版 )
链接:wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/ 前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布 ...
- Redis 分布式锁的正确实现方式(Java版)[转]
本文来源: https://www.cnblogs.com/linjiqin/p/8003838.html 前言 分布式锁一般有三种实现方式: 数据库乐观锁: 基于Redis的分布式锁: 基于ZooK ...
随机推荐
- LeetCode 周赛 346(2023/05/21)仅 68 人 AK 的最短路问题
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. LeetCode 单周赛第 345 场 · 体验一题多解的算法之美 单周赛 345 概览 T1. 删除子串后 ...
- Pandas 加载数据的方法和技巧
哈喽大家好,我是咸鱼 相信小伙伴们在学习 python 数据分析的过程中或多或少都会听说或者使用过 pandas pandas 是 python 的一个拓展库,常用于数据分析 今天咸鱼将介绍几个关于 ...
- ASP.NET Core 6框架揭秘实例演示[36]:HTTPS重定向
HTTPS是确保传输安全最主要的手段,并且已经成为了互联网默认的传输协议.不知道读者朋友们是否注意到当我们利用浏览器(比如Chrome)浏览某个公共站点的时候,如果我们输入的是一个HTTP地址,在大部 ...
- Windows全能终端神器MobaXterm
MobaXterm 又名 MobaXVT,是一款增强型终端.X 服务器和 Unix 命令集(GNU/ Cygwin)工具箱. MobaXterm 可以开启多个终端视窗,以最新的 X 服务器为基础的 X ...
- 华为防火墙NAT技术
---我是陈小瓜,一个普通的路人,和大家一起交流学习,完善自己. 源NAT NAT-no-pat 安全策略写法: 源NAT,写安全策略,写转换前的私网IP,因为先匹配安全策略.再匹配NAT策略 NAT ...
- python selenium自动化火狐浏览器开代理IP服务器
前言 Selenium是一款用于自动化测试Web应用程序的工具,它可以模拟用户在浏览器中的各种行为.而代理IP服务器则是一种可以帮助用户隐藏自己真实IP地址的服务器,使得用户可以在互联网上更加匿名地进 ...
- Java Websocket 01: 原生模式 Websocket 基础通信
目录 Java Websocket 01: 原生模式 Websocket 基础通信 Java Websocket 02: 原生模式通过 Websocket 传输文件 Websocket 原生模式 原生 ...
- FPGA加速技术在人机交互界面中的应用及优化
目录 引言 随着人工智能.云计算.大数据等技术的发展,人机交互界面的重要性也越来越凸显.作为用户与计算机之间的桥梁,人机交互界面的性能和效率直接影响用户的体验和使用效果.为了优化人机交互界面的性能,我 ...
- 【干货向】我想试试教会你如何修改Git提交信息
Git是目前IT行业使用率最高的版本控制系统,相信大家在日常工作中也经常使用,每次Git提交都会包含提交信息,常用的包括说明.提交人和提交时间等,此篇文章主要向大家介绍下如何修改这些信息,这些命令在正 ...
- 【技术积累】Mysql中的SQL语言【技术篇】【二】
什么是多表查询?如何在MySQL中进行多表查询? 多表查询就是在一个查询中涉及到多个表,通过特定的关联方式连接多个表,并根据条件从中查询出所需要的数据. 多表查询是关系型数据库中最为基础的应用之一. ...
