事务必须显式开启,否则 autocommit=1 会自动提交
MySQL 默认是
autocommit=1,每条
INSERT/
UPDATE/
DELETE语句都会立即生效并释放锁。想用事务控制一致性,第一步就是关掉它:
SET autocommit = 0;或者更推荐的方式是用
BEGIN或
START TRANSACTION显式开启——这两者等价,且能确保后续语句在同一个事务上下文中执行。忘记这一步,后面加再多
SELECT ... FOR UPDATE都没用,因为语句一执行就提交了。
READ COMMITTED 下的 SELECT ... FOR UPDATE 只锁命中行,但不阻止幻读
在默认隔离级别
READ COMMITTED中,
SELECT ... FOR UPDATE会对查询结果集中的**已存在行**加行级写锁(Record Lock),其他事务无法修改或删除这些行。但它不会锁住“不存在的间隙”,所以另一个事务仍可插入满足相同条件的新行,造成幻读。例如:
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE;此时若另一事务执行
INSERT INTO orders (status) VALUES ('pending'),能成功插入——这不是 bug,是 READ COMMITTED的行为边界。要避免幻读,需升级到
REPEATABLE READ并依赖间隙锁(Gap Lock)。
死锁不是配置问题,而是事务内操作顺序不一致导致的
两个事务按不同顺序访问相同资源时极易触发死锁,比如:
事务 A 先更新user_id = 100,再更新
user_id = 200事务 B 先更新
user_id = 200,再更新
user_id = 100MySQL 检测到循环等待后会回滚其中一个事务(报错
Deadlock found when trying to get lock)。解决方法不是调大
innodb_lock_wait_timeout,而是统一所有业务逻辑中对多行记录的操作顺序,例如始终按
user_id升序更新:
UPDATE accounts SET balance = balance - 100 WHERE user_id IN (100, 200) ORDER BY user_id;这样能从源头消除竞争路径。
长事务会拖垮并发性能,锁持有时间远超业务需要
一个事务从
BEGIN到
COMMIT之间,所有加过的锁都不会释放。如果中间夹杂了 HTTP 调用、文件读写、用户输入等待等外部耗时操作,锁就会一直占着,阻塞其他事务。典型反例:
BEGIN; UPDATE inventory SET stock = stock - 1 WHERE sku = 'A123'; -- 这里调用第三方支付接口,耗时 2s INSERT INTO orders (...) VALUES (...); COMMIT;正确做法是把数据库操作尽量聚合成最小原子单元,把非 DB 操作移出事务块;必要时用乐观锁(如版本号字段
version)替代长事务悲观锁。
