使用Redis构建高性能锁

发布者:上海IT外包来源:http://www.lanmon.net点击数:971

作者所在的公司,在经历了上周末的大型推广活动后,系统暴露出这样一个问题:分布式锁使用的zk锁,由于当天用户人数众多,系统疯狂解锁锁定,最后zk无法承受如此大的压力停机时间。由于它大约是618,为了避免再次发生这种情况,该公司决定用高性能Redis锁替换所有系统zk锁。
这里简单提一下,zk锁的性能低于redis:zk角色分为领导者,花,每个请求只能请求领导者,领导者会向所有鲜花播放写请求,如果鲜花成功则会提交事实上,对于领导者来说,这相当于一个2PC过程。锁定时,它是一个写请求。当有很多写请求时,zk会有很大的压力,最后服务器会慢慢响应。
话题:
在什么情况下你需要锁定?
当多个线程和用户同时竞争同一资源时,您需要锁定。例如,订购更少的库存,抓票,选择课程,抓取红包等。如果此处没有锁定控制,则会导致非常严重的问题。如果在库存减少时订单没有锁定,将导致货物过度销售;如果机票未锁定,将导致两个人抓住相同的位置;在课堂选择期间没有锁定控制,导致成功选课的次数大于课堂中的座位数;抓取红色数据包时没有锁定控制,红色数据包的数量大于红色数据包的实际数量。
什么是分布式锁?
已经学习过JAVA多线程的朋友知道,为了防止多个线程同时执行同一段代码,您可以使用JAVA API中的synchronized关键字或ReentrantLock类来控制。
但是,几乎所有系统当前都部署了多台机器。独立部署的应用程序很少。 Synchronized和ReentrantLock不起任何作用。在这种情况下,需要全局锁定而不是JAVA中的synchronized和ReentrantLock。
当Thread1线程获取锁定时,执行锁定中的代码,另一个线程或其他机器再次请求锁定,并发现锁定被Thread1占用,并且锁定失败。当Thread1释放锁时,其他线程可以获取锁并执行相应的操作。我们可以在Jedis中使用setnx命令来构建这个锁。首先,我将列出一些构建锁的错误方法:
错误示例1
长锁=jedis.setnx(键,值);
如果(锁定>
0){
//执行业务逻辑
}
通过setnx命令创建密钥和值。如果密钥不存在,则锁定成功。这有什么问题吗?如果锁定操作成功,则在释放锁定时,系统崩溃,导致密钥永远不会变为delta,即其他线程永远无法获得锁定。
导致发生死锁。为避免这种情况,请参阅下面的代码
错误示例2
长锁=jedis.setnx(键,值);
如果(锁定>
0){
Jedis.expire(键,expireTime);
}
与上面的示例类似,唯一的区别是还有一个步骤来设置密钥到期时间。如果系统在del期间关闭,则Redis将在到达到期时间时删除密钥。
其他线程可以再次获取锁定。所以你能做到万无一失吗?这里有一个问题。如果在第一个setnx成功后网络闪烁,则expire命令执行失败,并且还存在死锁的风险。这两个步骤不是原子的,不能保证全部或全部失败。
正确的构建方式
Public static boolean getDistributedLock(Jedis jedis,String lockKey,String requestId,int expireTime){
字符串结果=jedis.set(lockKey,requestId,'NX','EX',expireTime);
如果(LOCK_SUCCESS.equals(result)){
回归真实;
}
返回false;
}
参数说明:
关键:关键
价值:价值
Nx:如果当前密钥存在,则设置失败,否则成功
例如:设置密钥的到期时间
expireTime:密钥的到期时间。当时间到了,Redis会自动删除键和值。
此命令将上述错误示例2中的两个操作组合为原子操作,确保同时成功或同时失败。
解锁方法:错误示例1:
Jedis.del(键);
执行此操作的线程会在不确定锁的所有者的情况下删除锁。
还记得set命令可以设置值吗?在锁的操作中,主要是判断键是否存在,那么值的用途是什么?如果你删除了锁,不要判断当前锁的所有者,任何线程都可以释放锁。此时,值值将起作用。
错误示例2:
如果(价值==jedis.get(键)){
Jedis.del(键);
}
当我们锁定时,我们可以将值设置为唯一标识当前线程的值。该值可以是UUID。当释放锁定时,判断该值是否与该组的值相同。如果它们相同,则表示锁已锁定。并释放锁是同一个线程,允许释放。否则锁被释放。
这绝对安全吗?答案当然是否定的。这一步也不是原子的。如果在值==jedis.get(key)返回true之后ThreadA返回true,则del命令尚未执行,密钥到期,此时ThreadB获取锁定,然后ThreadA执行del命令,并且ThreadB锁已发布。
因此,为了确保两个操作的原子性,我们必须使用简单的Lua脚本。
正确的解锁姿势:
Public static boolean releaseDistributedLock(Jedis jedis,String lockKey,String requestId){
String script='if redis.call('get',KEYS [1])==ARGV [1]然后返回redis.call('del',KEYS [1])否则返回0结束';
Object result=jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId));
If(RELEASE_SUCCESS.equals(result)){
回归真实;
}
返回false;}
Redis在2.6之后内部嵌入了Lua脚本解释器,因此我们可以使用简单的Lua脚本保证上述操作的原子性。代码中Lua脚本的含义是:我们将LockKey分配给KEYS [1],将RequestId分配给ARGV [1],如果key中的值等于RequestId则返回true,或返回false。这保证了锁操作的原子操作的释放,并且当前客户端将仅释放当前客户端的锁。
IT外包
>
400-635-8089
立即
咨询
电话咨询
服务热线
400-635-8089
微信咨询
微信咨询
微信咨询
公众号
公众号
公众号
返回顶部