分布式幂等性
网络抖动、用户手抖、MQ重试.....任何一次“重复请求”都可能让扣钱、商品多发。
接口幂等性指:同一个请求(参数完全相同)重复调用多次,后端的处理结果应一致,且不会产生副作用(如重复扣款、重复入库)
token令牌
思路:先拿令牌->再执行业务->用完删除。
核心关键字:预生成、一次性、Redis原子删除。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private StringRedisTemplate redis;
// ① 预生成 Token,给前端
@GetMapping("/token")
public String getToken() {
//也可以将token的生成规则用业务规则生成,保证唯一性。
String token = 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) {
String key = "tk:" + token;
// 原子删除:成功返回 true 表示第一次使用
Boolean first = redis.delete(key);
if (Boolean.FALSE.equals(first)) {
return Result.fail("**请勿重复下单**");
}
// 真正创建订单
Order order = orderService.create(req);
return Result.ok(order);
}
}
数据库唯一索引
思路:“把业务唯一键”做成唯一索引,重复写入直接抛异常。
核心关键字:天然幂等
@Entity
@Table(name = "t_payment",
uniqueConstraints = @UniqueConstraint(columnNames = "transaction_id"))
public class Payment {
@Id
private Long id;
// 支付平台返回的流水号
@Column(name = "transaction_id")
private String txId;
private BigDecimal amount;
private String status;
}
@Service
public class PayService {
@Autowired
private PaymentRepo repo;
public Result pay(PayReq req) {
try {
Payment p = new Payment();
p.setTxId(req.getTxId());
p.setAmount(req.getAmount());
p.setStatus("SUCCESS");
repo.save(p); // 重复就抛 DataIntegrityViolationException
return Result.ok("**支付成功**");
} catch (DataIntegrityViolationException e) {
// 异常即查询结果,避免重复扣款
Payment exist = repo.findByTxId(req.getTxId());
return Result.ok("**已支付**", exist.getId());
}
}
}
分布式锁
核心思想:对“订单号 / 用户ID”加分布式锁,抢到锁再干活。
String lockKey = "order:lock:" + userId;
boolean locked = redisLock.tryLock(lockKey, 10); // 尝试获取锁 10 秒有效
if (!locked) {
return "请勿重复下单";
}
try {
// 下单逻辑
} finally {
redisLock.unlock(lockKey);
}
内容摘要
核心思想:用请求内容生成摘要(如 MD5/SHA256 哈希),在 Redis 或数据库中做去重判断,相同请求内容就只处理一次。
注:对请求体(如 JSON 参数)做规范化处理(去空格、排序字段等)
String redisKey = "idem:order:" + digest;
Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, "1", Duration.ofMinutes(10));
if (!Boolean.TRUE.equals(success)) {
return Result.fail("请勿重复提交");
}
MD5可以把任意长度报文压缩为32位十六进制内容,冲突概率极低。

浙公网安备 33010602011771号