Token分布式幂等防重

wen java案例 3

分布式系统下的Token幂等防重机制:原理、实现与最佳实践

目录导读

  1. 什么是Token幂等防重?
  2. 为什么需要分布式幂等防重?
  3. Token生成与分发策略
  4. 核心实现架构(含流程图)
  5. 常见问题与FAQ
  6. 生产环境最佳实践

什么是Token幂等防重?

在分布式系统中,Token幂等防重是一种通过唯一令牌(Token)确保同一请求仅被处理一次的技术,其核心逻辑是:客户端发起请求前,先向服务端申请一个唯一Token;服务端将Token存入缓存(如Redis),客户端在后续请求中携带此Token;服务端收到请求后,检查Token是否存在,若存在则执行逻辑并立即删除Token,若不存在或已过期则拒绝处理。

Token分布式幂等防重

关键特性

  • 唯一性:每个Token全局唯一(通常基于UUID或雪花算法)。
  • 时效性:Token设置过期时间(如60秒),避免堆积。
  • 原子性:检查与删除操作需原子执行(Redis Lua脚本或SET NX EX命令)。

为什么需要分布式幂等防重?

在微服务、高并发场景下,以下问题迫使我们需要Token防重:

  • 网络重复请求:客户端超时重试、用户快速连击导致同一请求多次到达。
  • 分布式事务不一致:跨服务调用时,重复扣款、重复创建订单会破坏数据一致性。
  • 消息队列重复消费:Kafka/RabbitMQ的at-least-once语义导致同一消息被多次处理。

无防重机制的风险

当用户点击支付按钮两次,若未防重,系统可能扣款两次但只生成一笔订单,造成资损。


Token生成与分发策略

Token生成算法对比

算法 优势 劣势 适用场景
UUID 简单、无中心化依赖 字符串长、无序 低并发内部系统
雪花算法 时间有序、ID短 依赖机器时钟 高并发分布式系统
Redis原子递增 可控、可预测 依赖Redis 短生命周期Token

推荐方案
采用「全局唯一ID + 时间戳 + 随机数」组合,

Token = snowflakeId("{业务代码}") + "_" + System.currentTimeMillis() + "_" + random.nextInt(99999)

分发接口设计

GET /api/token/generate?bizType=order  
Response: { "token": "order_1698000001_123456", "expireTime": 60 }

要求:

  • Token需绑定业务类型(bizType)。
  • 客户端必须在有效期内使用Token,过期作废。

核心实现架构

1 基于Redis的原子性操作

核心代码(Go语言示例):

func CheckAndConsumeToken(redisClient *redis.Client, token string) bool {
    // Lua脚本:检查Token是否存在 -> 存在则删除 -> 返回1/0
    script := `
        local exist = redis.call("EXISTS", KEYS[1])
        if exist == 1 then
            redis.call("DEL", KEYS[1])
            return 1
        else
            return 0
        end
    `
    result, _ := redisClient.Eval(script, []string{token}).Int()
    return result == 1
}

2 请求处理流程图

客户端                         服务端                         Redis
  |                             |                             |
  |-- 1.请求Token ------------> |                             |
  |                             |-- 2.生成Token -------------> |
  |                             |   (SET key token EX 60 NX)  |
  | <-- 3.返回Token ----------- |                             |
  |                             |                             |
  |-- 4.携带Token执行业务 ----> |                             |
  |                             |-- 5.检查Token -------------> |
  |                             |   (Lua脚本原子操作)         |
  |                             | <-- 存在/不存在 ------------ |
  |                             |                             |
  | <-- 6.返回业务结果 ---------|                             |

3 异常处理策略

  • Token过期:返回400错误,引导客户端重新获取Token。
  • Redis宕机:降级为「本地内存缓存+限流」方案,或直接返回错误。
  • 重复请求:返回200为“请求已处理”,避免前端误判。

常见问题与FAQ

Q1:Token防重与数据库唯一索引有什么区别?

  • Token防重:在业务逻辑执行前拦截,适用于高频、短时重复请求。
  • 唯一索引:依赖数据库约束,适用于低频、数据层去重。
    两者常结合使用:Token防重挡住大部分重复,唯一索引兜底防止并发穿透。

Q2:如果Redis集群发生主从切换,Token丢失怎么办?

  • 设置Token过期时间(如60秒),主从切换期间的旧Token自动失效。
  • 对于关键业务(如支付),可引入数据库持久化Token表作为最终保障。

Q3:如何防止恶意用户大量申请Token耗尽系统资源?

  • 对Token生成接口做限流(如每秒100次/用户)。
  • 设置Token最小过期时间(如10秒),减少无效占用。
  • 使用布隆过滤器记录已发放Token,拒绝无效Token。

Q4:Token防重能完全保证幂等吗?

:不能,因为Token检查与业务执行并非原子操作(分布式环境中)。
解决方案

  • 采用「业务方执行 + 状态机」:业务执行后修改订单状态为「已支付」,下次Token即使通过也返回已支付。
  • 引入最终一致性校验:通过MQ补偿或对账系统兜底。

生产环境最佳实践

  1. Token绑定业务上下文

    每个Token需关联userId、bizType、timestamp,避免跨业务伪造。

  2. 差异化过期时间

    • 低频操作(如退款):Token过期时间设为10分钟。
    • 高频操作(如点赞):过期时间设为3秒。
  3. 降级与熔断

    当Redis响应超过200ms时,进入半降级状态,从100%请求中抽样10%依赖本地缓存。

  4. 监控指标

    • Token生成QPS、Token重复率、Redis缓存命中率。
    • 注意:Token重复率超过5%时,需排查前端重复请求或分布式环境问题。
  5. 测试要点

    • 压测场景:同时发送10个相同Token的请求,验证仅执行一次。
    • 极端场景:Redis突然断开,验证降级逻辑是否符合预期。

Token幂等防重是分布式系统的基础设施,但需警惕“过度设计”——在非关键业务中,直接使用数据库唯一索引可能更简单可靠,始终记住:防重不是目的,数据一致性才是

抱歉,评论功能暂时关闭!