基于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…,如有侵权,请联系删除。