Redis分布式锁
并发场景下,多个进程或线程共享资源的读写,需要保证对资源的访问互斥。
在单机系统中,我们可以使用Java并发包中的API、synchronized关键字等方式。
在分布式系统下,这些方式不再适用,因为不同的节点的Java后端代码都是单独运行的,所以不是需要单节点的锁,是需要多节点的锁,即:增加第三方唯一组件,实现分布式锁。
常见的分布式锁的实现方案有:基于数据库、基于Redis、基于Zookeeper。
分布式锁理论
1、命令处理阶段:Redis使用单线程处理,同一个key同时只有一个线程能够处理,没有多线程竞争问题。
2、SET key value NX PX milliseconds 命令在不存在key的情况下添加具有过期时间的key,为安全加锁提供支持。
3、Lua脚本和DEL命令为安全解锁提供可靠支撑。
于是我们有了以下的redis分布式锁代码
/**
* 尝试获取分布式锁
* @param jedis redis客户端
* @param lockKey 锁
* @param requestId 请求标识//不一定要是请求id,只要是能标识出是谁加锁的,方便后面释放锁
* @param expireTime 过期时间
* @return
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
/**
*释放锁
* @param jedis
* @param lockKey 锁
* @param requestId 请求标识//不一定要是请求id,只要是能标识出是谁加锁的,方便后面释放锁
*/
public static boolean releaseDistributedLock(Jedis jedis, 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 = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
这样的代码,能够实现:
1、使用set命令NX、PX选项进行加锁,保证了加锁互斥,避免了死锁;
2、使用lua脚本解锁,防止解除其他线程的锁;
3、加锁、解锁命令都是原子操作;
但前提条件:单机版Redis、开启AOF持久化方式并设置appendfsync=always
在哨兵模式和集群模式下存在问题,原因如下:
1、哨兵模式和集群模式基于主从架构,主从之间通过命令传播实现数据同步,而命令传播是异步的。
2、就存在主节点数据写入成功,在还未通知从节点情况下,主节点就宕机的可能。
3、当从节点通过故障转移提升为新的主节点后,其他线程就有机会重新加锁成功,导致不满足分布式锁的互斥条件。
集群下的分布式锁
由于主从同步基于异步复制原理,所以哨兵模式、集群模式天生无法满足此条件。
解决方案是:RedLock(Redis Distribute Lock),有多种实现,最后推荐的是 Redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.15.5</version> <!-- 请使用最新版本 -->
</dependency>
特点:可重入特性,按照RedLock进行实现,支持独立实例模式、集群模式、主从模式、哨兵模式。
有的机制:加锁机制、锁互斥机制、Watch dog 机制、可重入加锁机制、锁释放机制对 Redisson 实现分布式锁的底层原理进行分析。
Redisson 优点
1、通过 Watch Dog 机制很好的解决了锁的续期(监控续签,防止锁失效)问题。
2、和 Zookeeper 相比较,Redisson 基于 Redis 性能更高,适合对性能要求高的场景。
3、通过 Redisson 实现分布式可重入锁,比原生的 SET mylock userId NX PX milliseconds + lua 实现的效果更好些,虽然基本原理都一样,但是它帮我们屏蔽了内部的执行细节。
4、在等待申请锁资源的进程等待申请锁的实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。