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等工具进行并发压测,确保系统在边界条件下能正确运行。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播