方案:
- 需要幂等的接口,前端发起请求前,先请求后端接口获取一个幂等字符串,后端生成一个字符串,并存入Redis
- 前端发起真正的接口请求时需要在请求头带上这个字符串
- 后端基于AOP在需要幂等的接口做拦截,从请求头拿到这个幂等字符串
- 校验幂等字符串,校验成功则删除该幂等字符串,则放行执行真正的业务逻辑,校验失败则把该请求视为无效请求
实现
定义一个幂等注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoIdempotent {
}
需要幂等的接口加上这个注解
@AutoIdempotent
@PostMapping("/pay")
public void pay(@RequestBody @Validated OrderPayParam param) {
orderService.pay(param);
}
定义幂等接口
public interface Idempotence {
/**
* 检查是否存在幂等号
*
* @param idempotenceId 幂等号
* @return 是否存在
*/
boolean check(String idempotenceId);
/**
* 记录幂等号
*
* @param idempotenceId 幂等号
*/
void record(String idempotenceId);
/**
* 记录幂等号
*
* @param idempotenceId 幂等号
* @param time 过期时间
*/
void record(String idempotenceId, Integer time);
/**
* 删除幂等号
*
* @param idempotenceId 幂等号
*/
void delete(String idempotenceId);
/**
* 生成幂等号
*
* @return uId
*/
String generateId();
/**
* 从Header里面获取幂等号
*
* @return idempotenceId
*/
String getHeaderIdempotenceId();
}
幂等接口实现类
@Service
public class RedisIdempotenceImpl implements Idempotence {
private final RedisUtil redisUtil;
public RedisIdempotenceImpl(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
@Override
public boolean check(String idempotenceId) {
return redisUtil.isKeyExist(idempotenceId);
}
@Override
public void record(String idempotenceId) {
redisUtil.set(idempotenceId, "1");
}
@Override
public void record(String idempotenceId, Integer time) {
redisUtil.setex(idempotenceId, "1", time);
}
@Override
public void delete(String idempotenceId) {
redisUtil.deleteKey(idempotenceId);
}
@Override
public String generateId() {
String uuid = UUID.randomUUID().toString();
String uId = Base64Util.encode(uuid).toLowerCase();
redisUtil.setex(uId, "1", 86400);
return uId;
}
@Override
public String getHeaderIdempotenceId() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request.getHeader("idempotenceId");
}
}
定义AOP增强类
@Aspect
@Slf4j
@Component
public class IdempotenceSupportAdvice {
private final Idempotence idempotence;
private final ReentrantLockUtil reentrantLockUtil;
private final String IDEMPOTENCE_LOCK_PREFIX = "IDEMPOTENCE_LOCK:";
public IdempotenceSupportAdvice(Idempotence idempotence, ReentrantLockUtil reentrantLockUtil) {
this.idempotence = idempotence;
this.reentrantLockUtil = reentrantLockUtil;
}
/**
* 拦截有@AutoIdempotent注解的方法
*/
@Pointcut("@annotation(com.yamu.bns.annotation.AutoIdempotent)")
public void idempotenceMethod() {
}
@AfterThrowing(value = "idempotenceMethod()()", throwing = "e")
public void afterThrowing(Throwable e) {
// 从HTTP header中获取幂等号idempotenceId
String idempotenceId = idempotence.getHeaderIdempotenceId();
if (StringUtils.isNotBlank(idempotenceId))
idempotence.delete(idempotenceId);
}
/**
* 让第一个请求拿到锁,执行接下来的 验证幂等字符串存在和删除幂等字符串,最后执行业务逻辑,其余请求全部获取锁不成功则直接返回
*
* @param joinPoint ProceedingJoinPoint
* @return joinPoint.proceed()
* @throws Throwable Throwable
*/
@Around(value = "idempotenceMethod()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 从HTTP header中获取幂等号idempotenceId
String idempotenceId = idempotence.getHeaderIdempotenceId();
if (StringUtils.isEmpty(idempotenceId) ||
!reentrantLockUtil.tryLock(IDEMPOTENCE_LOCK_PREFIX + idempotenceId, 0, 600) ||
!idempotence.check(idempotenceId))
// idempotenceId为空 || 加锁不成功 || 幂等号不存在则直接返回
throw new BusinessException(BnsResultCode.SYSTEM_INVALID_REQUEST);
// 删除幂等号
idempotence.delete(idempotenceId);
// 执行业务方法
return joinPoint.proceed();
}
}
ReentrantLockUtil是一个自定义的基于Redission的加锁工具类。这里使用一般的Redis工具类setKey也行。