如何在PHP构建的RestAPI中避免重复请求引发的订单号重复问题
纯PHP侧解决订单重复、接口限流的实现思路
核心问题本质
你遇到的毫秒级并发下的重复问题,根源是所有“先查询状态、再执行业务、最后写入状态标记”的逻辑都是非原子的:多PHP进程的内存相互隔离,哪怕你在第一个请求里刚写入“这个IP已经请求过了”的标记,其他同时发起的并行请求在同一时间节点的查询操作,根本读不到还没提交完成的写入结果,最终所有进程都判定“没有同类请求”,直接往下执行业务,生成重复订单。
所有没有依托操作系统/存储层原子操作实现的校验逻辑,在数毫秒间隔的并发下完全失效。
订单号生成的无重复实现
要在纯PHP层杜绝重复order_number,不能依赖普通的查询自增逻辑,必须用跨进程生效的原子互斥锁:
- 优先使用PHP原生文件锁实现,不需要额外中间件依赖:通过
flock()函数的内核级原子锁机制,保证同一时间只有一个进程能进入订单生成的临界区
参考实现代码:function place_order($data) { // 打开固定的锁文件,c模式代表文件不存在就创建,存在就打开 $lockFp = fopen('/tmp/order_create_global.lock', 'c'); // 非阻塞获取独占锁:LOCK_EX=独占锁,LOCK_NB=不等待锁占用,拿不到就立刻返回 if (!flock($lockFp, LOCK_EX | LOCK_NB)) { http_response_code(429); exit('当前请求繁忙,请稍后重试'); } // 拿到锁的进程进入临界区,完整执行订单生成逻辑 $order_number = $db->getOne('SELECT MAX(order_number) FROM orders'); $order_number++; try { $db->query('INSERT INTO orders(order_number, data) VALUES(?,?)', [$order_number, $data]); } finally { // 无论执行成功还是抛错,都主动释放锁 flock($lockFp, LOCK_UN); fclose($lockFp); } } - 如果服务部署了Redis,也可以用Redis的原子指令实现锁:用
SET 锁key 随机值 NX EX 过期时间,其中NX语义是只有key不存在的时候才会设置成功,是原子操作,跨进程生效,不会出现多个进程同时拿锁的问题
注意:必须直接用带NX参数的SET指令,不能拆成“先查key是否存在、不存在再设置”两步,否则和最初的订单号自增逻辑有一样的竞态问题
同IP短时间重复请求拦截
同样基于原子锁的逻辑实现限流,完全可以覆盖毫秒级的并行请求场景:
- 针对每个客户端IP生成对应的锁key,比如
ip_limit_123.123.123.123 - 尝试用和上面一致的原子操作(文件锁/Redis原子锁)获取这个IP对应的锁,锁的过期时间直接设置为你需要的限流窗口(比如3秒)
- 第一个发起请求的进程会成功拿到锁,后续同IP的所有并行请求在同一毫秒发起时,会立刻检测到锁已被占用,直接返回请求频繁的提示,根本不需要走到业务逻辑层
- 这个逻辑的响应延迟是微秒级的,完全不会被毫秒级的并行请求穿透
兜底机制
哪怕PHP层的锁逻辑做的再完善,也一定要给数据库的order_number字段加UNIQUE唯一索引:
- 这是最后一道防线,比如遇到进程崩溃没来得及释放锁、极端场景锁失效的情况,数据库的唯一约束会直接拦截重复值的写入,不会产生脏数据
- 配合这个约束,PHP层在收到数据库抛出的重复键报错时,可以自动重试一次订单创建流程,不需要人工介入修复
内容的提问来源于stack exchange,提问作者Frodik
相关产品推荐
相关产品推荐

