You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让PHP票务系统的票号分配机制更健壮?

解决票务系统重复票号问题

看起来你遇到了票务系统里典型的并发竞态条件问题——当批量购买时,循环里的多次调用(或者多用户同时操作)会同时读取可用票号列表:前一次选中的票号还没来得及写入数据库,后一次的freeNumbers查询依然会把这个票号标记为可用,最终导致重复插入。下面是具体的问题分析和解决方案:

一、先做兜底:数据库层面加唯一约束

无论应用层逻辑怎么优化,都要先给数据库加约束,从根源上阻止重复数据插入:
给transaction表的TicketNumber和DrawID添加联合唯一索引,这样即使应用层出现逻辑漏洞,数据库也会直接拒绝重复的票号插入请求。

执行这条SQL语句:

ALTER TABLE transaction ADD UNIQUE KEY unique_ticket_draw (TicketNumber, DrawID);

二、优化票号生成逻辑,解决竞态问题

方案1:数据库事务+行锁(推荐,适合所有场景)

不要在应用层获取全部可用票号,而是直接让数据库帮我们筛选未被占用的票号,同时加锁防止其他请求读取。这样能从数据库层面保证票号的唯一性。

重写你的randomTicket函数:

function randomTicket($drawID){
    $connect = new mysqli("localhost", "root", "", "dream");
    // 开启事务
    $connect->begin_transaction();
    try {
        $maxTickets = totalTickets($drawID);
        // 用SQL随机筛选未被占用的票号,同时加行锁(FOR UPDATE)
        $stmt = $connect->prepare("
            SELECT all_tickets.num AS TicketNumber
            FROM (
                -- 生成1到maxTickets的所有票号(如果MySQL版本≥8.0,可用递归CTE替代临时表)
                SELECT 1 AS num UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL 
                SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL 
                SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
                -- 如果票号范围更大,建议预先创建一个numbers表存储所有可能的数字
            ) AS all_tickets
            LEFT JOIN transaction t 
                ON t.TicketNumber = all_tickets.num AND t.DrawID = ?
            WHERE t.TicketNumber IS NULL AND all_tickets.num <= ?
            ORDER BY RAND()
            LIMIT 1
            FOR UPDATE
        ");
        $stmt->bind_param("ii", $drawID, $maxTickets);
        $stmt->execute();
        $result = $stmt->get_result();
        $row = $result->fetch_assoc();
        
        if (!$row) {
            throw new Exception("No available tickets left for this draw");
        }
        $ticketNumber = $row['TicketNumber'];
        $connect->commit();
        return $ticketNumber;
    } catch (Exception $e) {
        $connect->rollback();
        throw $e;
    }
}

注意:如果你的票号范围很大(比如超过1000),建议预先创建一个numbers表,存储1到最大可能的票号数字,这样SQL查询效率会更高。

方案2:应用层加全局锁(适合单服务器场景)

如果暂时不想修改数据库逻辑,可以在应用层加锁,确保同一时间只有一个请求能生成票号。比如用文件锁或者Redis锁:

function randomTicket($drawID){
    // 用文件锁做示例,生产环境分布式场景推荐用Redis锁
    $lockPath = "/tmp/ticket_lock_{$drawID}.lock";
    $lockFile = fopen($lockPath, "w+");
    
    // 获取排他锁,确保同一时间只有一个进程能进入逻辑
    if (flock($lockFile, LOCK_EX)) {
        $freeNumbers = freeNumbers($drawID);
        if (empty($freeNumbers)) {
            flock($lockFile, LOCK_UN);
            fclose($lockFile);
            throw new Exception("No available tickets left");
        }
        $randomIndex = array_rand($freeNumbers, 1);
        $ticketNumber = $freeNumbers[$randomIndex];
        
        // 释放锁
        flock($lockFile, LOCK_UN);
        fclose($lockFile);
        return $ticketNumber;
    } else {
        fclose($lockFile);
        // 锁获取失败,短暂重试
        usleep(100000); // 等待100毫秒
        return randomTicket($drawID);
    }
}

三、修改支付循环的事务逻辑

确保票号生成和插入数据库在同一个事务内完成,避免票号被选中后未插入就被其他请求抢走:

function paymentMade($paymentId, $token, $payerID, $drawID){
    $connect = new mysqli("localhost", "root", "", "dream");
    $connect->begin_transaction();
    try {
        // 在事务内生成票号,确保锁的有效性
        $ticketNumber = randomTicket($drawID);
        
        // 插入票务记录
        $stmt = $connect->prepare("INSERT INTO transaction (DrawID, TicketNumber, PaymentId, Token, PayerID) VALUES (?, ?, ?, ?, ?)");
        $stmt->bind_param("iisss", $drawID, $ticketNumber, $paymentId, $token, $payerID);
        $stmt->execute();
        
        $connect->commit();
        return "Success! Assigned ticket number: {$ticketNumber}";
    } catch (Exception $e) {
        $connect->rollback();
        return "Error: " . $e->getMessage();
    }
}

总结

最可靠的方案是数据库联合唯一约束+事务内锁查询,既保证了数据的一致性,又能高效处理并发场景。如果是单服务器部署,应用层锁也能临时解决问题,但分布式环境下还是推荐数据库层面的解决方案。

内容的提问来源于stack exchange,提问作者user9304280

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 08:42:29