redis实现分布式锁
如何设计安全的分布式锁
在生产环境下一般不会使用单实例redis, 然而集群下的redis使用如下方式获取分布式锁存在安全性问题, 不管是 sentinel 模式或者 cluster 模式, 主要受限于 redis 数据同步模型.
从 CAP 理论上讲, redis 实现了 AP 即放弃了一致性, 然而分布式锁应该实现 CP 才对.
不过, 在项目中如果可以容忍重复获取分布式锁, 可以考虑如下方案或者redlock.
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
概要
一个分布式锁需要满足以下三个条件:
- 分布式锁的加锁和释放锁要保证操作的原子性.
- 锁的拥有者因为异常退出, 锁在合理的时间范围内可以自动释放.
- 只有锁的拥有者可以释放锁.
使用 setnx key value 实现分布式锁的缺点:
- 没有设置超时时间, 获取锁的服务器一旦发生OOM挂掉, 其他服务器将不再有机会获取到锁.
- 如果在value中设置时间, 对锁的存在和超时判断将不是原子性操作.
加锁
获取锁使用如下命令:
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
此处的 value 存储了 requestId, 可以理解为锁的钥匙, 只有锁的拥有者知道. 可以是一个随机生成的字符串. 例如:
# NX: SET_IF_NOT_EXIST # PX: SET_WITH_EXPIRE_TIME 127.0.0.1:6379> set key value NX PX 3000 OK 127.0.0.1:6379> get key "value"
加锁成功会返回 "OK"
解锁
解锁时要验证锁的拥有者, 验证 requestId 是否正确, 正确则可以删除锁. 这包含了两步操作, 验证和删除. 我们可以通过 lua 脚本把这两个操作合并成一个操作.
redis 执行 lua 脚本具有原子性(注意: SharedJedis 客户端不支持 lua 脚本).
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
核心代码
/** * redis 实现分布式锁 * * @author xxx * @date 2021-01-15 15:50 **/ public class RedisDistributedLock implements DistributedLock { private static final String LOCK_SUCCESS = "OK"; private static final Long RELEASE_SUCCESS = 1L; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; @Override public boolean lock(String lockKey, String requestId, int expireMillis) { String result = RedisUtil.opsForValue() .set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMillis); return LOCK_SUCCESS.equals(result); } @Override public boolean unLock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = RedisUtil .eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); return RELEASE_SUCCESS.equals(result); } }
使用
@Test public void DistributedLockTest(){ DistributedLock distributedLock = new RedisDistributedLock(); String lockKey = "xxx"; try { if (distributedLock.lock(lockKey, "111", 3000)){ System.out.println("获取成功!"); } }finally { if (distributedLock.unLock(lockKey, "222")){ System.out.println("222--释放成功!"); } else { System.out.println("222--释放失败!"); } if (distributedLock.unLock(lockKey, "111")){ System.out.println("111--释放成功!"); }else { System.out.println("111--释放失败!"); } } }
输出结果:
获取成功! 222--释放失败! 111--释放成功!