基于注解实现分布式锁防重复提交
主要解决的是防止同一优惠卷重复提交的问题。
问题产生原因:
- 网络延迟:用户的网络连接可能有延迟,或者用户无意中刷新了页面,导致按钮被点击多次,系统接收到多个相同的请求。
- 按钮未禁用:前端页面中的按钮在用户点击后没有及时禁用,导致用户可以多次点击,从而发起多个创建请求。
- 系统处理延迟:系统在处理请求时可能出现延迟,用户误以为请求没有成功,从而重复提交相同的请求。
为什么不能使用本地锁?
- 范围有限:本地锁仅在应用程序的单个实例中有效。如果你的应用程序在多台服务器上运行(即分布式环境),每个实例的本地锁相互独立,无法在集群中共享锁状态。
- 竞争条件:不同实例上的本地锁无法相互感知,这意味着多个实例可能同时认为自己获得了锁,从而导致并发冲突。
分布式锁 KEY 设计:
- 分布式锁前缀。
- 请求路径。
- 当前访问用户。
- 参数 MD5。
String lockKey = String.format("no-duplicate-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(requestParam));
private final RedissonClient redissonClient;
@Override
public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
// 获取分布式锁标识
String lockKey = String.format("no-duplicate-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(requestParam));
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,获取锁失败就意味着已经重复提交,直接抛出异常
if (!lock.tryLock()) {
throw new ClientException("请勿短时间内重复提交优惠券模板");
}
try {
// 执行常规业务代码
// ......
} finally {
lock.unlock();
}
}
/**
* @return 获取当前线程上下文 ServletPath
*/
private String getServletPath() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return sra.getRequest().getServletPath();
}
/**
* @return 当前操作用户 ID
*/
private String getCurrentUserId() {
// 用户属于非核心功能,这里先通过模拟的形式代替。后续如果需要后管展示,会重构该代码
return "1810518709471555585";
}
/**
* @return joinPoint md5
*/
private String calcArgsMD5(CouponTemplateSaveReqDTO requestParam) {
return DigestUtil.md5Hex(JSON.toJSONBytes(requestParam));
}
以上就是分布式锁的基本逻辑,由于分布式锁在本项目中使用较多,我们考虑将其封装为组件。
组件封装于 framework-idempotent 包下: 自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoDuplicateSubmit {
/**
* 触发幂等失败逻辑时,返回的错误提示信息
*/
String message() default "您操作太快,请稍后再试";
}
AOP 切面:
@Aspect
@RequiredArgsConstructor
public final class NoDuplicateSubmitAspect {
private final RedissonClient redissonClient;
/**
* 增强方法标记 {@link NoDuplicateSubmit} 注解逻辑
*/
@Around("@annotation(com.nageoffer.onecoupon.framework.idempotent.NoDuplicateSubmit)")
public Object noDuplicateSubmit(ProceedingJoinPoint joinPoint) throws Throwable {
NoDuplicateSubmit noDuplicateSubmit = getNoDuplicateSubmitAnnotation(joinPoint);
// 获取分布式锁标识
String lockKey = String.format("no-duplicate-submit:path:%s:currentUserId:%s:md5:%s", getServletPath(), getCurrentUserId(), calcArgsMD5(joinPoint));
RLock lock = redissonClient.getLock(lockKey);
// 尝试获取锁,获取锁失败就意味着已经重复提交,直接抛出异常
if (!lock.tryLock()) {
throw new ClientException(noDuplicateSubmit.message());
}
Object result;
try {
// 执行标记了防重复提交注解的方法原逻辑
result = joinPoint.proceed();
} finally {
lock.unlock();
}
return result;
}
/**
* @return 返回自定义防重复提交注解
*/
public static NoDuplicateSubmit getNoDuplicateSubmitAnnotation(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = joinPoint.getTarget().getClass().getDeclaredMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
return targetMethod.getAnnotation(NoDuplicateSubmit.class);
}
/**
* @return 获取当前线程上下文 ServletPath
*/
private String getServletPath() {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return sra.getRequest().getServletPath();
}
/**
* @return 当前操作用户 ID
*/
private String getCurrentUserId() {
// 用户属于非核心功能,这里先通过模拟的形式代替。后续如果需要后管展示,会重构该代码
return "1810518709471555585";
}
/**
* @return joinPoint md5
*/
private String calcArgsMD5(ProceedingJoinPoint joinPoint) {
return DigestUtil.md5Hex(JSON.toJSONBytes(joinPoint.getArgs()));
}
}
切面中,封装的就是之前准备和获取分布式锁的逻辑。
使用配置类将组件交给 Spring 管理:
public class IdempotentConfiguration {
/**
* 防止用户重复提交表单信息切面控制器
*/
@Bean
public NoDuplicateSubmitAspect noDuplicateSubmitAspect(RedissonClient redissonClient) {
return new NoDuplicateSubmitAspect(redissonClient);
}
}
配置自动发现:
com.nageoffer.onecoupon.framework.config.IdempotentConfiguration
引入注解:
@NoDuplicateSubmit
@Operation(summary = "商家创建优惠券模板")
@PostMapping("/api/merchant-admin/coupon-template/create")
public Result<Void> createCouponTemplate(@RequestBody CouponTemplateSaveReqDTO requestParam) {
couponTemplateService.createCouponTemplate(requestParam);
return Results.success();
}
Comments NOTHING