高并发下,网络延迟时,前端用户快速点了2下,订单重复提交了
你会怎么处理?
加锁?
加队列?
前端增加防抖处理?
今天我们来运用幂等性来处理。
一、核心概念:什么是幂等性?
幂等性:同一个请求执行 1 次和执行 N 次,最终结果完全一致,不会产生副作用。
在下单场景中:
- 正常:用户点击 1 次「提交订单」→ 生成 1 笔订单
- 异常:网络卡顿 / 前端 BUG / 误触 → 重复发送 N 次下单请求
- 幂等目标:无论请求重复多少次,只创建 1 笔订单,杜绝重复支付、超卖
二、重复下单的核心场景
- 前端误触:用户连续点击下单按钮
- 网络重试:请求超时,前端 / 网关自动重试
- 接口重放:恶意用户抓取请求包重复提交
- 后端延迟:下单逻辑耗时,请求未完成就收到新请求
三、幂等性解决重复下单的标准方案
最优方案:全局唯一幂等号 + 分布式锁
流程:
- 进入下单页时,前端请求后端生成全局唯一幂等号(order_token)
- 前端提交订单时,必须携带这个
order_token - 后端通过分布式锁锁定幂等号,保证请求串行执行
- 执行下单逻辑后,持久化幂等号状态,后续重复请求直接拦截
技术选型
- 幂等号生成:UUID / 雪花算法
- 分布式锁:Redis(高并发、高性能,最适合电商场景)
- 存储:MySQL(记录幂等号与订单的绑定关系)
四、完整 PHP 实现代码
环境依赖
- PHP 7.4+
- Redis 扩展(
phpredis) - MySQL 5.7+
步骤 1:数据库表设计
1. 订单表(核心业务表)
CREATE TABLE `orders` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_sn` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态 0-待支付 1-已支付',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_sn` (`order_sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
2. 幂等记录表(保证幂等性)
CREATE TABLE `idempotent_order` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`order_token` varchar(64) NOT NULL COMMENT '全局幂等号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`order_sn` varchar(32) DEFAULT NULL COMMENT '关联订单号',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0-处理中 1-处理成功 2-处理失败',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_order_token` (`order_token`) COMMENT **【关键】唯一索引,强制幂等**
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单幂等记录表';
✅ 核心关键点:order_token 加唯一索引,数据库层面兜底防重复。
步骤 2:Redis 分布式锁工具类
<?php
/**
* Redis分布式锁:保证下单请求串行执行,解决并发重复下单
*/
class RedisLock
{
private $redis;
private $lockKey;
private $lockTimeout = 5; // 锁超时时间(秒),防止死锁
public function __construct($host = '127.0.0.1', $port = 6379, $password = null)
{
$this->redis = new Redis();
$this->redis->connect($host, $port);
if ($password) {
$this->redis->auth($password);
}
}
/**
* 获取锁
* @param string $key 锁键
* @param string $requestId 唯一请求ID(防止误删他人锁)
* @return bool
*/
public function lock($key, $requestId)
{
$this->lockKey = $key;
// SET命令原子性:不存在则设置,同时设置过期时间
return $this->redis->set($key, $requestId, ['nx', 'ex' => $this->lockTimeout]);
}
/**
* 释放锁(Lua脚本保证原子性,避免误解锁)
* @param string $requestId
* @return bool
*/
public function unlock($requestId)
{
$lua = "
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
return $this->redis->eval($lua, [$this->lockKey, $requestId], 1);
}
}
步骤 3:生成全局幂等号接口
前端进入下单页时调用,获取唯一的 order_token
<?php
/**
* 生成订单幂等号(前端进入下单页调用)
*/
function generateOrderToken()
{
header('Content-Type: application/json');
$userId = $_POST['user_id'] ?? 0;
if (!$userId) {
echo json_encode(['code' => 400, 'msg' => '用户ID不能为空']);
exit;
}
// 生成全局唯一幂等号:时间戳+用户ID+随机字符串
$orderToken = md5(uniqid(mt_rand(), true) . $userId . time());
// 返回给前端,必须携带此token提交订单
echo json_encode([
'code' => 200,
'data' => ['order_token' => $orderToken],
'msg' => '获取成功'
]);
}
// 执行接口
generateOrderToken();
步骤 4:核心:幂等性下单接口(PHP 完整实现)
<?php
/**
* 幂等性下单接口(核心代码)
*/
class OrderController
{
private $pdo;
private $redisLock;
public function __construct()
{
// 初始化MySQL连接
$this->pdo = new PDO(
"mysql:host=127.0.0.1;dbname=test;charset=utf8mb4",
'root',
'your_password'
);
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 初始化Redis锁
$this->redisLock = new RedisLock();
}
/**
* 下单主方法
*/
public function createOrder()
{
header('Content-Type: application/json');
// 1. 获取请求参数
$userId = $_POST['user_id'] ?? 0;
$orderToken = $_POST['order_token'] ?? ''; // 幂等号(必传)
$totalAmount = $_POST['total_amount'] ?? 0;
// 2. 基础参数校验
if (!$userId || !$orderToken || $totalAmount <= 0) {
$this->returnJson(400, '参数错误');
}
// 3. 定义锁标识:每个用户+幂等号一把锁
$lockKey = "order:lock:{$orderToken}";
$requestId = uniqid(); // 唯一请求ID,防止误解锁
try {
// ====================== 幂等核心1:获取分布式锁 ======================
if (!$this->redisLock->lock($lockKey, $requestId)) {
$this->returnJson(429, '请求过于频繁,请稍后再试');
}
// ====================== 幂等核心2:查询幂等记录 ======================
$idempotent = $this->getIdempotentRecord($orderToken);
if ($idempotent) {
// 已处理:直接返回结果
if ($idempotent['status'] == 1) {
$this->returnJson(200, '订单创建成功', ['order_sn' => $idempotent['order_sn']]);
}
// 处理中/失败:拦截重复请求
$this->returnJson(400, '订单正在处理中,请勿重复提交');
}
// ====================== 幂等核心3:插入幂等记录(预处理) ======================
$this->insertIdempotentRecord($orderToken, $userId);
// ====================== 4. 执行业务:创建订单 ======================
$orderSn = $this->generateOrderSn(); // 生成订单号
$this->insertOrder($orderSn, $userId, $totalAmount);
// ====================== 幂等核心4:更新幂等记录为成功 ======================
$this->updateIdempotentStatus($orderToken, 1, $orderSn);
// 5. 返回成功
$this->returnJson(200, '订单创建成功', ['order_sn' => $orderSn]);
} catch (PDOException $e) {
// 异常:更新幂等记录为失败
$this->updateIdempotentStatus($orderToken, 2);
$this->returnJson(500, '订单创建失败:' . $e->getMessage());
} finally {
// 无论成功失败,释放锁
$this->redisLock->unlock($requestId);
}
}
// -------------------------- 工具方法 --------------------------
// 查询幂等记录
private function getIdempotentRecord($orderToken)
{
$stmt = $this->pdo->prepare("SELECT * FROM idempotent_order WHERE order_token = ?");
$stmt->execute([$orderToken]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// 插入幂等记录(处理中)
private function insertIdempotentRecord($orderToken, $userId)
{
$stmt = $this->pdo->prepare("INSERT INTO idempotent_order (order_token, user_id, status) VALUES (?, ?, 0)");
$stmt->execute([$orderToken, $userId]);
}
// 更新幂等状态
private function updateIdempotentStatus($orderToken, $status, $orderSn = null)
{
if ($orderSn) {
$stmt = $this->pdo->prepare("UPDATE idempotent_order SET status = ?, order_sn = ? WHERE order_token = ?");
$stmt->execute([$status, $orderSn, $orderToken]);
} else {
$stmt = $this->pdo->prepare("UPDATE idempotent_order SET status = ? WHERE order_token = ?");
$stmt->execute([$status, $orderToken]);
}
}
// 生成订单号
private function generateOrderSn()
{
return date('YmdHis') . mt_rand(100000, 999999);
}
// 插入订单
private function insertOrder($orderSn, $userId, $totalAmount)
{
$stmt = $this->pdo->prepare("INSERT INTO orders (order_sn, user_id, total_amount) VALUES (?, ?, ?)");
$stmt->execute([$orderSn, $userId, $totalAmount]);
}
// JSON输出
private function returnJson($code, $msg, $data = [])
{
echo json_encode(compact('code', 'msg', 'data'));
exit;
}
}
// 执行下单接口
(new OrderController())->createOrder();
五、深度剖析:为什么这套方案能 100% 解决重复下单?
1. 三重防重复机制(层层兜底)
表格
| 层级 | 实现方式 | 作用 |
|---|---|---|
| 前端层 | 按钮点击后禁用、防抖 | 拦截用户误触 |
| 应用层 | Redis 分布式锁 | 拦截并发重复请求,保证串行执行 |
| 数据库层 | 幂等号唯一索引 | 终极兜底,即使锁失效,也无法插入重复记录 |

评论 (0)