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
相关产品推荐
相关产品推荐

