欢迎光临
我们一直在努力

别急着上 Kafka!MySQL 8.0 的 SKIP LOCKED 才是轻量级队列的神器

在这里插入图片描述
你的系统需要处理一些异步任务,比如“用户注册后发送邮件”或“生成 PDF 报表”。

  • 方案 A (Redis List): 速度快,但消息容易丢,且难以保证“事务原子性”(比如数据库插入成功了,但 Redis 推送失败了)。
  • 方案 B (RabbitMQ/Kafka): 功能强大,但运维太重。你需要处理 ACK 机制、消息回溯、以及复杂的分布式事务(最终一致性)。
  • 方案 C (数据库表): 最简单的做法。插入一条记录状态为 PENDING,开启多个线程去扫描并处理。

痛点: 传统的数据库表方案(方案 C)在多线程并发时会死锁或排队等待。
救星: MySQL 8.0 的 SELECT … FOR UPDATE SKIP LOCKED。它允许多个消费者并发读取队列,互不阻塞,自动跳过被锁定的行。


1. 核心痛点:传统 DB 队列为什么慢?

假设我们有一个任务表 jobs。

传统写法 (悲观锁):

START TRANSACTION;
— 1. 选取一个待处理的任务并锁住
SELECT * FROM jobs WHERE status = 'PENDING' LIMIT 1 FOR UPDATE;
— 2. 处理业务…
— 3. 更新状态
UPDATE jobs SET status = 'DONE' WHERE id = ?;
COMMIT;

问题所在:
当线程 A 执行 SELECT … FOR UPDATE 锁住了第一行时。
线程 B 此时也来查询,它会被阻塞(Block),直到线程 A 提交事务释放锁。
这意味着,无论你启动多少个消费者线程,它们实际上是串行执行的,吞吐量极低,且容易造成数据库连接池耗尽。


2. 核心原理:SKIP LOCKED 的魔法

SKIP LOCKED 告诉 MySQL 优化器:“如果在扫描过程中遇到了已经被其他事务锁住的行,不要等待,直接跳过,去找下一行。”

新写法 (高并发):

START TRANSACTION;
— 核心魔法:SKIP LOCKED
SELECT * FROM jobs
WHERE status = 'PENDING'
ORDER BY id
LIMIT 1
FOR UPDATE SKIP LOCKED;

— 如果查到了结果:
— 1. 在内存中处理业务
— 2. UPDATE jobs SET status = 'DONE' WHERE id = ?;

COMMIT;

效果:

  • 线程 A 锁住了 ID=1。
  • 线程 B 来查询,发现 ID=1 被锁,立马跳过,锁住了 ID=2。
  • 线程 C 来查询,发现 1、2 被锁,立马跳过,锁住了 ID=3。

所有线程并行工作,没有任何等待开销!


3. 实战场景:什么时候用它?

SKIP LOCKED 并不是要替代 Kafka,但在以下场景中,它是完美的替代品:

场景一:事务强一致性任务 (Transactional Outbox)

这是最经典的应用。
需求: 用户下单成功后,必须发积分。
痛点: 如果“订单入库”和“发消息到 MQ”不在一个事务里,可能出现“订单成功但消息没发”的情况。
解法:

  • 在同一个本地事务中:INSERT INTO orders 并且 INSERT INTO message_queue。
  • 消费者使用 SKIP LOCKED 从 message_queue 表里取任务执行。
  • 结果: 完美保证了数据和任务的一致性,无需复杂的分布式事务框架(如 Seata)。
  • 场景二:低频高价值任务 (PDF 生成/视频转码)

    这些任务执行时间长(几秒到几分钟),并发量不大(每秒几十个)。
    引入 Kafka 显得太重,且 Kafka 的 Rebalance 机制在长耗时任务下很难调优(容易误判消费者掉线)。
    使用 MySQL 队列,天然支持长事务,且状态可视化,随时可以用 SQL 查看到哪些任务正在执行。

    场景三:定时任务分发 (替代 Quartz 数据库锁)

    如果你的系统是多节点部署,需要抢占执行定时任务。
    传统 Quartz 使用行锁,并发高时竞争严重。使用 SKIP LOCKED 可以让不同节点领取不同的任务分片,实现高效的分布式调度。


    4. 代码实战 (Java/MyBatis)

    // Mapper XML
    <select id="pollJob" resultType="Job">
    SELECT id, payload
    FROM jobs
    WHERE status = 'PENDING'
    ORDER BY id ASC
    LIMIT 1
    FOR UPDATE SKIP LOCKED
    </select>

    // Service 层
    @Transactional
    public void processJob() {
    // 1. 抢占任务 (非阻塞)
    Job job = jobMapper.pollJob();

    if (job == null) {
    return; // 队列为空
    }

    try {
    // 2. 执行耗时业务逻辑
    executeBusinessLogic(job.getPayload());

    // 3. 标记完成 (或者直接物理删除)
    jobMapper.updateStatus(job.getId(), "DONE");
    } catch (Exception e) {
    // 4. 异常处理:标记为重试或记录错误
    jobMapper.updateStatus(job.getId(), "FAILED");
    }
    }


    5. 局限性与注意事项

    虽然好用,但不要滥用:

  • 不适合超高吞吐: MySQL 的连接数是有限的。如果你的 QPS 是几万级别,请老老实实去用 Kafka/Redis。SKIP LOCKED 适合 QPS 在 几百到一两千的场景。
  • 死元组问题 (Dead Tuples): 如果你选择“处理完删除记录”,频繁的 INSERT 和 DELETE 会导致表碎片。建议定期 OPTIMIZE TABLE,或者采用“标记删除 + 定期归档”的策略。
  • 排序开销: 尽量配合索引使用。ORDER BY id 需要利用主键索引,否则 SKIP LOCKED 可能会扫描过多的行导致 CPU 升高。

  • 6. 总结

    MySQL + SKIP LOCKED = 免费的、持久化的、支持 ACID 的、可视化的消息队列。

    对于 80% 的中小规模异步任务场景,它比 Kafka 更简单,比 Redis 更可靠。它是架构师工具箱里处理“并发抢占”问题的一把瑞士军刀。

    赞(0)
    未经允许不得转载:171主机测评 » 别急着上 Kafka!MySQL 8.0 的 SKIP LOCKED 才是轻量级队列的神器
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址