使用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重叠?
问题的核心原因有两点:
- 连接隔离失效:如果你的并行处理逻辑中,把“插入
tests→获取LAST_INSERT_ID→插入dependent”拆分成了多个独立的db.run调用,Slick会从连接池分配不同的连接执行这些操作。此时你拿到的LAST_INSERT_ID可能来自其他连接的插入操作,自然会出现ID重叠。即使你用了transactionally,如果没把三个操作包裹成一个完整的DBIO动作,依然会出现跨连接的问题。 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
相关产品推荐
相关产品推荐

