Skip to content

MySQL 事务与锁

理解事务是理解数据库的核心


1. 事务基础

1.1 什么是事务?

事务是数据库执行的最小单元:

sql
-- 典型场景:转账
START TRANSACTION;

UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;

-- 如果中间出错, rollback
-- 如果成功, commit
COMMIT;

1.2 ACID 特性

特性说明
Atomic(原子性)要么全成功,要么全失败
Consistency(一致性)事务前后数据一致
Isolation(隔离性)并发事务互不干扰
Durability(持久性)提交后数据永久保存

2. 事务隔离级别

2.1 四种隔离级别

sql
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别脏读不可重复读幻读
READ UNCOMMITTED✓ 可能✓ 可能✓ 可能
READ COMMITTED✗ 不可能✓ 可能✓ 可能
REPEATABLE READ (MySQL默认)✗ 不可能✗ 不可能✓ 可能
SERIALIZABLE✗ 不可能✗ 不可能✗ 不可能

2.2 脏读示例

sql
-- READ UNCOMMITTED 会发生脏读

-- 事务A
START TRANSACTION;
UPDATE users SET balance = balance - 1000 WHERE id = 1;
-- 未提交

-- 事务B(READ UNCOMMITTED)
SELECT * FROM users WHERE id = 1;  -- 看到 A 未提交的修改

-- 事务A ROLLBACK;
-- 事务B 读到的数据是错误的(脏读)

2.3 不可重复读

sql
-- READ COMMITTED 会发生不可重复读

-- 事务B
START TRANSACTION;
SELECT balance = 1; FROM users WHERE id  -- 1000

-- 事务A
UPDATE users SET balance = 2000 WHERE id = 1;
COMMIT;

-- 事务B 再次查询
SELECT balance FROM users WHERE id = 1;  -- 2000
-- 两次结果不同(不可重复读)

3. MySQL 锁机制

3.1 锁分类

按粒度:
- 表锁:锁整张表
- 行锁:锁单行记录
- 页锁:锁一页数据

按模式:
- 共享锁(S锁):读锁,可共存
- 排他锁(X锁):写锁,互斥

3.2 行锁与表锁

sql
-- 行锁(InnoDB)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- 共享锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;         -- 排他锁

-- 表锁(MyISAM)
LOCK TABLE users READ;
LOCK TABLE users WRITE;
UNLOCK TABLES;

3.3 意向锁

sql
-- InnoDB 的意向锁(表锁)
- 意向共享锁(IS):事务想要获取某行的共享锁
- 意向排他锁(IX):事务想要获取某行的排他锁

-- 目的:快速判断表是否有行锁

4. MVCC 机制

4.1 什么是 MVCC?

MVCC(Multi-Version Concurrency Control)多版本并发控制:

  • 同一行数据可以同时存在多个版本
  • 读操作不加锁,通过版本号实现
  • 提高并发性能

4.2 隐藏字段

sql
-- InnoDB 每行数据有三个隐藏字段:
- DB_ROW_ID:主键ID
- DB_TRX_ID:事务ID
- DB_ROLL_PTR:回滚指针

4.3 快照读 vs 当前读

sql
-- 快照读(不加锁)
SELECT * FROM users;  -- REPEATABLE READ 下的快照

-- 当前读(加锁)
SELECT * FROM users LOCK IN SHARE MODE;
SELECT * FROM users FOR UPDATE;
INSERT INTO users VALUES(...);
UPDATE users SET ...;
DELETE FROM users;

5. 锁问题与解决

5.1 死锁

sql
-- 死锁示例
-- 事务A:先锁 id=1,再锁 id=2
START TRANSACTION;
UPDATE users SET name = 'A' WHERE id = 1;
UPDATE users SET name = 'A' WHERE id = 2;
COMMIT;

-- 事务B:先锁 id=2,再锁 id=1
START TRANSACTION;
UPDATE users SET name = 'B' WHERE id = 2;
UPDATE users SET name = 'B' WHERE id = 1;
COMMIT;

-- 可能发生死锁

5.2 解决死锁

sql
-- 1. 统一加锁顺序
-- 2. 减小事务大小
-- 3. 查看死锁日志
SHOW ENGINE INNODB STATUS;

-- 4. 设置锁超时
SET GLOBAL innodb_lock_wait_timeout = 10;

6. 最佳实践

6.1 事务使用原则

sql
-- ✅ 正确用法
START TRANSACTION;
try {
    UPDATE accounts SET balance = balance - 1000 WHERE user_id = 1;
    UPDATE accounts SET balance = balance + 1000 WHERE user_id = 2;
    COMMIT;
} catch {
    ROLLBACK;
}

-- ❌ 错误用法
START TRANSACTION;
SELECT * FROM users;  -- 耗时操作
-- 其他事务等待...
COMMIT;
-- 事务时间过长,锁住资源

6.2 隔离级别选择

  • READ COMMITTED:大多数场景
  • REPEATABLE READ:需要一致性读
  • SERIALIZABLE:数据一致性要求极高

7. 总结

概念说明
ACID事务的四个特性
隔离级别解决并发问题
MVCC多版本并发控制
行锁/表锁锁的粒度
死锁互相等待,需要避免

📚 续篇:《MySQL SQL 优化实战》

> 学而时习之,不亦说乎?