- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在现在工作中,为保障服务的高可用,应对单点故障、负载量过大等单机部署带来的问题,生产环境常用多机部署。为解决多机房部署导致的数据不一致问题,我们常会选择用分布式锁.
目前其他比较常见的实现方案我列举在下面:
本文是基于redis缓存实现分布式锁,其中使用了setnx命令加锁,expire命令设置过期时间并lua脚本保证事务一致性。Java实现部分基于JIMDB提供的接口。JIMDB是京东自主研发的基于Redis的分布式缓存与高速键值存储服务.
基本语法: 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命令设置过期时间.
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序种,从而为程序提供灵活的扩展和定制功能.
本文的锁实现是基于两个Redis命令 - SETNX 和 EXPIRE 。 为保证命令的原子性,我们将这两个命令写入LUA脚本,并上传至Redis服务器。Redis服务器会单线程执行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是Redis6的新特性,是RESP v2的新版本。该协议用于客户端和服务器之间的请求响应通信。由于该协议可以不对称的使用,即客户端发送一个简单的请求,服务器可以将更复杂的并扩充后的相关信息返回到客户端。升级后的协议,引入了13种数据类型,使之更适用于数据库的交互场景.
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启用需要谨慎,否则分片不均导致流量的集中,造成服务器压力过大.
通过上述介绍我们了解到如何保证Redis多个命令的原子性。当然,Redis事务一致性,也可以选择Redis的事务(Transaction)操作来实现。Jimdb也有API支持事务的multi,discard,exec,watch和unwatch命令。本文之所以选择使用LUA脚本来进行实现,主要是考虑到目前Jimdb在执行事务时,流量只会打到主实例,多实例的负载均衡会失效。更多的可行方案等待大家的探索,我们下个文档见.
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脚本的java实现的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我有一个关于 Redis Pubsub 的练习,如下所示: 如果发布者发布消息但订阅者没有收到服务器崩溃。订阅者如何在重启服务器时收到该消息? 请帮帮我,谢谢! 最佳答案 在这种情况下,消息将永远消失
我们正在使用 Service Stack 的 RedisClient 的 BlockingDequeue 来保存一些数据,直到它可以被处理。调用代码看起来像 using (var client =
我有一个 Redis 服务器和多个 Redis 客户端。每个 Redis 客户端都是一个 WebSocket+HTTP 服务器,其中包括管理 WebSocket 连接。这些 WebSocket+HTT
我有多个 Redis 实例。我使用不同的端口创建了一个集群。现在我想将数据从预先存在的 redis 实例传输到集群。我知道如何将数据从一个实例传输到集群,但是当实例多于一个时,我无法做到这一点。 最佳
配置:三个redis集群分区,跨三组一主一从。当 Master 宕机时,Lettuce 会立即检测到中断并开始重试。但是,Lettuce 没有检测到关联的 slave 已经将自己提升为 master
我想根据从指定集合中检索这些键来删除 Redis 键(及其数据集),例如: HMSET id:1 password 123 category milk HMSET id:2 password 456
我正在编写一个机器人(其中包含要禁用的命令列表),用于监视 Redis。它通过执行禁用命令,例如 (rename-command ZADD "")当我重新启动我的机器人时,如果要禁用的命令列表发生变化
我的任务是为大量听众使用发布/订阅。这是来自 docs 的订阅的简化示例: r = redis.StrictRedis(...) p = r.pubsub() p.subscribe('my-firs
我一直在阅读有关使用 Redis 哨兵进行故障转移的内容。我打算有1个master+1个slave,如果master宕机超过1分钟,就把slave变成master。我知道这在 Sentinel 中是
与仅使用常规 Redis 和创建分片相比,使用 Redis 集群有哪些优势? 在我看来,Redis Cluster 更注重数据安全(让主从架构解决故障)。 最佳答案 我认为当您需要在不丢失任何数据的情
由于 Redis 以被动和主动方式使 key 过期, 有没有办法得到一个 key ,即使它的过期时间已过 (但 在 Redis 中仍然存在 )? 最佳答案 DEBUG OBJECT myKey 将返回
我想用redis lua来实现monitor命令,而不是redis-cli monitor。但我不知道怎么办。 redis.call('monitor') 不起作用。 最佳答案 您不能从 Redis
我读过 https://github.com/redisson/redisson 我发现有几个 Redis 复制设置(包括对 AWS ElastiCache 和 Azure Redis 缓存的支持)
Microsoft.AspNet.SignalR.Redis 和 StackExchange.Redis.Extensions.Core 在同一个项目中使用。前者需要StackExchange.Red
1. 认识 Redis Redis(Remote Dictionary Server)远程词典服务器,是一个基于内存的键值对型 NoSQL 数据库。 特征: 键值(key-value)型,value
1. Redis 数据结构介绍 Redis 是一个 key-value 的数据库,key 一般是 String 类型,但 value 类型多种多样,下面就举了几个例子: value 类型 示例 Str
1. 什么是缓存 缓存(Cache) 就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高。 缓存的作用: 降低后端负载 提高读写效率,降低响应时间 缓存的成本: 数据一致性成本 代码维护成本
我有一份记录 list 。对于我的每条记录,我都需要进行一些繁重的计算,因为我要在Redis中创建反向索引。为了达到到达记录,需要在管道中执行多个redis命令(sadd为100 s + set为1
我有一个三节点Redis和3节点哨兵,一切正常,所有主服务器和从属服务器都经过验证,并且哨兵配置文件已与所有Redis和哨兵节点一起更新,但是问题是当Redis主服务器关闭并且哨兵希望选举失败者时再次
我正在尝试计算Redis中存储的消息之间的响应时间。但是我不知道该怎么做。 首先,我必须像这样存储chat_messages的时间流 ZADD conversation:CONVERSATION_ID
我是一名优秀的程序员,十分优秀!