在分布式系统里,网络超时和客户端重试常常让同一个业务请求被送达多次。如果接口没有做幂等处理,像下单、扣款、发送通知这类操作就可能重复生效,造成资损或脏数据。Redis由于具备原子命令和高性能,非常适合用来在网关或业务层做幂等拦截。下面从几个实际可用的方案出发,详细说明如何落地。

基于Token的一次性消费方案
最常见的做法是前端在调用业务接口前,先向服务端申请一个唯一Token,服务端把Token写入Redis并设置较短过期时间。真正提交业务时,客户端带上该Token,后端使用Redis的SETNX或SET key value NX命令尝试写入,如果写入成功说明是首次请求,失败后则直接返回重复提交提示。这种方案能有效避免用户重复点击或页面重试带来的问题。
该方案的核心在于Token的生命周期控制。如果过期时间太短,用户填表时间较长可能导致Token失效;太长又会被滥刷。通常可结合业务形态设为五到十分钟。另外,Token生成必须使用足够随机的算法,例如UUID配合时间戳,防止被猜测。下面是一个Spring Boot风格的拦截示例代码:
// 申请Token
public String createToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idemp:" + token, "1", 10, TimeUnit.MINUTES);
return token;
}
// 校验并消费Token
public boolean checkToken(String token) {
String key = "idemp:" + token;
// 使用SETNX语义,仅当key不存在时设置成功
Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "used", 10, TimeUnit.MINUTES);
return success != null && success;
}
从优缺点来看,Token方案对前端配合度有要求,但逻辑清晰、侵入性低。它更适合表单类提交,不太适合纯服务端之间的异步调用。在集群部署时,只要Redis是共享的,多台机器也能正确判重。
基于请求唯一标识的防重方案
当接口是开放给第三方或内部服务调用时,往往无法让对方先拿Token。此时可以让调用方在请求头或参数中携带一个全局唯一标识,例如订单号、requestId。服务端以该标识为Key写入Redis,利用SETNX保证只有第一次能写进去,后续相同标识直接拒绝或返回首次结果。
这种方案要求调用方必须保证标识唯一且不变,否则可能误拦或漏拦。为了不让Redis数据无限增长,一定要给Key设置合理的过期时间,比如二十四小时,覆盖正常重试窗口即可。下面是一段基于Redis命令直接操作的Node.js示例:
const redis = require('redis');
const client = redis.createClient();
async function idempCheck(requestId) {
const key = 'req:' + requestId;
// NX表示不存在才设置,EX设置过期秒数
const result = await client.set(key, '1', 'NX', 'EX', 86400);
if (result === 'OK') {
return true; // 首次请求
}
return false; // 重复请求
}
与Token方案相比,请求唯一标识更适用于系统间通信,不需要额外交互 round trip。但如果调用方发来的标识重复率过高或碰撞,就会影响正常业务。实践中通常会在网关层统一生成或校验requestId,再向后传递。
基于分布式锁的并发控制方案
有些场景不是单纯防重,而是要控制同一资源同时只能被一个请求处理,例如库存扣减。这时可以用Redis分布式锁,以资源ID为锁Key,使用SET key value NX PX加锁,处理完再释放。它既能挡住重复请求,也能解决并发竞争。
分布式锁要特别注意死锁和误删问题。锁值应使用唯一串,释放时必须用Lua脚本校验值一致才删,不能简单DEL。另外要设置自动过期防止宕机不释放。下面是Python示例:
import redis
import uuid
import time
r = redis.Redis(host='127.0.0.1', port=6379)
def lock(resource_id):
val = str(uuid.uuid4())
ok = r.set('lock:' + resource_id, val, nx=True, px=5000)
return val if ok else None
def unlock(resource_id, val):
lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
r.eval(lua, 1, 'lock:' + resource_id, val)
rid = 'stock_1001'
v = lock(rid)
if v:
try:
# 处理扣减逻辑
pass
finally:
unlock(rid, v)
分布式锁比前两种方案更重,但在需要严格串行化的场景不可替代。选择时应当根据业务是防重还是防并发来决定。三种方案都能基于Redis轻松实现接口幂等性,关键是理清请求模型和重试边界。