1. 关于分布式锁
分布式锁就是应用在分布式环境下多个节点之间进行同步或者协作的锁,分布式锁和普通锁一样,也需要有以下特性:
- 互斥性,保证只有持有锁的某个线程才能进行操作,即在任意时刻,只有一个节点的客户端能持有分布式锁;
- 可重入性,在同一个节点进程内,同一个线程可多次获取锁;
- 超时处理机制,需要支持超时自动释放锁,避免死锁的产生,以及避免其他节点长期等待造成的资源浪费;
- 锁释放机制,加锁和解锁必须是节点内的同一个线程;
2. RedisLockRegistry上手
RedisLockRegistry
是Spring-Integration集成工具包项目提供的基于Redis的分布式锁管理器,使用时,首先导入依赖:
1 | <dependency> |
其次配置分布式锁:
1 | /** |
RedisLockRegistry
相当于一个锁的管理器,所有的分布式锁都可以从中获取,如上定义,锁的键名为“redis-lock:你定义的key”,超时时间也可以自己设定,默认超时时间是60s。
使用分布式加锁时,只需要参考如下代码:
1 | // 锁测试 |
RedisLockRegistry
是基于Redis的setnx和ReentrantLock
可重入锁实现。下一章节,我们可以对其源码展开阅读及分析。
3. RedisLockRegistry源码分析
3.1 构造函数
1 |
|
其中OBTAIN_LOCK_SCRIPT
是一个上锁的lua脚本,因为若你在应用层面是分步骤的get/set/expire操作,是不符合原子性的,如果SETNX成功,在服务器挂掉、重启或网络问题等,导致EXPIRE命令没有执行,锁没有设置超时时间,后续就有可能变成死锁,所以最好的方式是通过lua脚本来实现加锁的操作。其lua加锁脚本为:
其中KEYS[1]代表当前锁的key值,ARGV[1]代表当前的客户端标识,ARGV[2]代表过期时间。首先根据KEYS[1]从redis中拿到对应的客户端标识,如果已存在的客户端标识和ARGV[1]相等,那么重置过期时间为ARGV[2];如果值不存在,设置KEYS[1]对应的值为ARGV[1],并且过期时间设置ARGV[2],从逻辑上来说,这就是一个简单的get和setnx操作。
3.2 获取锁
获取锁的代码如下:
1 | private final Map<String, RedisLock> locks = new ConcurrentHashMap<>(); |
RedisLockRegistry
维护了一个key-RedisLock类型的ConcurrentHashMap
,即在RedisLockRegistry
中,每个key对应一个RedisLock
。
3.3 RedisLock
RedisLock
是RedisLockRegistry
的内部实现类,实现了Lock
接口,是锁的定义和实现逻辑落地的类。首先看加锁过程:
1 | private final ReentrantLock localLock = new ReentrantLock(); |
从代码可以看到,lock方法首先尝试获取ReentrantLock
,如果获取,再尝试去获取分布式锁,使用localLock的目的在于减少节点本地多线程竞争分布式锁,使得每刻只有一个线程去竞争分布式锁,以减少不必要的资源开销,减轻Redis的压力。
本地线程如果获取不到分布式锁,则进行阻塞,直至获取到锁或者出现异常,所以每隔100毫秒会去尝试获取分布式锁,直到获取成功或者抛出异常为止。我们再来看下obtainLock方法的内容:
1 | private final long expireAfter; |
获取锁的过程比较简单,通过redisTemplate执行lua脚本获取Redis锁。
RedisLock
也定义了可中断锁的过程:
1 |
|
lock方法不会响应中断信号,lockInterruptibly方法利用ReentrantLock
的可中断机制,会响应中断信号,即假如获取锁的过程如果出现中断,则结束获取操作过程。
3.4 解锁过程
解锁的方法代码如下:
1 | // 解锁的入口 |
4. 使用总结
本文主要介绍了基于
5. 参考文档
以上内容就是关于RedisLockRegistry分布式锁应用及分析的全部内容了,谢谢你阅读到了这里!
Author:zhaoyh