防并发刷的核心是数据库事务+行级锁(SELECT ... FOR UPDATE),确保用户、订单、奖池操作的原子性;充值需幂等回调并用唯一索引防重;抽奖须按奖池或用户+活动维度精准加锁;所有关键更新必须在事务中完成,且PHP异常时及时rollback。

PHP游戏网站做充值和发奖时,防并发刷的核心是用数据库事务 + 行级锁(SELECT ... FOR UPDATE),确保同一用户、同一订单、同一奖池在多请求下不会重复处理。光靠 PHP 层判断或简单更新是扛不住秒杀或脚本刷的。
充值成功后扣余额/发虚拟币:加事务+行锁
用户充值回调(如支付平台异步通知)必须幂等且原子。不能先查余额再更新,否则并发时可能超发。
- 把用户账户表设计成有
user_id主键,余额字段如gold、coin - 在事务中用
SELECT ... FOR UPDATE锁住该用户行,再更新:
START TRANSACTION;
SELECT gold FROM user_account WHERE user_id = 123 FOR UPDATE;
-- 检查是否已处理过该订单(用订单号去 trade_log 表查,也建议加唯一索引)
INSERT INTO trade_log (order_no, user_id, amount, status) VALUES ('ORD123', 123, 100, 'success') ON DUPLICATE KEY UPDATE status = status;
UPDATE user_account SET gold = gold + 100 WHERE user_id = 123;
COMMIT;注意:trade_log.order_no 必须建唯一索引,防止重复插入;FOR UPDATE 只在事务里生效,且要求引擎为 InnoDB。
抽奖/发奖逻辑:锁奖池或用户+活动维度
如果是固定奖池(比如“今日限量100个iPhone”),不能只锁用户,要锁奖池记录本身;如果是每人限1次,则锁用户+活动ID组合。
立即学习“PHP免费学习笔记(深入)”;
- 建奖池表
prize_pool:id、activity_id、prize_type、total_count、used_count - 抽奖时先锁住对应 activity_id 的池子行:
START TRANSACTION; SELECT used_count, total_count FROM prize_pool WHERE activity_id = 456 AND prize_type = 'iphone' FOR UPDATE; <p>-- PHP 中判断 used_count < total_count UPDATE prize_pool SET used_count = used_count + 1 WHERE id = 789; INSERT INTO user_prize (user_id, activity_id, prize_type, created_at) VALUES (123, 456, 'iphone', NOW()); COMMIT;
如果奖池按用户限制(如“每人每天抽1次”),就建联合唯一索引 (user_id, activity_id, date),并在事务开头用 SELECT ... FOR UPDATE 锁这行(可先 upsert 占位再查,或用 INSERT ... SELECT ... FOR UPDATE 组合)。
关键细节别漏
- 所有涉及金额、库存、次数的更新,必须走事务,哪怕只是单条 UPDATE —— 因为要配合前面的 SELECT FOR UPDATE
- 锁的粒度要准:不要锁全表,也不要锁错行(比如用 nickname 锁不如用 user_id,后者有主键索引才生效)
- PHP 代码里 catch 异常后必须 rollback,否则连接挂着锁不放,会拖垮数据库
- 避免长事务:网络请求、日志写入、Redis 操作等不要放在事务里,只做 DB 读写
- 前端防重不是防并发:按钮置灰、时间戳、token 都只能防手抖,挡不住 curl 脚本,真正防线在 DB 层
补充建议:用 Redis 做轻量预检(非替代 DB 锁)
可在事务前加一层 Redis 分布式锁或原子计数器,快速拦截明显异常流量(如1秒内同一用户提交5次充值回调),但不能跳过 DB 事务和行锁。例如:
- 用
SET order_no:ORD123 EX 60 NX标记订单已进入处理流程 - 如果 set 成功,继续走 DB 事务;失败则直接返回“处理中”
- 事务成功后删 Redis key;失败或异常时由定时任务兜底清理
这只是锦上添花,核心防线永远在 MySQL 的事务隔离与行锁机制上。



















