1. 分布式锁
- 概念:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
- 场景:分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。
2. redis Template 实现分布式锁
2.1 DistributedLock接口
public interface DistributedLock {
/**
* 获取锁
* @param requestId 请求id uuid
* @param key 锁key
* @param expiresTime 失效时间 ms
* @return 获取锁成功 true
*/
boolean lock(String requestId,String key,int expiresTime);
/**
* 释放锁
* @param key 锁key
* @param requestId 请求id uuid
* @return 成功释放 true
*/
boolean releaseLock(String key,String requestId);
}
2.2 Spring Boot 中配置 redis Template
参照 redis template 配置
2.3 获取锁实现
获取锁,采用的是lua脚本,这样可以保证加锁 和 设置失效时间的原子性。
避免获取锁成功后,异常退出,造成锁无法释放的问题。
- lua脚本
lua 脚本配置在 application.properties中,jedis 中 setnx 命令 可以 直接设置失效时间,但是使用Spring Boot redis Template 没找到带失效时间的api。
lua.lockScript=if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then return redis.call('expire',KEYS[1],ARGV[2]) else return 0 end
- 加锁实现
public boolean lock(String requestId, String key, int expiresTime) {
DefaultRedisScript longDefaultRedisScript = new DefaultRedisScript<>(luaScript.lockScript, Long.class);
Long result = stringRedisTemplate.execute(longDefaultRedisScript, Collections.singletonList(key), requestId,String.valueOf(expiresTime));
return result == 1;
}
2.4 释放锁实现
锁的释放,要保证释放的锁就是自己获取的锁.如果释放了别人已经获取的锁,就会乱套了。
- 释放锁lua脚本
lua.releaseLockScript=if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
- 释放锁实现
@Override
public boolean releaseLock(String key, String requestId) {
DefaultRedisScript longDefaultRedisScript = new DefaultRedisScript<>(luaScript.releaseLockScript, Long.class);
Long result = stringRedisTemplate.execute(longDefaultRedisScript, Collections.singletonList(key), requestId);
return result == 1;
}
3 使用分布式锁
项目中使用分布式锁可以结合 Spring AOP 使用。
3.1 加锁注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface AddLock {
//spel表达式
String spel() ;
//失效时间 单位秒
int expireTime() default 10;
//log信息
String logInfo() default "";
}
3.2 AOP 切面
获取加锁的key 是通过 解析Spel 表达式完成的,SpelUtil 的代码 参考SpelUtil。
@Aspect
@Component
public class AddLockAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private DistributedLock distributedLock;
@Pointcut("@annotation(com.example.demo.annotation.AddLock)")
public void addLockAnnotationPointcut() {
}
@Around(value = "addLockAnnotationPointcut()")
public Object addKeyMethod(ProceedingJoinPoint joinPoint) throws Throwable {
//定义返回值
Object proceed;
//获取方法名称
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
AddLock addLock = AnnotationUtils.findAnnotation(method, AddLock.class);
String logInfo = getLogInfo(joinPoint);
//前置方法 开始
String redisKey = getRediskey(joinPoint);
logger.info("{}获取锁={}",logInfo,redisKey);
// 获取请求ID;保证 加锁者 和释放者 是同一个。
String requestId = UUID.randomUUID().toString();
boolean lockReleased = false;
try {
boolean lock = distributedLock.lock(requestId, redisKey, addLock.expireTime());
if (!lock ) {
throw new RuntimeException(logInfo+":加锁失败");
}
// 目标方法执行
proceed = joinPoint.proceed();
boolean releaseLock = distributedLock.releaseLock(redisKey, requestId);
lockReleased = true;
if(releaseLock){
throw new RuntimeException(logInfo+":释放锁失败");
}
return proceed;
} catch (Exception exception) {
logger.error("{}执行异常,key = {},message={}, exception = {}", logInfo,redisKey, exception.getMessage(), exception);
throw exception;
} finally {
if(!lockReleased){
logger.info("{}异常终止释放锁={}",logInfo,redisKey);
boolean releaseLock = distributedLock.releaseLock(redisKey, requestId);
logger.info("releaseLock="+releaseLock);
}
}
}
/**
* 获取 指定 loginfo
* 需要接口方法声明处 添加 AddLock 注解
* 并且 需要填写 loginfo
* @param joinPoint 切入点
* @return logInfo
*/
private String getLogInfo(ProceedingJoinPoint joinPoint){
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
AddLock annotation = AnnotationUtils.findAnnotation(method, AddLock.class);
if(annotation == null){
return methodSignature.getName();
}
return annotation.logInfo();
}
/**
* 获取拦截到的请求方法
* @param joinPoint 切点
* @return redisKey
*/
private String getRediskey(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
Object target = joinPoint.getTarget();
Object[] arguments = joinPoint.getArgs();
AddLock annotation = AnnotationUtils.findAnnotation(targetMethod, AddLock.class);
String spel=null;
if(annotation != null){
spel = annotation.spel();
}
return SpelUtil.parse(target,spel, targetMethod, arguments);
}
}
3.3 应用实列
分布式锁 和 Spring 申明式事务配合问题:
在分布式锁 和 Spring事务 配合使用的时候,有个问题:用分布式锁 保护 数据库事务。
执行顺序 1. 获取锁 ,2. 开启事务 ,3.事务提交,4.释放锁。
但是如果 在第4步, 释放锁失败,就是事务执行超过了 锁的失效时间,锁自动释放,那么事务怎么回滚。
我想到的解决方案是 添加 比 锁失效时间 小 一点的事务失效时间。
可以利用@Transaction 的timeout 属性。设置比锁失效时间小一些的 失效时间。
在分布式锁失效之前,事务会先超时,并且回滚事务。
@AddLock(spel = "'spel:'+#p0",logInfo = "测试分布式锁")
@Transactional(timeout = 5)
public void doWorker(String key) {}
4. 总结
现在的redis 分布式锁 实现 仍然存在问题,
- 存在单点故障问题,官方给出了 解决的方法,就是RedLock算法。
- 获取锁失败后,只能抛出异常,不能阻塞线程。
Redisson 开源框架 解决了 这些问题 。