一、问题场景
在跨境电商订单系统中,幂等性是一个必须解决的问题。用户可能因为网络超时、页面刷新、误操作等原因多次点击“提交订单”,导致同一笔订单被重复创建。
我遇到过最严重的一次:用户提交订单时网络超时,前端重试了3次,结果系统创建了3笔一模一样的订单,扣了3次款。用户投诉、客服介入、财务对账,折腾了整整一周。
二、幂等性的核心原理
幂等性(Idempotence)指的是:同一个操作执行一次和执行多次的效果是一样的。在订单系统中,这意味着即使用户多次提交同一个订单,系统也只应该创建一笔订单、扣一次款。
三、方案一:基于数据库唯一索引
最简单粗暴的方案——在订单表的order_no字段上建立唯一索引:
sql
CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) UNIQUE NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2), status TINYINT, created_at DATETIME);
生成订单号时,用“用户ID + 商品ID + 时间戳”的Hash作为订单号。如果两次提交生成的是同一个订单号,数据库会报唯一索引冲突,第二次插入失败。
但这个方法有个问题——订单号生成逻辑必须保证相同请求生成相同订单号,这需要客户端配合传递一个幂等令牌。
四、方案二:幂等令牌(Idempotent Token)
这是目前最主流的方案:
python
import redisimport uuidclass IdempotentService: def init(self): self.redis = redis.Redis(decode_responses=True) def generate_token(self, user_id): """生成幂等令牌""" token = str(uuid.uuid4()) key = f"idempotent:{user_id}:{token}" # 存储令牌,有效期5分钟 self.redis.setex(key, 300, "pending") return token def process_order(self, user_id, token, order_data): key = f"idempotent:{user_id}:{token}" # 使用Lua脚本保证原子性 script = """ local key = KEYS[1] local status = redis.call('GET', key) if status == 'pending' then -- 第一次请求:标记为processing,继续处理 redis.call('SET', key, 'processing') return 'processing' elseif status == 'processing' then -- 重复请求:返回已有结果 local result = redis.call('GET', key .. ':result') return result or 'processing' else return 'expired' end """ result = self.redis.eval(script, 1, key) if result == 'processing': # 执行下单逻辑 order_result = self._create_order(order_data) # 存储结果,标记为done self.redis.setex(key, 300, "done") self.redis.setex(key + ":result", 300, json.dumps(order_result)) return order_result elif result == 'expired': raise Exception("幂等令牌已过期,请重新提交") else: # 重复请求,返回之前的结果 return json.loads(result)
前端流程:
1.
用户进入下单页时,调用generate_token()获取一个令牌
2.
3.
提交订单时,把令牌一起传给后端
4.
5.
后端根据令牌判断是首次请求还是重复请求
6.
五、踩坑:令牌过期与补偿
如果用户拿到了令牌但5分钟内没有提交(比如去凑单了),令牌过期后再次提交会被判定为“新请求”,可能会创建两笔订单。
解决方案是:订单创建成功后,用订单号本身作为幂等键:
python
def create_order_with_idempotent(order_data): # 用订单号作为幂等键 order_no = generate_order_no(order_data) # 尝试插入订单,如果order_no重复则报错 try: order = Order.create( order_no=order_no, user_id=order_data['user_id'], amount=order_data['amount'] ) return order except IntegrityError: # 订单已存在,查询返回 return Order.get(Order.order_no == order_no)
六、分布式环境下的幂等性
在微服务架构中,订单创建涉及多个服务(订单服务、库存服务、支付服务),每个服务都需要保证幂等性:
text
用户请求 → API网关(生成幂等令牌) → 订单服务(令牌校验 + 创建订单) → 库存服务(基于订单号幂等扣减) → 支付服务(基于订单号幂等扣款)
库存服务的幂等扣减:
python
def deduct_stock_idempotent(order_no, product_id, quantity): key = f"stock_deduct:{order_no}" # 如果已经扣过,直接返回成功 if redis.exists(key): return True # 扣减库存 result = deduct_stock(product_id, quantity) if result: redis.setex(key, 86400, "done") # 记录已扣减 return result
七、总结
幂等性设计没有银弹,核心原则就三条:唯一标识 + 状态记录 + 原子操作。不管是用数据库唯一索引、Redis令牌还是分布式锁,只要能保证这三个要素,就能解决重复提交的问题。