Redis Lua脚本 实现分布式锁

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 分布式锁 实现 仍然存在问题,

  1. 存在单点故障问题,官方给出了 解决的方法,就是RedLock算法。
  2. 获取锁失败后,只能抛出异常,不能阻塞线程。
    Redisson 开源框架 解决了 这些问题 。

你可能感兴趣的:(Redis Lua脚本 实现分布式锁)