mysql中事务与锁结合实现高并发写入

3次阅读

事务隔离级别决定锁行为:READ COMMITTED下for UPDATE仅锁命中行,REPEATABLE READ默认加间隙锁防幻读但易死锁,SERIALIZABLE使所有select加共享锁;INSERT…ON DUPLICATE KEY UPDATE依赖唯一索引记录锁,不加间隙锁;显式加锁需索引命中、避免耗时操作与顺序不一致;自增锁在高并发插入时成瓶颈,建议调优模式或改用无锁ID。

mysql中事务与锁结合实现高并发写入

事务隔离级别直接影响锁行为

mysqlSELECT ... FOR UPDATEINSERT ... ON DUPLICATE KEY UPDATE 是否加锁、加什么锁,首先取决于当前事务的隔离级别。在 READ COMMITTED 下,普通 SELECT 不加锁,但 FOR UPDATE 只锁定命中行;而在 REPEATABLE READ(InnoDB 默认)下,它还会隐式加间隙锁(Gap Lock),防止幻读——这既是保护,也是死锁温床。

  • 生产环境若大量使用范围条件更新(如 WHERE status = 0 LIMIT 10),REPEATABLE READ 下可能锁住整个索引区间,阻塞并发插入
  • 若业务能接受“读已提交”,可显式设为 SET TRANSACTION ISOLATION LEVEL READ COMMITTED,减少间隙锁范围
  • SERIALIZABLE 会把所有普通 SELECT 都转成 SELECT ... LOCK IN SHARE MODE,几乎等于串行化,极少用

INSERT … ON DUPLICATE KEY UPDATE 是最轻量的写冲突处理

当存在唯一键(UNIQUEPRIMARY KEY)时,INSERT ... ON DUPLICATE KEY UPDATE 在内部由 MySQL 自动处理“先查后插/更”的竞争,无需手动加锁,且只在发生冲突时才触发更新逻辑。它底层依赖的是唯一索引上的记录锁(Record Lock),不会扩展到间隙。

INSERT INTO order_log (order_id, status, updated_at)  VALUES (123, 'paid', NOW())  ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at = NOW();
  • 必须确保 order_id 有唯一索引,否则语句不生效或报错
  • 如果 UPDATE 子句中引用了未在 VALUES() 中提供的列(如 updated_at = updated_at + 1),要注意是否真的需要该语义——多数场景应直接赋值 NOW()
  • 该语句在 binlog 中以 ROW 格式记录,主从一致,但若用 MIXED 模式,某些函数(如 NOW())可能被转成 STATEMENT,引发主从时间不一致

显式加锁要慎用 SELECT … FOR UPDATE

当你必须“读出再计算再更新”(比如扣库存:查余额 → 判断是否足够 → 扣减),就得用 SELECT ... FOR UPDATE。但它不是万能解药,容易成为性能瓶颈和死锁源头。

  • 务必在 WHERE 条件上命中索引,否则会升级为表锁(尤其在 REPEATABLE READ 下)
  • 避免在事务里做耗时操作(如调用外部 http、复杂计算),否则锁持有时间过长
  • 多个 FOR UPDATE 语句访问行的顺序不一致(A 先锁 id=1 再锁 id=2,B 反过来),是典型死锁诱因;建议统一按主键升序加锁
  • 可以用 SELECT ... FOR UPDATE SKIP LOCKED(MySQL 8.0+)跳过已被锁的行,适合队列类消费场景

自增主键与 insert 并发的锁表现

InnoDB 的自增锁(auto-inc lock)在高并发 INSERT 时可能成为隐形瓶颈。默认 innodb_autoinc_lock_mode = 1(连续模式)下,简单插入(不含 INSERT ... SELECT)不锁表,但批量插入仍需获取表级自增锁。

  • 若应用频繁执行 INSERT INTO t SELECT ... FROM other_t,考虑调大 innodb_autoinc_lock_mode = 2(交错模式),但要求 binlog 格式为 ROW
  • 不要依赖 AUTO_INCREMENT 值做业务逻辑(如“订单号=时间戳+自增ID”),因为 ID 分配不连续、不可预测
  • 对于超高并发写入,可考虑分库分表、或改用雪花 ID 等无锁生成策略,把写压力从单表主键分配上卸下来

真正难的不是写对一条 FOR UPDATE,而是理清哪条语句在什么索引路径下会锁住哪些索引项、是否包含间隙、会不会被其他事务的相同语句反向覆盖——这些只有看 INFORMATION_SCHEMA.INNODB_TRXINNODB_LOCKS(MySQL 5.7)或 performance_schema.data_locks(8.0+)才能确认。

text=ZqhQzanResources