Loading... # Redis 异步锁的分析与解决方案 ### 1. SETNX + EXPIRE > setnx 跟 expire 分开执行,并不是原子操作,会有死锁产生的可能 ```java if(redis.setnx(lockKey,lockValue) == 1){ expire(lockKey,100); // set expired time try { //@todo }catch(Exception e){ }finally{ redis.del(key_resource_id); //release } } ``` ### 2. SETNX + value (expire time) > 1) 要求每个 server 时间保持一致 > 2) 如果锁过期的时间,同时又多个请求 getset ,最终只有一个拿到锁,但是过期时间有可能被覆盖 > 3) 该锁没有唯一标识,可能被别的客户端 释放/解锁 ```java long expires = System.currentTimeMillis() + expireTime; // get current time String expiresStr = String.valueOf(expires); // if not lock before if (jedis.setnx(key_resource_id, expiresStr) == 1) { return true; } // get the value of the lock String currentValueStr = jedis.get(key_resource_id); // compare the time , current time > lock time , update the lock time if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { String oldValueStr = jedis.getSet(key_resource_id, expiresStr); // avoid data update in other way if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { return true; } } return false; } ``` ### 3. LUA Script > LUA 是原子操作 ```java 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"; Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values)); //判断是否成功 return result.equals(1L); ``` ### 4. Set Extra CMD > 1) 锁过期了,业务逻辑还没处理完,在 call 多个 api 时,时常会出现这种 case > 2) 当处理完业务逻辑后,去 release lock ,但是可能释放别的锁(可能)*key_resource_id not unique* ```java // SET key value[EX seconds][PX milliseconds][NX|XX] // NX : set when the key not exist // XX : set when the key is exist // EX : expired time => seconds // PX : expired time => milliseconds if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁 try { //@todo }catch(){ }finally{ jedis.del(key_resource_id); } } ``` ### 5. Set Extra CMD (Unique) > 1) 还是存在锁过期了,业务逻辑还没处理完,在 call 多个 api 时,时常会出现这种 case ```java if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁 try { //@todo }catch(){ }finally { // you can use lua script to replace if (uni_request_id.equals(jedis.get(key_resource_id))) { jedis.del(lockKey); } } } // Lua script if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end; ``` ### 6. Redisson Framework (watch dog) > 开一个守护进程,每过一段时间看锁被释放没有,没有的话给锁加时间。 ### 7.Redlock + Redisson * 获取当前时间,以毫秒为单位。 * 按顺序向 5 个 master 节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在 5-50 毫秒之间,我们就假设超时时间是 50ms 吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。 * 客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是 5/2+1=3 个节点)的 Redis master 节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(10s> 30ms+40ms+50ms+4m0s+50ms) * 如果取到了锁,key 的真正有效时间就变啦,需要减去获取锁所使用的时间。 * 如果获取锁失败(没有在至少 N/2+1 个 master 实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。 © Reprint prohibited Support Appreciate the author AliPayWeChat Like 76 If you think my article is useful to you, please feel free to appreciate