
在 mariadb 中,仅靠 serializable 隔离级别无法让普通 select 语句主动加锁并阻塞并发事务;需结合显式锁机制(如 get_lock)或 select ... for update 才能实现确定性的串行化执行。
在 mariadb 中,仅靠 serializable 隔离级别无法让普通 select 语句主动加锁并阻塞并发事务;需结合显式锁机制(如 get_lock)或 select ... for update 才能实现确定性的串行化执行。
在您的场景中,目标是确保两个并发事务严格串行执行:先执行 SELECT MAX(id) 获取当前最大 ID,休眠后插入 MAX(id) + 1,最终使 id 与 test 字段值保持一致。但问题在于——标准 SELECT MAX(id) 在 SERIALIZABLE 下不会自动加表级或行级锁,也不会阻塞其他事务的同类型查询。MariaDB 的 SERIALIZABLE 隔离级别虽会将普通 SELECT 转为 SELECT ... LOCK IN SHARE MODE(在某些引擎下),但对 MAX() 这类聚合查询的锁行为不可靠,尤其在 InnoDB 中,它通常只对扫描到的索引记录加间隙锁(Gap Lock),而无法保证后续 INSERT 不冲突,更无法强制第二个事务在 SELECT 处等待。
✅ 正确且推荐的解决方案是:使用 SELECT ... FOR UPDATE 显式加写锁(适用于 InnoDB 表),而非依赖隔离级别或应用层命名锁:
-- 替换原 SELECT MAX(id) 为带锁查询(需确保有索引支持) SELECT MAX(id) FROM test FOR UPDATE;
该语句会在 test 表的聚簇索引(或覆盖索引)上施加排他锁(X lock),阻塞其他事务对同一范围的 SELECT ... FOR UPDATE 或 INSERT/UPDATE/DELETE 操作,从而自然实现您期望的“U2 在 U1 的 SELECT 处等待”行为。
⚠️ 注意事项:
- 表必须使用 InnoDB 存储引擎(MyISAM 不支持行级锁,且 FOR UPDATE 无效);
- id 列应为主键或有唯一索引,否则 MAX(id) 可能触发全表扫描并升级为表锁,影响性能;
- FOR UPDATE 仅在事务内有效(即 autocommit = 0 下),且锁持续到 COMMIT 或 ROLLBACK;
- 若 test 表为空,MAX(id) 返回 NULL,需在 PHP 中做空值处理(如 COALESCE(MAX(id), 0))。
? 替代方案(不推荐但可行):使用 GET_LOCK() 应用级命名锁
如答案所提,可通过 DO GET_LOCK('test_table_serial', 30) 实现互斥,但属于应用层逻辑锁,存在以下缺陷:
- 锁名需全局唯一且易冲突(如多表操作需多个锁名);
- 锁不与事务绑定,若进程崩溃未释放,可能造成死锁(需依赖超时自动释放);
- 绕过数据库并发控制机制,降低可维护性与一致性保障。
✅ 推荐重构后的安全代码示例:
$pdo->query("SET autocommit = 0;");
try {
// 关键:使用 FOR UPDATE 强制加锁,确保串行读取
$max_id = $pdo->query("SELECT COALESCE(MAX(id), 0) AS max_id FROM test FOR UPDATE")->fetchColumn('max_id');
sleep(3);
$stmt = $pdo->prepare("INSERT INTO test(test) VALUES(:test)");
$stmt->execute(['test' => $max_id + 1]);
$pdo->query("COMMIT;");
} catch (Throwable $e) {
$pdo->query("ROLLBACK;");
throw $e;
}
? 总结:不要依赖 SERIALIZABLE 隔离级别来“让 SELECT 等待”,而应主动使用 SELECT ... FOR UPDATE 显式声明锁意图。这是 InnoDB 原生、原子、事务安全的解决方案,既满足业务逻辑一致性要求,又符合数据库最佳实践。











