分布式锁
请注意Redis的分布式锁,就算是Redlock,也不能完全实现百分百高可用,因此仅建议在能够容忍失败的场景下使用。如果有更高精度的要求,建议使用事务,zk或者etcd等工具。
另外,尽可能不要用锁,共享数据的内容尽可能执行快一点,不要长期阻塞。业务逻辑也建议使用MQ/Akka等流式方案,避免分布式全局变量。
单机版
加锁
通过执行如下命令实现
# EX seconds -- Set the specified EXpire time, in seconds.
# NX -- Only set the key if it does Not already eXist.
set key random-token EX 30 NX
→ OK
这里的失效时间比较难配,而且random-token
也要是分布式唯一ID。
如果一定需要获取到锁才能执行,那么需要再外面包装一层轮询才够
网上有许多JedisLock的实现,我个人实现的Java代码如下
// 此处没用UUID(懒得维护全局变量),我这里注入了Eureka的InstanceId,详见我的Eureka相关文档
String uniqueId = "10.0.0.2:8080:node1-sz"
//阻塞获取锁方案
public boolean compareAndSetSync(String lock, int waitTime, int leaseTime){
while(waitMs>=0){
boolean ok = jedis.set(lock, uniqueId, "EX", leaseTime, "NX").equals("OK");
if(ok){
return true;
}
synchronized(JedisLock.this){
waitMs- = 100; //ms
try{
JedisLock.this.wait()
}catch(ignored){
}
}
}
}
相对网上开源方案的变更
- 将synchronized范围降低,将
sleep
改为wait
(wait释放时间片,此处代码可以参考我以前写的OkHttp连接池分析) - 支持阻塞等待指定时间获取锁
移除锁
通过EVAL单线程的Lua脚本实现合并两条Command为原子指令,有点类似CAS命令
# 1: paraments count is 1
EVAL "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 key random-token
→ OK
总体状态
理想情况,假设当某台机器访问某个锁时
动作 | 无锁/失效 | 自己的锁 | 他人的锁 |
---|---|---|---|
AcquireLock | OK | (nil) | (nil) |
ReleaseLock | 0 | OK | 0 |
从上面我们也看出很多缺点
- 轮询阻塞: 在真实场景中,一般会通过轮询查询锁。但是轮询for循环获取锁没有优先级调度,全靠网线质量
- 超时配置: 短了,需要业务侧维护看门狗不断延期,增加了复杂性;配置时间长了,如果当前业务挂掉,其它机器也要阻塞等待至expire。
- FailOver: 集群环境中(主从同步为异步),主机挂掉,而从机没有同步Lock信息时,锁失去唯一性;主机解锁后挂掉,而从机没解锁,所有人都要等到超时才行。
针对这些问题,在真实场景中,我们并没有用Redis去完成苛刻的任务,而主要用作全局Flag(比如防止两个耗时任务同时执行),而且Redis方案也有替代品,比如Quartz中的SELECT FOR UPDATE
就是更加完善的方案。