Java基于Redisson实现幂等性组件
何为幂等?
**幂等(Idempotent)**是一个数学和计算机科学中的概念,主要描述的是一种属性,即一个操作可以被多次应用,但结果仍然保持不变。在数学中,幂等通常用于描述某些运算或函数的特性。例如,对于单目运算,如果一个运算对于在范围内的所有数,多次进行该运算所得的结果和进行一次该运算所得的结果相同,那么该运算就是幂等的。在双目运算中,如果当参与运算的两个值是等值的情况下,运算结果与参与运算的两个值相等,那么该运算也是幂等的。
实现幂等性的方式
主要有三种:
- 基于分布式锁
- 利用MySQL的去重性,做去重表
- 前端请求带Token,后端根据Token做幂等校验
基于Redisson分布式锁实现幂等
幂等注解:
@Target(value = {ElementType.TYPE, ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RepeatExecuteLimit {
String name() default "";
String[] keys();
// 保持幂等的时间
long durationTime() default 0L;
String message() default "提交频繁,请稍后重试";
}
切面类:
@Slf4j
@Aspect
@Order(-11)
public class RepeatExecuteLimitAspect {
private LocalLockCache localLockCache;
private RedissonClient redissonClient;
private ExpressionParser parser;
public RepeatExecuteLimitAspect(LocalLockCache localLockCache, RedissonClient redissonClient) {
this.localLockCache = localLockCache;
this.redissonClient = redissonClient;
this.parser = new SpelExpressionParser();
}
@Around("@annotation(repeatLimit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatExecuteLimit repeatLimit) throws Throwable {
long durationTime = repeatLimit.durationTime();
String message = repeatLimit.message();
Object obj;
String lockName = getLockName(joinPoint, repeatLimit);
String repatFlageName = "repeat_flag" + lockName;
// 判断幂等标识是否存在
String flag = (String) redissonClient.getBucket(repatFlageName).get();
if ("success".equals(flag)) {
// 触发了幂等
throw new RuntimeException(message);
}
// 没有触发幂等,先获取本地锁
ReentrantLock localLock = localLockCache.getLock(lockName, true);
if (!localLock.tryLock()) {
// 触发了幂等
throw new RuntimeException(message);
}
try {
// 获取分布式锁
RLock lock = redissonClient.getLock(lockName);
if (!lock.tryLock()) {
// 触发幂等
throw new RuntimeException(message);
}
try {
obj = joinPoint.proceed();
if (durationTime > 0) {
// 设置幂等标志,同时设置过期时间
redissonClient.getBucket(repatFlageName).set("success", durationTime, TimeUnit.SECONDS);
}
return obj;
} finally {
// 分布式锁解锁
lock.unlock();
}
} finally {
// 释放本地锁
localLock.unlock();
}
}
private String getLockName(ProceedingJoinPoint joinPoint, RepeatExecuteLimit repeatLimit) {
String name = repeatLimit.name();
String[] keys = repeatLimit.keys();
StringBuilder sb = new StringBuilder(name);
for (String key : keys) {
EvaluationContext context = new StandardEvaluationContext();
Object[] args = joinPoint.getArgs();
String[] paramNames = new DefaultParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
String value = parser.parseExpression(key).getValue(context, String.class);
sb.append("_").append(value);
}
return sb.toString();
}
}
本地锁使用Caffine做实现,Caffine是线程安全的,加上缓存过期,清理不常用的锁:
public class LocalLockCache {
private Cache<String, ReentrantLock> localLockCache;
@Value("${durationTime:48}")
private Integer durationTime;
@PostConstruct
public void init() {
this.localLockCache = Caffeine.newBuilder()
.expireAfterWrite(durationTime, TimeUnit.HOURS)
.build();
}
public ReentrantLock getLock(String lockKey, boolean fair) {
return localLockCache.get(lockKey, key -> new ReentrantLock(fair));
}
}
自动装配类:
public class RepateAutoConfig {
@Bean
public LocalLockCache localLockCache() {
return new LocalLockCache();
}
@Bean
public RepeatExecuteLimitAspect repeatExecuteLimitAspect(LocalLockCache localLockCache, RedissonClient redissonClient) {
return new RepeatExecuteLimitAspect(localLockCache, redissonClient);
}
}
在resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中声明自动装配类信息:
com.jie.config.RepateAutoConfig
至此,一个幂等组件就封装好了。
测试
编写测试类:
@RestController
public class TestController {
@GetMapping("/test")
@RepeatExecuteLimit(name = "test", keys = {"#test"}, durationTime = 10L, message = "提交频繁,请稍后重试")
public String test() {
return "hello world";
}
}
浏览器调用方法,多次刷新会看到”提交频繁,请稍后重试“的信息。