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

MariaDB事务行读锁问题:ISOLATION级别与race condition排查

解决MariaDB事务竞态条件:避免多进程重复获取同一记录

嘿,这个竞态条件的坑我之前在处理多进程任务分发的时候踩过!本质问题在于你原来的事务里用的是普通SELECT,这属于快照读(在默认的REPEATABLE READ隔离级别下),不会对读取的行加锁,导致多个进程能同时读到同一条status=1的记录,最后都去更新它,自然就出现了重复获取的情况。

下面给你几个靠谱的解决方案,根据你的并发场景选就行:

1. 用SELECT ... FOR UPDATE加排他锁

这是最经典的解决方案,通过FOR UPDATE把普通查询改成当前读,会对选中的行加上排他锁,其他事务必须等当前事务提交/回滚后才能读取或修改这行数据,从根源上避免竞态。

修改后的事务伪代码:

START TRANSACTION;
-- 关键:用FOR UPDATE锁定符合条件的最旧行
SELECT id, status FROM your_table WHERE status = 1 ORDER BY created_at ASC LIMIT 1 FOR UPDATE;

-- 这里判断是否拿到了有效行(比如id不为空),如果有就执行更新
IF 拿到了行 THEN
    UPDATE your_table SET status = 2 WHERE id = [刚才查询到的id];
END IF;

COMMIT;

注意:如果你的表没有针对status和created_at建立索引,FOR UPDATE可能会锁全表,严重影响性能。建议创建复合索引:

CREATE INDEX idx_status_created ON your_table(status, created_at);

2. 用SELECT ... FOR SKIP LOCKED实现无阻塞并发(MariaDB 10.6+支持)

如果你的并发量很高,不想让进程因为等待锁而阻塞,可以用FOR SKIP LOCKED。这个语法会直接跳过已经被其他事务锁定的行,返回下一个符合条件的未锁定行,多个进程可以同时处理不同的记录,效率更高。

伪代码示例:

START TRANSACTION;
-- 跳过已锁定的行,直接取可用的最旧记录
SELECT id, status FROM your_table WHERE status = 1 ORDER BY created_at ASC LIMIT 1 FOR SKIP LOCKED;

IF 拿到了行 THEN
    UPDATE your_table SET status = 2 WHERE id = [查询到的id];
END IF;

COMMIT;

这个方案适合高并发的任务分发场景,不会出现进程排队等待的情况。

3. 合并读和更新为原子操作:UPDATE ... LIMIT

如果你的业务不需要先读取记录内容再做判断,直接把“查找最旧行+更新状态”合并成一个UPDATE语句更简单,因为UPDATE本身是原子操作,数据库会自动锁定目标行,避免竞态。

示例代码:

-- 原子性更新:找到status=1的最旧行,直接改成2
UPDATE your_table SET status = 2 WHERE status = 1 ORDER BY created_at ASC LIMIT 1;

-- 如果需要获取刚才更新的记录信息,可以用ROW_COUNT()判断是否更新成功,再查询
IF ROW_COUNT() > 0 THEN
    SELECT id, status FROM your_table WHERE status = 2 ORDER BY updated_at DESC LIMIT 1;
END IF;

这个方案不需要显式开启事务(当然也可以放在事务里),代码更简洁,原子性也有保障。

最后再提个关键点

不管用哪种方案,都要确保你的隔离级别不是READ UNCOMMITTED(虽然一般不会用这个),另外,一定要测试并发场景,比如用多个进程同时执行,验证是否还会出现重复获取的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 08:27:33