分布式幂等性

网络抖动、用户手抖、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位十六进制内容,冲突概率极低。

posted @ 2025-07-30 23:03  Charlie-Pang  阅读(8)  评论(0)    收藏  举报