赞
踩
作者: 编程界的小学生
日期: 2021/09/06
修订: 初版,未修订。2021/09/06
版权: 内部资料,切勿泄漏,违者必究。
比如如下就不具有原子性,就是错误的代码:
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(orderId, userId);
stringRedisTemplate.expire(lockId, 30L, TimeUnit.SECONDS);
那怎么改正确呢?用Redis的setnx命令,一个命令代替两个:
boolean lockStatus = stringRedisTemplate.opsForValue().setIfAbsent(orderId, userId, 30L, TimeUnit.SECONDS);
或者有lua脚本将两个命令放到一个lua里,然后redis执行lua,相当于只执行了一个命令。这样就保证原子性了。
锁要带过期时间,否则会有死锁的风险。
比如:如果上锁成功了,还没释放呢,服务宕机了,这把锁将永驻,服务起来后再去抢占锁的时候发现已经有锁了,无法抢占,但是这把锁又永远得不到释放,死锁了。
所以记得要用setnx设置过期时间,或者set+expire放到lua里进行设置。
为什么需要续期?假设锁设置了3s,但是业务代码执行了4s还没执行完,那锁过期了,其他线程在请求接口的时候又加上了锁(redis里又setnx值了),这时候不就并发执行了吗?相当于还是线程不安全!所以需要锁续期。
可以起个线程续期,上锁的时候就起个线程进行死循环续期,检查时间过了二分之一了就给他重新续期为上锁时间。比如设置的锁是4s,检查超过2s了还没执行完,那就重新给这个锁续期为4s。
也就是说锁永远都在释放锁的时候才进行过期?那为啥还要设置过期时间?这个上面说了,防止死锁。
要正确释放锁,啥叫正确?看个例子:
问题:释放锁可能释放了别人的锁,比如锁设置了3s,但是业务代码执行了4s还没执行完,那锁过期了,其他线程在请求接口的时候又加上了锁(redis里又setnx值了),然后第一个执行了4s的线程运行完了,释放了第二个线程加的锁,这时候其他线程又能抢锁了,这不安全!
方案:
下面对这两种方案做个伪代码演示:
续期
private volatile boolean isRunning; // 抢锁成功 if (RESULT_OK.equals(client.setNxPx(key, value, ttl))) { // 续期 renewalTask = new RenewTask(new IRenewalHandler() { @Override public void callBack() throws LockException { // 刷新值 client.expire(key, ttl <= 0 ? 10 : ttl); } }, ttl); // 设置为后台线程 renewalTask.setDaemon(true); renewalTask.start(); } // 续期线程的逻辑 @Override public void run() { while (isRunning) { try { // 1、续租,刷新值 call.callBack(); LOGGER.info("续租成功!"); // 2、三分之一过期时间续租 TimeUnit.SECONDS.sleep(this.ttl * 1000 / 3); } catch (InterruptedException e) { close(); } catch (LockException e) { close(); } } } public void close() { isRunning = false; }
正确解锁
// 判断订单id的锁是自己上的方可释放。
if((userId).equals(stringRedisTemplate.opsForValue().get(orderId))) {
stringRedisTemplate.delete(orderId);
}
这样行吗?肯定不行的,因为判断里的redis获取操作和del操作是非原子的,如果你设置了超时时间,那么你也的业务在超时时间内没有执行完,那么这个锁就会被释放,其他线程拿到锁—以上恰好发生在get之后,del之前,会删除其他的锁,那么是不是就脏读了呢?但是如果有续期的话就不存在此问题。但还是尽量用lua脚本,lua脚本如下:
// 如果get的值等于传进来的值,就给他del
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
直接单机上锁,这台机器挂了就GG了,整个业务系统都获取不到锁了,单点故障!
既然单点故障,那我搞个哨兵,Sentinel,自动主从切换。这下稳了吧?但是会有如下新问题:
锁写到Master后,还没同步到Slave呢,Master挂了。Slave选举成了Master,但是Slave里没有锁,其他线程再次能上锁了。不安全。
集群只是做了slot分片,锁还是只写到一个Master上,所以和Sentinel哨兵模式有同样的问题。
也称RedLock,非常著名!是Redis实现分布式锁相对最安全可靠的一种手段。
他的核心思路是:搞几个独立的Master,比如5个。然后挨着个的加锁,只要超过一半以上(这里是5 / 2 + 1 = 3
个)那就代表加锁成功,然后释放锁的时候也逐台释放。这样的好处在于一台Master挂了的话,还有其他的,所以不耽误,看起来好像完美解决了上面的问题。但是并不是100%安全,后面会说。
具体细节为:
需要注意两点:
主要存在的问题:
有国外大佬提了两个问题推翻RedLock的绝对安全性,当然Redis作者肯定不认同他的说法,但又无法证实。感兴趣的可以搜搜看看,很好玩的。
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
所以我认为不管你用Redis的哪种方式来实现分布式锁,都不是100%安全的,那就不用Redis做分布式锁了吗?不然,我觉得取决于业务吧,如果你业务要求必须,100%不能出问题,那用zk/etcd来实现吧。但是据我了解,至少80%的互联网公司都不这么强烈要求,大对数还是Redis分布式锁,即使用zk来实现的也可能不是业务上100%要求不能出现问题。比如你项目就没用zk,只用了Redis,那完全没必要搭建一套zk来做分布式锁,Redis的红锁也能保证高可用,几乎不会出现问题的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。