触发器 无法启用分布式事务_可靠的分布式锁 RedLock 与 redisson 的实现

1. 引言

此前的文章中,我们详细介绍了基于 redis 的分布式事务锁的实现:

厉害了,原来分布式锁有这么多坑

我们看到,分布式锁是如何一步步改进方案最终实现其高可用的。

但就“高可用”来说,似乎仍然有所欠缺,那就是如果他所依赖的 redis 是单点的,如果发生故障,则整个业务的分布式锁都将无法使用,即便是我们将单点的 redis 升级为 redis 主从模式或集群,对于固定的 key 来说,master 节点仍然是独立存在的,由于存在着主从同步的时间间隔,如果在这期间 master 节点发生故障,slaver 节点被选举为 master 节点,那么,master 节点上存储的分布式锁信息可能就会丢失,从而造成竞争条件。

那么,如何避免这种情况呢?

redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案 — RedLock,本文我们就来详细介绍一下。

2. RedLock 的加解锁过程

基于上述理论,我们知道,RedLock 是在多个 Redis 集群上部署的一种分布式锁的实现方式,他有效避免了单点问题。

假设我们有 N 个 Redis 服务或集群,RedLock 的加锁过程就如下所示:

  1. client 获取当前毫秒级时间戳,并设置超时时间 TTL
  2. 依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key
  3. 如果从 N/2+1 个 redis 服务中都获取锁成功,那么,本次分布式锁的获取被视为成功,否则视为获取锁失败。
  4. 如果获取锁失败,或执行达到 TTL,则向所有 Redis 服务都发出解锁请求。

cd52eaf873935d07486b4534285255b4.png

3. Java 实现 — redisson

java 语言中,redisson 包实现了对 redlock 的封装,主要是通过 redis client 与 lua 脚本实现的,之所以使用 lua 脚本,是为了实现加解锁校验与执行的事务性。

此前主页君对 redis 结合 lua 实现严格的事务有过一篇文章来介绍,可以参考:

Redis 事务与 Redis Lua 脚本的编写

3.1 唯一 ID 的生成

分布式事务锁中,为了能够让作为中心节点的存储节点获悉锁的持有者,从而避免锁被非持有者误解锁,每个发起请求的 client 节点都必须具有全局唯一的 id。

通常我们是使用 UUID 来作为这个唯一 id,redisson 也是这样实现的,在此基础上,redisson 还加入了 threadid 避免了多个线程反复获取 UUID 的性能损耗:

protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
return id + ":" + threadId;
}

3.2 加锁逻辑

redisson 加锁的核心代码非常容易理解,通过传入 TTL 与唯一 id,实现一段时间的加锁请求。

下面是可重入锁的实现逻辑:

RFuturetryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);// 获取锁时向5个redis实例发送的命令return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,// 校验分布式锁的KEY是否已存在,如果不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hset', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +// 如果分布式锁的KEY已存在,则校验唯一 id,如果唯一 id 匹配,表示是当前线程持有的锁,那么重入次数加1,并且设置失效时间"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +// 获取分布式锁的KEY的失效时间毫秒数"return redis.call('pttl', KEYS[1]);",// KEYS[1] 对应分布式锁的 key;ARGV[1] 对应 TTL;ARGV[2] 对应唯一 id
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

3.3 释放锁逻辑

protected RFutureunlockInnerAsync(long threadId) {
// 向5个redis实例都执行如下命令
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 如果分布式锁 KEY 不存在,那么向 channel 发布一条消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
// 如果分布式锁存在,但是唯一 id 不匹配,表示锁已经被占用
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 如果就是当前线程占有分布式锁,那么将重入次数减 1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
// 重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,不删除
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
// 重入次数减1后的值如果为0,则删除锁,并发布解锁消息
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
// KEYS[1] 表示锁的 key,KEYS[2] 表示 channel name,ARGV[1] 表示解锁消息,ARGV[2] 表示 TTL,ARGV[3] 表示唯一 id
Arrays.asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
}

4. redisson RedLock 的使用

redisson 实现的分布式锁的使用非常简单:

Config config = new Config();
config.useSentinelServers().addSentinelAddress("127.0.0.1:6369","127.0.0.1:6379", "127.0.0.1:6389")
.setMasterName("masterName")
.setPassword("password").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock redLock = redissonClient.getLock("REDLOCK_KEY");
boolean isLock;
try {
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
redLock.unlock();
}

可以看到,由于 redisson 包的实现中,通过 lua 脚本校验了解锁时的 client 身份,所以我们无需再在 finally 中去判断是否加锁成功,也无需做额外的身份校验,可以说已经达到开箱即用的程度了。

5. redisson 的高级功能

5.1 异常情况的处理

分布式事务锁最常见的一个问题就是如果已经获取到锁的 client 在 TTL 时间内没有完成竞争资源的处理,而此时锁会被自动释放,造成竞争条件的发生。

这种情况如果让 client 端设置定时任务自动延长锁的占用时间,会造成 client 端逻辑的复杂和冗余。

redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的可选特性,并且增加了 lockWatchdogTimeout 配置参数,看门狗线程会自动在 lockWatchdogTimeout 超时后顺延锁的占用时间,从而避免上述问题的发生。

但是,由于看门狗作为独立线程存在,对于性能有所影响,如果并非是处理高度竞争且处理时长不固定的特殊资源,那么并不建议启用 redisson 的看门狗特性。

5.2 多个锁联合使用 — 联锁

既然 redisson 通过多个 redis 节点实现了 RedLock,那么,如果一个业务同时需要占用若干资源,是否可以将多个锁联合使用呢?答案也是可以的。

基于 Redis 的分布式 RedissonMultiLock 对象将多个 RLock 对象分组,并将它们作为一个锁处理。每个 RLock 对象可能属于不同的 Redisson 实例。

在这种复杂场景下,上述“看门狗”特性建议一定要启用,因为任何一个锁状态的崩溃都有可能会造成其他所有锁的持续挂起或资源被意外抢占。

下面是 redisson 联锁的一个示例:

public void multiLock(Integer expireTime,TimeUnit timeUnit,String ...lockKey){
RLock [] rLocks = new RLock[lockKey.length];
for(int i = 0,length = lockKey.length; i < length ;i ++){
RLock lock = redissonClient.getLock(lockKey[i]);
rLocks[i] = lock;
}
RedissonMultiLock multiLock = new RedissonMultiLock(rLocks);
multiLock.lock(expireTime,timeUnit);
logger.info("【Redisson lock】success to acquire multiLock for [ "+lockKey+" ],expire time:"+expireTime+timeUnit);
}

微信公众号

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,全部原创,只有干货没有鸡汤

adb443ee04ddd0a54a6fa6af72848bb4.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]:触发器是由逻辑门组成,逻辑门是由继电器组成。当RAM正在被使用并且电源突然中断时,电路不通,导致数据丢失。\[1\]引用\[2\]:128x1bit的双口RAM需要四个LUT实现,也正好是一个SLICEM,并且分别使用了F7AMUX、F7BMUX。用两份128bitRAM,读写地址分开的方式实现读写互不干扰。\[2\]引用\[3\]:LUT原理是将结果预存储在ROM中,通过输入信号作为地址对预存的结果进行寻址。借助LUT和几个端口,可以实现RAM。SLICEM中的LUT可以用来实现RAM资源。\[3\] 查找表与触发器构成分布式RAM的方式是通过将多个触发器连接在一起,并使用查找表来控制触发器的状态。每个触发器存储一个位的数据,而查找表则根据输入信号的地址来选择对应的触发器进行读写操作。通过这种方式,可以实现分布式的RAM存储器,其中每个触发器都存储着一部分数据,并且可以通过查找表进行读写操作。这种分布式的RAM结构可以提供更高的存储容量和更快的读写速度。 #### 引用[.reference_title] - *1* [6.触发器实现RAM](https://blog.csdn.net/xiazdong/article/details/6627493)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [LUT查找表实现各种RAM及ROM原理精讲](https://blog.csdn.net/weiaipan1314/article/details/104327009)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值