PHP 送礼扣币并发处理方案
数据库事务方案(推荐)
// 使用事务+行锁保证并发安全
function sendGiftWithTransaction($userId, $giftId, $giftPrice) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$pdo->beginTransaction();
try {
// 使用悲观锁锁定用户行(SELECT ... FOR UPDATE)
$stmt = $pdo->prepare("SELECT balance FROM users WHERE id = ? FOR UPDATE");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user) {
throw new Exception('用户不存在');
}
if ($user['balance'] < $giftPrice) {
throw new Exception('余额不足');
}
// 扣减余额
$updateStmt = $pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?");
$updateStmt->execute([$giftPrice, $userId]);
// 记录送礼记录
$recordStmt = $pdo->prepare("INSERT INTO gift_logs (user_id, gift_id, price, created_at) VALUES (?, ?, ?, NOW())");
$recordStmt->execute([$userId, $giftId, $giftPrice]);
// 提交事务
$pdo->commit();
return ['success' => true, 'message' => '送礼成功'];
} catch (Exception $e) {
$pdo->rollBack();
return ['success' => false, 'message' => $e->getMessage()];
}
}
Redis Lua脚本方案(高性能)
// Redis Lua 脚本 - 原子操作
$luaScript = <<<LUA
local balance = redis.call('GET', KEYS[1]) -- 获取余额
if not balance then
return -1 -- 用户不存在
end
if tonumber(balance) < tonumber(ARGV[2]) then
return 0 -- 余额不足
end
redis.call('DECRBY', KEYS[1], ARGV[2]) -- 扣减余额
redis.call('LPUSH', KEYS[2], ARGV[3]) -- 记录日志
return 1 -- 成功
LUA;
// PHP调用
function sendGiftWithRedis($userId, $giftPrice, $logId) {
try {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$result = $redis->eval(
$luaScript,
[
"user:balance:{$userId}", // KEYS[1]
"user:logs:{$userId}", // KEYS[2]
null, // ARGV[1] (不需要)
$giftPrice, // ARGV[2]
json_encode([ // ARGV[3]
'user_id' => $userId,
'gift_id' => time(),
'price' => $giftPrice,
'timestamp' => time()
])
],
2 // 2个KEYS
);
if ($result == -1) {
return ['success' => false, 'message' => '用户不存在'];
} elseif ($result == 0) {
return ['success' => false, 'message' => '余额不足'];
}
return ['success' => true, 'message' => '送礼成功'];
} catch (Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
乐观锁方案(适合低并发)
function sendGiftWithOptimisticLock($userId, $giftPrice) {
$maxRetries = 3;
for ($i = 0; $i < $maxRetries; $i++) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
// 查询当前数据及版本号
$stmt = $pdo->prepare("SELECT balance, version FROM users WHERE id = ?");
$stmt->execute([$userId]);
$user = $stmt->fetch();
if (!$user || $user['balance'] < $giftPrice) {
return ['success' => false, 'message' => '余额不足或用户不存在'];
}
// 使用版本号更新
$updateStmt = $pdo->prepare(
"UPDATE users
SET balance = balance - ?, version = version + 1
WHERE id = ? AND version = ?"
);
$updateStmt->execute([$giftPrice, $userId, $user['version']]);
if ($updateStmt->rowCount() > 0) {
// 更新成功
$pdo->prepare("INSERT INTO gift_logs (user_id, gift_price) VALUES (?, ?)")
->execute([$userId, $giftPrice]);
return ['success' => true, 'message' => '送礼成功'];
}
// 更新失败,重试
usleep(100000); // 0.1秒
}
return ['success' => false, 'message' => '操作超时,请重试'];
}
消息队列方案(适合高并发)
// 生产者:将送礼请求放入队列
class GiftProducer {
public static function send($userId, $giftId, $price) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$data = [
'user_id' => $userId,
'gift_id' => $giftId,
'price' => $price,
'timestamp' => time()
];
return $redis->lpush('gift_queue', json_encode($data));
}
}
// 消费者:异步处理送礼逻辑
class GiftConsumer {
public function process() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 启动多进程处理
while (true) {
$message = $redis->brpop('gift_queue', 60);
if ($message) {
$data = json_decode($message[1], true);
$this->processGift($data);
}
}
}
private function processGift($data) {
// 使用事务处理
sendGiftWithTransaction($data['user_id'], $data['gift_id'], $data['price']);
}
}
综合优化方案
class GiftService {
// 1. 预扣减(Redis缓存)
public function prepareBalance($userId, $price) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 使用WATCH/MULTI/EXEC实现事务
$redis->multi();
$balance = $redis->get("user:balance:{$userId}");
if (!$balance || $balance < $price) {
return false;
}
$redis->decrby("user:balance:{$userId}", $price);
$redis->exec();
// 异步持久化到数据库
$this->asyncPersist($userId, $price);
return true;
}
// 2. 定时同步数据库
public function asyncPersist($userId, $price) {
// 记录到MQ,异步批量更新数据库
$this->sendToMQ('balance_persist', [
'user_id' => $userId,
'price' => $price
]);
}
// 3. 数据库批量更新
public function batchUpdateDB() {
$messages = $this->consumeMQ('balance_persist', 100);
if (!$messages) return;
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$pdo->beginTransaction();
try {
foreach ($messages as $msg) {
$pdo->prepare("UPDATE users SET balance = balance - ? WHERE id = ?")
->execute([$msg['price'], $msg['user_id']]);
}
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
}
}
}
性能优化建议
-
数据库优化:

- 使用InnoDB引擎(支持事务)
- 对user_id建立唯一索引
- 使用连接池
-
缓存策略:
- 热点用户使用Redis缓存
- 定时同步数据库
-
削峰限流:
- 使用消息队列异步处理
- 设置请求频率限制
-
监控告警:
- 监控余额一致性
- 监控故障恢复
选择建议
- 低并发:使用数据库事务方案
- 中高并发:使用Redis Lua脚本
- 高并发:使用消息队列+Redis
- 最终一致:使用Redis预扣减+异步同步
测试建议:使用如Apache JMeter等工具进行并发压测,确保系统在边界条件下能正确运行。