PHP 送礼扣币并发处理

wen PHP项目 2

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();
        }
    }
}

性能优化建议

  1. 数据库优化

    PHP 送礼扣币并发处理

    • 使用InnoDB引擎(支持事务)
    • 对user_id建立唯一索引
    • 使用连接池
  2. 缓存策略

    • 热点用户使用Redis缓存
    • 定时同步数据库
  3. 削峰限流

    • 使用消息队列异步处理
    • 设置请求频率限制
  4. 监控告警

    • 监控余额一致性
    • 监控故障恢复

选择建议

  • 低并发:使用数据库事务方案
  • 中高并发:使用Redis Lua脚本
  • 高并发:使用消息队列+Redis
  • 最终一致:使用Redis预扣减+异步同步

测试建议:使用如Apache JMeter等工具进行并发压测,确保系统在边界条件下能正确运行。

抱歉,评论功能暂时关闭!