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

来源:这里教程网 时间:2026-02-28 20:45:43 作者:

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

MySQL 的

SELECT ... FOR UPDATE
INSERT ... 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 是最轻量的写冲突处理

当存在唯一键(

UNIQUE
PRIMARY 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_TRX
INNODB_LOCKS
(MySQL 5.7)或
performance_schema.data_locks
(8.0+)才能确认。

相关推荐