在项目中遇到了插入重复数据的问题,为了防止并发造成重复数据的插入,故使用加锁,而传统的加锁是单机锁并不适用分布式项目,故采用Redis分布式锁。
步骤 | 线程一操作 | 线程二操作 |
---|---|---|
第一步 | 获取锁 | 获取锁 |
第二步 | 获取到锁,执行业务代码 | 获取不到锁,等待/返回失败 |
第三步 | 释放锁 | ………… |
RepeatSubmit
/**
* @author :xxx
* @date :Created in 2023/5/23
* @description: 重复提交注解
* @version: 1.0.0
*/
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交(设置间隔时间应大于业务执行时间,默认12000ms)
*/
long interval() default 12000;
/**
* 提示消息
*/
String message() default "不允许重复提交,请稍候再试";
/**
* 支持自定义key后缀
*/
String key() default "";
/**
* 开启锁续期,默认开启(锁续期是为了防止代码时间执行>锁过期时间,锁提前过期,造成并发)
*/
boolean watchLog() default true;
}
RepeatSubmitAspect
/**
* @author xxx
* @version 1.0
* @description: 重复提交切面类(适用于防止重复点击造成并发的接口,使用redis锁实现)
* @date 2023/5/23
*/
@Component
@Aspect
@Slf4j
public class RepeatSubmitAspect {
private final RedisLockTemplate redisLockTemplate;
public RepeatSubmitAspect(RedisLockTemplate redisLockTemplate) {
this.redisLockTemplate = redisLockTemplate;
}
@Pointcut("@annotation(RepeatSubmit注解的全限定名)")
private void repeatSubmit() {}
@Around("repeatSubmit()")
public Object repeatSubmitAspectAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class);
String key = redisLockTemplate.buildKey(method, repeatSubmit);
// 返回结果
Object result = null;
// 是否获取到锁标志
Boolean getLock = false;
try {
// 加锁
getLock = redisLockTemplate.getLock(key, repeatSubmit.interval());
if (getLock) {
log.info("{}:成功获取[{}]锁", Thread.currentThread().getName(), key);
// 是否开启锁续期
if(repeatSubmit.watchLog()) {
// 开启锁续期(进行redis锁续期,防止锁提前过期(业务执行时间>锁过期时间)产生的并发问题)
log.info("{}:[{}]开启锁续期", Thread.currentThread().getName(), key);
// 锁续期次数初始化
AtomicInteger count = new AtomicInteger(0);
redisLockTemplate.watchdog(key, repeatSubmit.interval(), count);
}
// 开始执行业务代码
result = point.proceed();
log.info("业务代码执行完毕");
}
} catch (Exception e) {
// 捕获异常处理……
} finally {
if (getLock) {
// 如果获取锁,则释放锁
redisLockTemplate.releaseLock(key);
log.info("{}:成功释放[{}]锁", Thread.currentThread().getName(), key);
} else {
// 未获取到锁
log.info("{}:未获取[{}]锁", Thread.currentThread().getName(), key);
// 未获取到锁后续处理……
}
}
// 返回结果
return result;
}
}
RedisLockTemplate
/**
* @author xxx
* @version 1.0
* @description: redis锁模板类
* @date 2023/5/23
*/
@Slf4j
@Component
public class RedisLockTemplate {
@Resource(name = "实现数据序列化的redisTemplate名称")
private RedisTemplate redisTemplate;
private static final String DEFAULT_KEY_PREFIX = "自定义前缀名称:repeatsubmit:";
/**
* 定时器
*/
private HashedWheelTimer timer = new HashedWheelTimer(ThreadFactoryBuilder.create().setNamePrefix("锁续期线程-").build());
/**
* 锁续期lua脚本
*/
String luaScript = "if (redis.call('exists',KEYS[1]) == 1) then redis.call('pexpire',KEYS[1], ARGV[1]) return 1 end return 0";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript(luaScript, Boolean.class);
/**
* 加锁
**/
public Boolean getLock(String key, long expire){
// 设置锁value值
long value = System.currentTimeMillis() + expire;
// 判断不存在则设置锁返回true,存在则返回false(设置过期时间为了防止业务出问题宕机等问题造成死锁)
Boolean lockStatus = this.redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMillis(expire));
return lockStatus;
}
/**
* 释放锁
**/
public Boolean releaseLock(String key){
// 存在key删除返回true,否则返回false
String luaScript = "if redis.call('exists', KEYS[1]) > 0 then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Boolean> redisScript = new DefaultRedisScript<>(luaScript, Boolean.class);
Boolean releaseStatus = (Boolean) this.redisTemplate.execute(redisScript, Collections.singletonList(key));
return releaseStatus;
}
/**
* 构建key
* @param method
* @param repeatSubmit
* @return
*/
public String buildKey(Method method, RepeatSubmit repeatSubmit) {
// 使用包类方法名
StringBuilder sb = new StringBuilder(DEFAULT_KEY_PREFIX);
sb.append(method.getDeclaringClass().getName()).append(".").append(method.getName());
// 拼接用户Id
Long currentUserId = 获取登录用户id的方法;
sb.append(":" + currentUserId);
// 获取用户定义的key后缀
String suffixkey = repeatSubmit.key();
if (!StringUtils.isEmpty(suffixkey)) {
sb.append(":" + suffixkey);
}
return sb.toString();
}
public void watchdog(String key, long time, AtomicInteger count) {
timer.newTimeout(
timerTask -> {
try {
// 赋值执行脚本
Boolean execute = (Boolean) redisTemplate.execute(redisScript, Arrays.asList(key), Long.valueOf(time));
if (execute) {
log.info("{}:[{}]锁续期第{}次,", Thread.currentThread().getName(), key, count.incrementAndGet());
watchdog(key, time, count);
} else {
log.info("{}:[{}]锁已经正常释放,不进行锁续期", Thread.currentThread().getName(), key);
}
} catch (Exception e) {
log.info("{}:[{}]执行锁续期任务出错了,{}", Thread.currentThread().getName(), key, e.getMessage());
}
}, time / 3, TimeUnit.MILLISECONDS);
}
}
在要使用的方法前加注解即可
// 例如
@RepeatSubmit
@RequestMapping("test")
public void test() {
// 业务代码
}
未实现公平锁、可重入锁等操作,后续有完善优化空间……