分布式锁

请注意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 

总体状态

理想情况,假设当某台机器访问某个锁时

动作无锁/失效自己的锁他人的锁
AcquireLockOK(nil)(nil)
ReleaseLock0OK0

从上面我们也看出很多缺点

  • 轮询阻塞: 在真实场景中,一般会通过轮询查询锁。但是轮询for循环获取锁没有优先级调度,全靠网线质量
  • 超时配置: 短了,需要业务侧维护看门狗不断延期,增加了复杂性;配置时间长了,如果当前业务挂掉,其它机器也要阻塞等待至expire。
  • FailOver: 集群环境中(主从同步为异步),主机挂掉,而从机没有同步Lock信息时,锁失去唯一性;主机解锁后挂掉,而从机没解锁,所有人都要等到超时才行。

针对这些问题,在真实场景中,我们并没有用Redis去完成苛刻的任务,而主要用作全局Flag(比如防止两个耗时任务同时执行),而且Redis方案也有替代品,比如Quartz中的SELECT FOR UPDATE就是更加完善的方案。

对开源方案的改进

集群版(redlock)