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

使用Slick与MariaDB批量插入时LAST_INSERT_ID冲突问题排查

问题描述

我使用Scala的Alpakka Slick向MariaDB插入数据,涉及两张关联表:需先批量插入主表tests,再用新插入行的ID批量插入从表dependent。
当前采用的算法步骤:

  • 批量插入tests表数据
  • 执行SELECT 1838933;获取该批次首行的自增ID
  • 根据首行ID和批次行数计算出该批次所有行的ID,以此插入dependent表

单连接场景下该算法运行正常,但模拟多并行写入(使用Scala并行集合+Akka Stream Source)时,出现LAST_INSERT_ID重叠问题:不同连接返回的ID互相干扰,导致dependent表出现重复的tests.id。
即使将整个流程用Slick的transactionally包裹成事务,在innodb_autoinc_lock_mode=1时问题依然存在,仅当设置innodb_autoinc_lock_mode=0时问题消失。

限制条件

  • 必须兼容不支持INSERT...RETURNING的MariaDB 10.4
  • Slick的++=操作搭配returning时会拆分为单条INSERT,不符合流式批量写入的性能需求
  • 生产环境自增步长固定为1,无多主复制架构

核心疑问

LAST_INSERT_ID是绑定单个数据库连接的,为什么多连接并行写入时会出现ID重叠的情况?


问题分析与解决思路

为什么会出现ID重叠?

问题的核心原因有两点:

  1. 连接隔离失效:如果你的并行处理逻辑中,把“插入tests→获取LAST_INSERT_ID→插入dependent”拆分成了多个独立的db.run调用,Slick会从连接池分配不同的连接执行这些操作。此时你拿到的LAST_INSERT_ID可能来自其他连接的插入操作,自然会出现ID重叠。即使你用了transactionally,如果没把三个操作包裹成一个完整的DBIO动作,依然会出现跨连接的问题。
  2. innodb_autoinc_lock_mode=1的特性放大了问题:当该参数设为1(连续自增锁模式)时,InnoDB对确定行数的批量INSERT使用语句级锁——只要当前INSERT语句执行完成,锁就会释放,其他连接可立刻分配下一段自增ID。如果此时你的流程跨连接执行,就很容易拿到其他连接的自增ID;而设为0(传统锁模式)时,InnoDB会持有表级自增锁直到事务提交,相当于强制串行化ID分配,巧合掩盖了连接隔离的问题,但会严重降低并行写入性能。

可行解决方案

1. 确保全程使用同一个连接与事务

将“批量插入tests、获取批次首ID、批量插入dependent”封装为一个完整的DBIO动作,通过transactionally保证全程使用同一个数据库连接。示例代码如下:

import slick.jdbc.MySQLProfile.api._

// 假设TestData和DependentData是对应表的case class
def processBatch(testBatch: Seq[TestData]): DBIO[Unit] = {
  for {
    // 批量插入主表
    _ <- TableQuery[Tests] ++= testBatch
    // 获取当前连接中该批次的首行自增ID
    firstId <- sql"SELECT 1838933".as[Long].head
    // 生成从表数据(首ID + 索引即为对应主表行的ID)
    dependentBatch = testBatch.zipWithIndex.map { case (data, idx) =>
      DependentData(testId = firstId + idx, /* 其他字段 */)
    }
    // 批量插入从表
    _ <- TableQuery[Dependents] ++= dependentBatch
  } yield ()
}

// 执行事务
db.run(processBatch(testBatch).transactionally)

这种方式能保证LAST_INSERT_ID是当前连接插入批次的首ID,完全不受其他连接干扰。

2. 预分配ID范围(可选优化)

如果生产环境允许额外维护一个序列表,可以提前为每个并行批次预分配ID范围,彻底规避自增ID的分配问题:

  • 创建一个序列表(如id_sequence),包含table_name和current_value字段
  • 每个批次通过原子操作申请ID范围:UPDATE id_sequence SET current_value = current_value + ${batchSize} WHERE table_name = 'tests' RETURNING current_value - ${batchSize}
  • 用申请到的起始ID生成主表和从表的插入数据,无需依赖LAST_INSERT_ID
    MariaDB 10.4支持UPDATE...RETURNING,可以实现原子性的ID范围申请。

3. 不推荐:强制使用innodb_autoinc_lock_mode=0

虽然设置该参数为0可以解决问题,但它会启用表级自增锁,直到事务提交才释放,会严重降低并行写入的吞吐量,仅适合低并发场景临时应急。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 06:03:20