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

PHP脚本并发执行时MySQL锁机制失效问题求助

解决MySQL行锁并发冲突的方案

你当前的锁机制存在竞态条件——两个脚本同时读取到锁值为0后,会同时执行后续操作,最终导致数据不一致。问题根源在于check和lock是两个独立的SQL操作,无法保证原子性。以下是几种可行的解决方案:

方案1:原子性UPDATE替代分离的查询+更新

把锁的检查与设置合并成一个原子操作,利用MySQL的行级锁特性,确保同一时间只有一个会话能成功获取锁。

function acquireLock($menu) {
    global $Db;
    // 仅当锁为0或已超时,才更新为锁定状态,整个操作是原子的
    $query = "UPDATE `lock` SET $menu='1', date=NOW() WHERE $menu='0' OR date < NOW() - INTERVAL 12 SECOND;";
    mysqli_query($Db, $query);
    // 受影响行数为1表示成功获取锁,0表示锁已被占用
    return mysqli_affected_rows($Db) === 1;
}

function releaseLock($menu) {
    global $Db;
    $query = "UPDATE `lock` SET $menu='0'";
    mysqli_query($Db, $query);
}

使用逻辑:调用acquireLock,返回true则执行业务逻辑,完成后调用releaseLock释放锁;返回false则等待或退出脚本。

方案2:使用MySQL内置的GET_LOCK()函数

MySQL提供的GET_LOCK()可以创建跨会话的命名锁,无需维护单独的lock表,实现更简洁。

function acquireLock($menu) {
    global $Db;
    // 生成唯一锁名,避免不同菜单锁冲突
    $lockName = "lock_{$menu}";
    // 尝试获取锁,超时时间设为12秒(和原有超时逻辑匹配)
    $result = mysqli_query($Db, "SELECT GET_LOCK('$lockName', 12);");
    $row = mysqli_fetch_row($result);
    mysqli_free_result($result);
    // 返回1表示成功获取锁,0表示超时,NULL表示出错
    return $row[0] === 1;
}

function releaseLock($menu) {
    global $Db;
    $lockName = "lock_{$menu}";
    mysqli_query($Db, "SELECT RELEASE_LOCK('$lockName');");
}

优势:如果脚本异常退出,MySQL会自动释放锁,避免死锁;无需维护lock表的结构和数据。

方案3:基于事务+行锁优化原表操作

如果必须保留现有lock表结构,可以通过事务结合排他锁实现原子操作:

function acquireLock($menu) {
    global $Db;
    mysqli_begin_transaction($Db);
    try {
        // 查询时锁定该行,防止其他会话修改
        $query = "SELECT $menu FROM `lock` FOR UPDATE;";
        mysqli_query($Db, $query);
        
        // 检查锁状态
        $checkQuery = "SELECT COUNT(*) AS c FROM `lock` WHERE $menu='1' AND date >= NOW() - INTERVAL 12 SECOND;";
        $lock = mysqli_query($Db, $checkQuery);
        $lockData = mysqli_fetch_object($lock);
        mysqli_free_result($lock);
        
        if ($lockData->c === 0) {
            // 更新为锁定状态
            $updateQuery = "UPDATE `lock` SET $menu='1', date=NOW();";
            mysqli_query($Db, $updateQuery);
            mysqli_commit($Db);
            return true;
        } else {
            mysqli_rollback($Db);
            return false;
        }
    } catch (Exception $e) {
        mysqli_rollback($Db);
        return false;
    }
}

原理:SELECT ... FOR UPDATE会给该行加排他锁,其他会话必须等待当前事务提交/回滚后才能操作,确保检查与更新的原子性。

额外注意事项

  • SQL注入风险:当前代码直接将$menu拼接进SQL,存在注入隐患,建议使用预处理语句,示例:
$stmt = mysqli_prepare($Db, "UPDATE `lock` SET ?='1', date=NOW() WHERE ?='0' OR date < NOW() - INTERVAL 12 SECOND;");
mysqli_stmt_bind_param($stmt, "ss", $menu, $menu);
mysqli_stmt_execute($stmt);
  • 死锁与超时:业务逻辑执行时间尽量缩短,避免长时间占用锁;设置合理的超时机制,防止脚本异常导致锁永久占用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 13:20:14