今天一次讲透 4 种常用幂等性实现,附可 copy 的代码 。
@RestController
@RequestMapping
(
"/order"
)
publicclassOrderController {
@Autowired
private
StringRedisTemplate redis;
// ① 预生成 Token,给前端
@GetMapping
(
"/token"
)
public
String
getToken
()
{
Stringtoken= UUID.randomUUID().toString();
// 10 分钟有效期,足够前端完成下单
redis.opsForValue().set(
"tk:"
+ token,
"1"
, Duration.ofMinutes(
10
));
return
token;
}
// ② 下单接口,Header 中带令牌
@PostMapping
public
Result
create
(@RequestHeader(
"Idempotent-Token"
)
String token,
@RequestBody OrderReq req)
{
Stringkey=
"tk:"
+ token;
// 原子删除:成功返回 true 表示第一次使用
Booleanfirst= redis.delete(key);
if
(Boolean.FALSE.equals(first)) {
return
Result.fail(
"**请勿重复下单**"
);
}
// 真正创建订单
Orderorder= orderService.create(req);
return
Result.ok(order);
}
}
@Entity
@Table
(name =
"t_payment"
,
uniqueConstraints =
@UniqueConstraint
(columnNames =
"transaction_id"
))
publicclassPayment {
@Id
private
Long id;
// 支付平台返回的流水号
@Column
(name =
"transaction_id"
)
private
String txId;
private
BigDecimal amount;
private
String status;
}
@Service
publicclassPayService {
@Autowired
private
PaymentRepo repo;
public
Result
pay
(PayReq req)
{
try
{
Paymentp=newPayment();
p.setTxId(req.getTxId());
p.setAmount(req.getAmount());
p.setStatus(
"SUCCESS"
);
repo.save(p);
// 重复就抛 DataIntegrityViolationException
return
Result.ok(
"**支付成功**"
);
}
catch
(DataIntegrityViolationException e) {
// 异常即查询结果,避免重复扣款
Paymentexist= repo.findByTxId(req.getTxId());
return
Result.ok(
"**已支付**"
, exist.getId());
}
}
}
@Service
publicclassStockService {
@Autowired
private
RedissonClient redisson;
public
Result
deduct
(DeductCmd cmd)
{
StringlockKey=
"lock:stock:"
+ cmd.getProductId();
RLocklock= redisson.getLock(lockKey);
try
{
// 最多等 3 秒,持锁 5 秒自动释放
if
(!lock.tryLock(
3
,
5
, TimeUnit.SECONDS)) {
return
Result.fail(
"**处理中,请稍后**"
);
}
// 业务幂等检查:根据请求 ID 查记录
if
(repo.existsByRequestId(cmd.getRequestId())) {
return
Result.ok(
"**已扣减**"
);
}
// 真正扣库存
repo.deductStock(cmd);
return
Result.ok(
"**扣减成功**"
);
}
catch
(InterruptedException e) {
Thread.currentThread().interrupt();
return
Result.fail(
"**系统繁忙**"
);
}
finally
{
if
(lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
@Target
(ElementType.METHOD)
@Retention
(RetentionPolicy.RUNTIME)
public
@interface
Idempotent {
intexpire()default3600;
// 秒
}
@Aspect
@Component
publicclassIdempotentAspect {
@Autowired
private
StringRedisTemplate redis;
@Around
(
"@annotation(idem)"
)
public
Object
around
(ProceedingJoinPoint pjp, Idempotent idem)
throws
Throwable
{
HttpServletRequestreq= ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// ① 计算请求体摘要
Stringbody= IOUtils.toString(req.getReader());
Stringdigest= DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8));
Stringkey=
"idem:digest:"
+ digest;
// ② 第一次:setIfAbsent 返回 true
Booleanabsent= redis.opsForValue().setIfAbsent(key,
"1"
, Duration.ofSeconds(idem.expire()));
if
(Boolean.FALSE.equals(absent)) {
return
Result.fail(
"**重复请求**"
);
}
try
{
return
pjp.proceed();
}
catch
(Exception e) {
redis.delete(key);
// 异常时释放,允许重试
throw
e;
}
}
}
// 使用
@RestController
publicclassTransferApi {
@PostMapping
(
"/transfer"
)
@Idempotent
(expire =
7200
)
public
Result
transfer
(@RequestBody TransferCmd cmd)
{
return
Result.ok(transferSvc.doTransfer(cmd));
}
}|
|
|
|
|
|
|---|---|---|---|---|
| Token |
|
|
|
|
| 唯一索引 |
|
|
|
|
| 分布式锁 |
|
|
|
|
| 内容摘要 |
|
|
|
|
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70019641/viewspace-3187006/ ,如需转载,请注明出处,否则将追究法律责任。