Redis 实现
基于缓存实现分布式锁性能上会有优势,可以使用 Redis SETNX(SET if Not eXists)实现分布式锁.
注意锁需要设置过期时间,防止应用程序崩溃导致锁没有释放而阻塞后面的所有操作。
获取锁:使用 jedis.set(lockKey,lockvalue,"","PX”,lockTimeout)尝试获取锁,NX 确保键不存在时才设置,PX 设置键的过期时间(毫秒)。
释放锁:使用 Lua 脚本确保只有持有锁的客户端才能删除锁。Lua 脚本会检查键的值是否等于 lockValue,如果是则删除该键。
简单示例如下,acquireLock为获取锁,releaseLock为释放锁:
public class RedisDistributedLock {
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
private Jedis jedis;
private String lockKey;
private String lockValue;
private int lockTimeout;
public RedisDistributedLock(Jedis jedis, String lockKey, int lockTimeout) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockTimeout = lockTimeout;
this.lockValue = UUID.randomUUID().toString();
}
public boolean acquireLock() {
String result = jedis.set(lockKey, lockValue, "NX", "PX", lockTimeout);
return LOCK_SUCCESS.equals(result);
}
public boolean releaseLock() {
String releaseScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(releaseScript, Collections.singletonList(lockKey), Collections.singletonList(lockValue));
return RELEASE_SUCCESS.equals(result);
}
public static void main(String[] args) {
// 创建一个 Jedis 连接实例
Jedis jedis = new Jedis("localhost", 6379);
// 创建分布式锁实例
RedisDistributedLock lock = new RedisDistributedLock(jedis, "my_lock", 10000);
// 尝试获取锁
if (lock.acquireLock()) {
try {
// 执行你的业务逻辑
System.out.println("Lock acquired, executing business logic...");
} finally {
// 释放锁
lock.releaseLock();
System.out.println("Lock released.");
}
} else {
System.out.println("Unable to acquire lock, exiting...");
}
// 关闭 Jedis 连接
jedis.close();
}
}注意 lockValue 需要保证唯一,防止被别的客户端释放了锁。
这里有个问题,如果业务还没执行完,则 Redis 的锁已经到期了怎么办? 因此引入“看门狗”机制,即起一个后台定时任务,不断地给锁续期,如果锁释放了或客户端实例被关闭则停止续期,Redison 提供了此功能注意:未指定超时时间的分布式锁才会续期,如果指定了超时时间则不会续期,默认 30s 超时,每 10s 续期一次,续期时长为 30s。
除了锁续期问题,还有单点故障问题,如果这台 Redis 挂了怎么办?分布锁就加不上了,业务就被阻塞了。因此需要引入Redis 主从,利用哨兵进行故障转移,但是这又会产生新的问题,如果 master挂了,锁的信息还未传给 slave节点,此时 slave 上是没加锁的,因此可能导致多个实例都成功上锁。所以 Redis 作者又提出了 RedLock 即红锁,通过引入多个主节点共同加锁来解决单点故障问题(没有哨兵和 slave 了)。
比如现在有5个Reds 节点(官方推荐至少5个),客户端获取当前时间T1,然后依次利用SETNK对5个 Reds节点加锁,如果成功3个及以上(大多数),再次获取当前时间T2,如果 T2-T1小于锁的超时时间,则加锁成功,反之则失败
如果加锁失败则向全部节点调用释放锁的操作。
但是这个 redlock 还是有缺点的,首先它比较重,需要5个实例,成本不低。
其次如果发生时钟偏移,比如5个节点中有几个节点时间偏移了,导致锁提前超时了,那么有可能出现新客户端争抢到锁的情况,但是这个属于运维层面的问题
还有一个就是 GC问题,如果客户端抢到锁之后,发生了长时的 Gc导致redis中锁都过期了,这样一来别的客户端就能得到锁了,目老客户端 GC后正常执行后续的课作,导致并发修改,数据可能就不对了,不过这个问题无法避免,任可锁都可能会这样。
关于 RedLock 可以使用 Redisson ,它提供了 RedLock。
一般业务上,如果我们要使用 Redis 分布式锁,基本上使用 Redisson 客户端。
ZooKeeper
除了 Redis 还可以使用 ZooKeeper 的临时有序节点实现分布式锁
临时 能保证超时释放,有序 能选出谁抢到了锁。
大致流程:多进程争抢创建 ZooKeeper 指定目录下的临时有序节点,创建序号最小的节点即抢到锁的进程,释放锁可以删除此节点,如果服务端挂了也会释放这个节点。
优点:如果本身已经引入了 ZooKeeper 则成本不大,实现比较简单。
缺点:相比于Redis 性能没那么好,ZooKeeper的写入只能写到主节点,然后同步到从节点。并且临时节点如果产生网络抖动,节点也会被删除,导致多个客户端抢到锁(当然有重试机制,产生的概率比较低)
可使用 curator 客户端实现的分布式锁接口。
