Spring-Boot快速集成Redis分布式锁(基于Jedis实现,参照CAS乐观锁设计)
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
前提
所有子系统,所有并发线程,在同一时刻只有一处可以取得锁,可以进行读写操作。所以全局只能有一个对所有子系统,所有并发线程可见的标识。通过该标识来控制全局线程。
分析
分布式锁一般常见实现方式
实现方式 |
---|
基于数据库实现分布式锁; |
基于缓存(Redis等)实现分布式锁; |
基于Zookeeper实现分布式锁; |
此篇博客主要介绍redis实现分布式锁,同时保证并发线程,和外部子系统的可用的分布式锁。
基于redis,首先一点,setnx指令,setnx(key,value),当key存在时返回0,当key不存在时,设定成功返回1。所以全局的取锁操作都可以用该指令来实现,通过返回值的0或1来判断是否取得锁。释放锁使用redis的delete指令来实现。
具体实现
1.实现取锁操作,同时保证不会死锁
使用setnx指令尝试取锁,当取锁失败时,参照CAS乐观锁,开始内部自旋,尝试下次取锁,当锁取得成功时,使用expire指令设置过期时间,保证不会出现死锁的情况。
/**
* jedis实例管理服务
*/
@Autowired
private JedisService jedisService;
/**
* jedis实例
*/
private volatile Jedis jedis;
/**
* threadLocal当前线程持有
*/
private volatile ThreadLocal<String> threadLocal = new ThreadLocal<>();
public boolean lock(String key) {
//加锁用key取得
String lockKey = getLockKey(key);
boolean result;
if(jedis==null){
jedis = jedisService.getJedis();
}
//锁版本取得
String lockVersion = getLockVersion();
//当前线程所持有的版本号设置
threadLocal.set(lockVersion);
//自旋
out:for(;;) {
//尝试取得锁
result = jedis.setnx(lockKey, lockVersion)>0;
//锁取得成功
if(result){
//设置过期时间,防止死锁
jedis.expire(lockKey, 30);
break out;
}
}
return result;
}
private String getLockKey(String key){
return key+".global.lock";
}
private String getLockVersion(){
//TODO
}
2.保证分布式锁全局的版本号唯一
我这里使用的UUID+线程号的方式保证分布锁所覆盖的范围版本号唯一,同时将版本号保存在ThreadLocal中,保证单一线程持有,线程安全。
private String getLockVersion(){
//UUID随机字符串取得
String uuid = UUID.randomUUID().toString().replaceAll("_", "");
//拼接当前的线程号
uuid+=Thread.currentThread().getId();
return uuid;
}
3.锁释放操作,只释放当前线程所持有的锁
锁释放时,要确保当前释放的锁是归当前线程所有。
public boolean unlock(String key) {
String lockKey = getLockKey(key);
boolean result;
if(jedis==null){
jedis = jedisService.getJedis();
}
//判断锁是否为当前线程所持有
if(jedis.get(lockKey).equals(threadLocal.get())){
//尝试释放锁
result = jedis.del(lockKey)>0;
//锁释放成功
} else {
result = false;
}
return result;
}
测试
@Test
void testRedis() {
globalLock.lock("test01");
jedisService.asString().set("test01", "test01String");
globalLock.unlock("test01");
jedisService.asString().get("test01");
jedisService.asList().lpush("test02", "testList01");
jedisService.asList().get("test02");
}
缺陷
setnx指令和expire指令没办法保证原子性,可能出现setnx指令成功后续节点失败,导致没有设定失效时间,在未手动释放锁的情况下,会出现死锁。所需要用到redis的事务管理,或使用带有参数的set指令来保证原子性。
使用方式
通过手动去调用取锁,和释放锁的方式不太优雅
可以使用切面编程的思想来优化使用方法
定义方法拦截器
**
* 分布式锁拦截器
* @author machenike
*/
public class JedisLockInterceptor implements MethodInterceptor {
GlobalLock globalLock;
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result;
//方法取得
Method method = methodInvocation.getMethod();
//实参取得
Object[] args = methodInvocation.getArguments();
//判断方法注解
if(method.getAnnotation(JedisLock.class)!=null){
Parameter[] parameters = method.getParameters();
Parameter LockParameter =parameters[0];
String lockKey =args[1].toString();
int index = 0;
//迭代形参
out:for(Parameter parameter:parameters){
Annotation [] annotations =parameter.getAnnotations();
//判断形参注解
for(Annotation annotation:annotations){
if(annotation instanceof LockKey){
LockParameter = parameter;
lockKey = args[index].toString();
break out;
}
}
index++;
}
//取锁
globalLock.lock(lockKey);
result = methodInvocation.proceed();
//释放锁
globalLock.unlock(lockKey);
} else {
result = methodInvocation.proceed();
}
return result;
}
public JedisLockInterceptor(GlobalLock globalLock) {
this.globalLock = globalLock;
}
}
此处需要需要定义两个注解@JedisLock 用方法注解,@LockKey 用于参数注解,当把这个拦截器加入到方法拦截链中就可以实现注解方式实现分布式锁。
/**
* @author machenike
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JedisLock {
}
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockKey {
}
使用方式
@JedisLock
public Boolean set(@LockKey String key,String value){
//TODO
}
源码demo
https://github.com/DavidLei08/BlogRedis/tree/master/src/main/java/club/blog/redis/lock