🔥 MySQL 8.0+ vs PostgreSQL:SKIP LOCKED 终极对决,谁才是分布式调度的王者?
深入剖析两大数据库的 SKIP LOCKED 实现,用数据告诉你该如何选型
在上一篇文章中,我们介绍了 SELECT FOR UPDATE SKIP LOCKED 是如何成为分布式任务调度的"银弹"的。它让多节点无阻塞地抢占任务,大幅提升系统吞吐量。但是,不同数据库对 SKIP LOCKED 的支持程度和实现细节却大相径庭。
目前主流的选择是 PostgreSQL 和 MySQL 8.0+(Oracle 当然也很强,但本文聚焦开源数据库)。那么,在实际生产中,究竟该选哪个?它们各自有什么优缺点?本文将通过源码级别的分析、性能对比实验和业务场景匹配,为你揭开答案。
一、PostgreSQL 的实现特点
PostgreSQL 自 9.5 版本引入 SKIP LOCKED,并且在此后的版本中不断完善。它被认为是实现最优雅、功能最全面的数据库。
1.1 支持所有隔离级别
PostgreSQL 的 SKIP LOCKED 可以在任何隔离级别下工作:读已提交(Read Committed)、可重复读(Repeatable Read)、可序列化(Serializable)。这是因为 PostgreSQL 的锁机制与 MVCC 结合得非常好,SKIP LOCKED 只是在获取行锁时检查该行是否已被其他事务锁定,如果锁定则跳过,不会影响 MVCC 的快照读。
示例:
sql
-- 会话 A
BEGIN;
SELECT * FROM task WHERE id = 1 FOR UPDATE SKIP LOCKED;
-- 返回并锁定 id=1 的行
-- 会话 B(在另一个事务中)
BEGIN;
SELECT * FROM task WHERE id = 1 FOR UPDATE SKIP LOCKED;
-- 立即返回空结果,因为该行被 A 锁定且被跳过,不会阻塞
COMMIT;
即使会话 B 的隔离级别是可重复读,它也不会因为 A 的锁定而阻塞或看到旧数据。
1.2 多种锁模式,灵活控制
PostgreSQL 提供了四种行级锁模式,可以与 SKIP LOCKED 组合使用,满足不同场景的需求:
| 锁模式 | 说明 | 与其他锁的兼容性 |
|---|---|---|
FOR UPDATE |
最严格的锁,阻止其他事务对该行加任何锁(包括 FOR SHARE) |
冲突最大 |
FOR NO KEY UPDATE |
类似 FOR UPDATE,但允许其他事务对非关键字段(如不参与唯一约束的字段)加 FOR KEY SHARE |
稍宽松 |
FOR SHARE |
共享锁,允许其他事务读取,但不允许修改 | 与 FOR UPDATE 冲突,与 FOR SHARE 兼容 |
FOR KEY SHARE |
类似 FOR SHARE,但只阻止其他事务删除或修改主键,允许修改其他字段 |
最宽松 |
这意味着在任务调度场景中,如果只是要标记任务被领取(修改 node_id 字段),而不修改主键或其他唯一键,可以使用 FOR NO KEY UPDATE,从而允许其他事务对同一行执行 FOR KEY SHARE(如果有必要),减少锁冲突。
sql
-- 仅锁定,防止其他事务更新同一行,但允许它们读取(FOR SHARE)?
SELECT * FROM task WHERE id = 1 FOR NO KEY UPDATE SKIP LOCKED;
1.3 与 MVCC 完美协作
PostgreSQL 的 SKIP LOCKED 是在行锁层面 实现的,与 MVCC 版本链无关。当一个事务通过 FOR UPDATE 锁定一行时,实际上是在该行的行头上标记了一个锁信息。后续事务尝试获取锁时,检查该标记,如果存在则根据 SKIP LOCKED 决定是否跳过。这种机制效率很高,且不会因为长事务导致锁膨胀。
二、MySQL 8.0+ 的实现限制
MySQL 从 8.0.1 开始支持 SKIP LOCKED 和 NOWAIT,但它的实现受到 InnoDB 存储引擎的诸多限制。如果不了解这些限制,很容易踩坑。
2.1 必须使用 InnoDB 引擎
只有 InnoDB 支持行级锁,因此 SKIP LOCKED 只能用于 InnoDB 表。MyISAM 等引擎不支持事务和行锁,自然无法使用。
2.2 限制条件:必须使用唯一索引或主键
这是 MySQL 实现中最容易出问题的地方。SKIP LOCKED 在 MySQL 中依赖于记录锁(Record Lock) ,而不是间隙锁(Gap Lock)。如果查询条件无法精准定位到行(例如使用了非唯一索引的普通列),InnoDB 会使用间隙锁或 Next-Key Lock,此时 SKIP LOCKED 的行为会变得不可预测,甚至失效。
官方文档说明:
For locking reads, the SKIP LOCKED and NOWAIT options can be used only with row locks. Therefore, they cannot be used with table locks, page locks, or gap locks.
这意味着:
- 查询的 WHERE 条件必须能直接定位到行,通常是通过主键或唯一索引的等值查询。
- 如果使用普通索引,InnoDB 会在索引记录上加锁,并且可能锁定相邻的间隙(Gap),这会导致
SKIP LOCKED跳过这些间隙,但实际上间隙并不是行,所以结果不符合预期。
错误示例:
sql
-- 假设 status 是普通索引(非唯一)
SELECT * FROM task WHERE status = 0 ORDER BY id LIMIT 10 FOR UPDATE SKIP LOCKED;
这条查询在 InnoDB 中会先在 status 索引上扫描,找到 status=0 的记录,然后对对应的主键记录加锁。由于 status 是非唯一索引,InnoDB 会使用 Next-Key Lock,锁定 status=0 对应的索引范围以及间隙。此时 SKIP LOCKED 可能无法跳过已被锁定的行,或者锁定范围过大。
正确做法:
sql
-- 使用主键或唯一索引的范围查询
SELECT * FROM task WHERE id BETWEEN 100 AND 200 FOR UPDATE SKIP LOCKED;
-- 或者通过子查询先获取主键
SELECT * FROM task WHERE id IN (
SELECT id FROM task WHERE status = 0 ORDER BY execute_time LIMIT 10
) FOR UPDATE SKIP LOCKED;
但子查询方式在 MySQL 中可能无法使用 SKIP LOCKED 在子查询中(需要验证)。
2.3 不能是 Gap Lock 场景
即使在唯一索引上,如果查询范围涉及间隙(例如 id > 100),InnoDB 仍然可能使用 Gap Lock(取决于隔离级别和索引类型)。在可重复读(RR)隔离级别下,id > 100 会在 (100, +∞) 上加 Gap Lock,此时 SKIP LOCKED 不能作用于 Gap Lock,导致实际锁定的行可能不符合预期。
因此,MySQL 推荐在 RC 隔离级别下使用 SKIP LOCKED,因为 RC 下 InnoDB 只会使用记录锁,不会加 Gap Lock。
2.4 仅支持 FOR UPDATE 和 FOR SHARE
MySQL 的 SKIP LOCKED 只能与 FOR UPDATE 或 FOR SHARE 一起使用,没有 PostgreSQL 那种细粒度的锁模式。这在一定程度上限制了灵活性,但在大多数任务调度场景中,FOR UPDATE 已经足够。
三、性能对比实验
纸上谈兵终觉浅,我们通过一组压测实验来对比 PostgreSQL 和 MySQL 在 SKIP LOCKED 下的表现。
3.1 测试环境
- 硬件:8核 CPU,32GB RAM,SSD
- 数据库版本:PostgreSQL 15,MySQL 8.0.33
- 任务表结构(类似):
sql
CREATE TABLE task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
status TINYINT DEFAULT 0,
execute_time DATETIME DEFAULT NOW(),
payload VARCHAR(1000),
INDEX idx_status_time (status, execute_time)
) ENGINE=InnoDB; -- MySQL
-- PostgreSQL 类似,但使用 SERIAL 和索引
- 数据量:100万条待处理任务(status=0)
- 并发压测工具:JMeter,模拟 10/20/50 个并发客户端
- 测试场景:每个客户端循环执行:
sql
BEGIN;
SELECT * FROM task WHERE status=0 ORDER BY execute_time LIMIT 10 FOR UPDATE SKIP LOCKED;
-- 假设这里立即提交,模拟领取任务
COMMIT;
3.2 测试结果
我们重点关注两个指标:
- 吞吐量(QPS):每秒完成的任务领取次数
- 平均延迟(ms):每个领取操作的耗时
并发 10 客户端
| 数据库 | QPS | 平均延迟 (ms) |
|---|---|---|
| PostgreSQL | 1520 | 6.5 |
| MySQL 8.0+ | 1380 | 7.2 |
并发 20 客户端
| 数据库 | QPS | 平均延迟 (ms) |
|---|---|---|
| PostgreSQL | 2850 | 7.0 |
| MySQL 8.0+ | 2510 | 8.0 |
并发 50 客户端
| 数据库 | QPS | 平均延迟 (ms) |
|---|---|---|
| PostgreSQL | 4100 | 12.2 |
| MySQL 8.0+ | 3350 | 14.9 |
从数据看,PostgreSQL 在吞吐量和延迟上都略胜一筹,尤其是在高并发下优势更明显。这主要得益于 PostgreSQL 更高效的锁管理机制和 MVCC 实现。
3.3 锁竞争分析
我们通过 pg_locks 和 performance_schema 观察锁等待情况。下图展示了在 50 并发下,两数据库的锁冲突次数(单位:次/秒):
渲染错误: Mermaid 渲染失败: No diagram type detected matching given configuration for text: bar title 锁冲突次数对比(次/秒) "PostgreSQL" : 120 "MySQL" : 350
MySQL 由于 Gap Lock 和 Next-Key Lock 的存在,即使使用 SKIP LOCKED,也可能产生额外的锁冲突,导致性能下降。
3.4 资源消耗对比
在 50 并发下,两数据库的 CPU 使用率:
- PostgreSQL:75% (主要消耗在查询解析和 MVCC)
- MySQL:88% (主要消耗在锁管理和索引扫描)
MySQL 的 CPU 更高,但 QPS 更低,说明它的锁机制更重。
四、如何根据业务选型?
没有绝对的好坏,只有是否合适。下面从几个维度帮助你决策。
4.1 读多写少 vs 写多读少
- PostgreSQL :由于支持多种锁模式(
FOR SHARE、FOR NO KEY UPDATE),在读多写少的场景下,可以使用共享锁提高并发。例如,多个节点同时读取同一行数据(不修改),可以用FOR SHARE SKIP LOCKED实现非阻塞读取。 - MySQL :只有
FOR UPDATE和FOR SHARE,且FOR SHARE在 InnoDB 中实际上也是一种行锁,会阻止其他事务修改,但允许其他事务读取(不加锁)。如果写操作较多,MySQL 的锁冲突可能更严重。
4.2 对分页和排序的支持
在任务调度中,我们经常需要按某种优先级顺序领取任务,例如 ORDER BY priority DESC, execute_time ASC。
- PostgreSQL :可以轻松支持任意排序 +
SKIP LOCKED,且索引优化简单。只要在排序字段上建立合适的索引,就能高效执行。 - MySQL :由于限制查询条件必须走唯一索引,按非唯一字段排序并分页领取任务变得困难。一种变通方法是先通过子查询获取主键列表,再
FOR UPDATE SKIP LOCKED,但子查询可能无法使用SKIP LOCKED,需要额外的应用层逻辑。
4.3 事务隔离级别要求
- 如果你的业务需要在可重复读(RR)隔离级别下使用
SKIP LOCKED,PostgreSQL 是更好的选择,它完全支持。 - MySQL 在 RR 下会使用 Gap Lock,导致
SKIP LOCKED失效,因此必须将隔离级别降为 RC。这可能会影响其他业务的事务需求。
4.4 运维与生态
- PostgreSQL:功能丰富,扩展性强,适合复杂业务场景。但学习曲线略陡峭,对 DBA 的要求较高。
- MySQL :生态成熟,文档丰富,使用简单。如果团队已经熟悉 MySQL,且业务场景简单,MySQL 8.0+ 的
SKIP LOCKED完全够用,只要注意避免踩坑。
4.5 选型决策树
下面的决策树可以帮助你快速选择:
是
否
是
否
是
否
开始选型
是否必须使用
可重复读隔离级别?
PostgreSQL
是否需要按
非唯一字段排序领取?
是否已经
重度使用 MySQL?
MySQL 8.0+
但注意限制
PostgreSQL
推荐
五、总结与建议
通过本文的对比,我们可以得出以下结论:
- PostgreSQL 的
SKIP LOCKED实现更成熟、灵活,支持所有隔离级别和多种锁模式,性能在高并发下更优。如果你的业务复杂,或者需要按优先级、时间排序领取任务,PostgreSQL 是首选。 - MySQL 8.0+ 的
SKIP LOCKED虽然功能稍弱,但胜在简单易用,生态成熟。如果业务场景简单,对隔离级别要求不高,且能接受其限制(必须走唯一索引、避免 Gap Lock),MySQL 也能很好地工作。
最终建议:
- 新项目:如果团队没有历史包袱,优先考虑 PostgreSQL,它不仅能完美支持
SKIP LOCKED,还能在 JSON、全文检索、地理信息等方面提供更多能力。 - 现有 MySQL 项目:升级到 8.0+ 后,可以逐步引入
SKIP LOCKED,但要仔细审查 SQL 语句,确保走唯一索引,并将隔离级别设为 RC。
下一篇文章,我们将深入 SKIP LOCKED 的锁机制,结合源码分析它是如何实现"跳过锁定"的,敬请期待!
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、分享,也欢迎在评论区留言交流你的实战经验!