基于lua脚本实现的setnx+expire 保证原子性的锁

69 阅读2分钟

基于lua脚本的锁的实现

@Component
public class RedisLocker {

  //  脚本逗号之前为设置key-value , 脚本之后为判断 当key不存在的时候,设置key的过期时间
    private final String LUA_LOCK_SCRIPT = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";

    private final RedisScript<String> redisLockScript = new DefaultRedisScript<String>(LUA_LOCK_SCRIPT, String.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public LockContext getKey(String key) {

        String lockKey = RedisConstant.LOCK_KEY + key;

        String lockValue = UUID.randomUUID().toString();

        String result = stringRedisTemplate.execute(redisLockScript,
                stringRedisTemplate.getStringSerializer(),
                stringRedisTemplate.getStringSerializer(),
                Collections.singletonList(lockKey),
                new Object[]{lockValue, "1000"});

        if (StrUtil.isNotEmpty(result) && StrUtil.equalsAnyIgnoreCase(result, "ok")) {
            return new LockContext(lockKey, lockValue);
        }
        return null;
    }

    @Override
    public void releaseLock(LockContext lockContext) {

    }

    @Override
    public void releaseLock(LockContext lockContext, Long expireTime) {
        if (lockContext == null || StrUtil.isEmpty(lockContext.getKey()) || StrUtil.isEmpty(lockContext.getValue())) {
            return;
        }
        String result = stringRedisTemplate.opsForValue().get(lockContext.getKey());
        // 判断是否是同一个锁
        if (!StrUtil.equals(lockContext.getValue(), result)) {
            return;
        }
        if (expireTime > 0) {
            stringRedisTemplate.expire(lockContext.getKey(), expireTime, TimeUnit.MILLISECONDS);
        } else {
            stringRedisTemplate.delete(lockContext.getKey());
        }
    }
}

类 LockContext

@NoArgsConstructor
@AllArgsConstructor
@Data
public class LockContext {

    private String key;

    private String value;
}

aop 拦截 NoRepeatSubmitAspect

@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAspect {

    @Autowired
    private RedisLocker redisLocker;

    @Pointcut("@annotation(com.jovision.mix.common.core.annoations.NoRepeatSubmit)")
    public void noRepeatSubmit() {
    }

    @Around(value = "noRepeatSubmit()")
    public Object checkRepeatSubmit(ProceedingJoinPoint jp) throws Throwable {

        // 获取请求参数
        String paramJSON = JSONUtil.toJsonStr(jp.getArgs());
        // 计算请求参数hash值
        String hash = String.valueOf(HashUtil.fnvHash(paramJSON));
        // 获取到分布式锁,如果获取不到,说明重复提交,直接丢弃
        LockContext lockContext = redisLocker.getKey(hash);
        if (lockContext != null) {
            log.info("获取到锁:{}", lockContext.getKey());
            Object reuslt = jp.proceed();
            redisLocker.releaseLock(lockContext);
            log.info("释放锁锁:{}", lockContext.getKey());
            return reuslt;
        }
        return null;
    }
}

此为校验接口幂等性,重复提交问题。根据aop拦截到访问接口的请求参数哈希值,以此为key,当首次访问,放入redis中(setnx)设置过期时间,返回结果为ok。当在过期时间范围内,再次访问(即重复提交问题),判读key是否存在,存在(氢请求参数相同),则在执行lua脚本的时间无法设置成功,返回结果为null。如果不存在(请求参数不同),非重复提交,那么执行lua脚本,结果为ok,执行代码。
其中 这里为核心判断逻辑

if (lockContext != null) {
            log.info("获取到锁:{}", lockContext.getKey());
            Object reuslt = jp.proceed();// 执行代码
            redisLocker.releaseLock(lockContext);
            log.info("释放锁锁:{}", lockContext.getKey());
            return reuslt;
        }
        return null;//不执行,直接返回

其中 这里为lua脚本加锁的核心

String lockKey = RedisConstant.LOCK_KEY + key;

        String lockValue = UUID.randomUUID().toString();
          
        // 设置key-value,当key不存在(首次访问),那么set成功,返回ok。
        // 如果key存在,那么 set 失败,返回null
        String result = stringRedisTemplate.execute(redisLockScript,
                stringRedisTemplate.getStringSerializer(),
                stringRedisTemplate.getStringSerializer(),
                Collections.singletonList(lockKey),
                new Object[]{lockValue, "1000"});

        if (StrUtil.isNotEmpty(result) && StrUtil.equalsAnyIgnoreCase(result, "ok")) {
            return new LockContext(lockKey, lockValue);
        }
        return null;

具体代码,详见test项目

https://gitee.com/flgitee/test

==================================经过最近的学习,我发现上面的方法太low了,繁琐===========================
最近有个更好的方法,即本身spring集成的redis就支持,setnx 和 ex 的原子操作,不过redis的版本要大于2.0 (现在都是5.几了),所以可以直接用原本的方法,只需要一行代码搞定~~~

Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,lockValue,1000L,TimeUnit.MILLISECONDS);

setIfAbsent这个方法就是两步操作的合体,可以保证原子性,结束战斗!!!!!

本文转自 jimolvxing.blog.csdn.net/article/det…,如有侵权,请联系删除。