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<Long> 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<Long> 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 开源框架 解决了 这些问题 。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容

  • 分布式锁在很多场景中是非常有用的原语, 不同的进程必须以独占资源的方式实现资源共享就是一个典型的例子。例如目前所在...
    BeStronger30阅读 568评论 0 2
  • 引题 比如在同一个节点上,两个线程并发的操作A的账户,都是取钱,如果不加锁,A的账户可能会出现负数,正确的方式是对...
    阿康8182阅读 4,799评论 0 75
  • 明色的一角淌着琉璃 经过七彩的洗礼 天空终于蓝得可怕 仿佛触手可及 总能闻到厚重的青色之香 烈日伴之随行 透过钢铁...
    藤野海斗阅读 621评论 0 0
  • 2015/04/24 暂且今天就当第一天吧,得得瑟瑟的这几天一直在跑步,一直都是分享数据到朋友圈,我估计朋友圈的人...
    StevenDeveloper阅读 4,172评论 0 4
  • "不敢把朋友圈翻到最后 有太多的你们不敢去触碰" "也许不是合适的朋友 但也从不曾想过会以这样一种不堪的方式与你们...
    简zhi阅读 157评论 0 0