事务与锁

一、事务基础

1.1 事务概念

1.1.1 什么是事务

事务:一组数据库操作,要么全部成功,要么全部失败。

东巴文理解:

事务 = 原子操作
转账操作 = 扣款 + 到账
要么都成功,要么都失败
不能只完成一半

1.1.2 ACID特性

原子性(Atomicity):

  • 事务是不可分割的工作单位
  • 要么全部完成,要么全部不完成

一致性(Consistency):

  • 事务必须使数据库从一个一致性状态变换到另一个一致性状态
  • 数据完整性约束不被破坏

隔离性(Isolation):

  • 多个事务并发执行时,一个事务的执行不应影响其他事务的执行

持久性(Durability):

  • 事务一旦提交,对数据库的改变是永久性的

1.2 事务控制语句

1.2.1 开启事务

语法格式:

START TRANSACTION;
-- 或
BEGIN;

示例:

-- 开启事务
START TRANSACTION;

-- 执行SQL语句
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交事务
COMMIT;

1.2.2 提交事务

语法格式:

COMMIT;

示例:

-- 转账事务
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 提交事务
COMMIT;

1.2.3 回滚事务

语法格式:

ROLLBACK;

示例:

-- 转账事务
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;

-- 发现错误,回滚事务
ROLLBACK;

1.2.4 保存点

语法格式:

SAVEPOINT 保存点名;
ROLLBACK TO SAVEPOINT 保存点名;
RELEASE SAVEPOINT 保存点名;

示例:

START TRANSACTION;

INSERT INTO orders (id, amount) VALUES (1, 100);
SAVEPOINT sp1;

INSERT INTO orders (id, amount) VALUES (2, 200);
SAVEPOINT sp2;

INSERT INTO orders (id, amount) VALUES (3, 300);

-- 回滚到保存点sp2
ROLLBACK TO SAVEPOINT sp2;

-- 提交事务
COMMIT;
-- 最终只插入id=1和id=2的记录

1.3 自动提交

1.3.1 查看自动提交

-- 查看自动提交状态
SHOW VARIABLES LIKE 'autocommit';

1.3.2 设置自动提交

-- 关闭自动提交
SET autocommit = 0;

-- 开启自动提交
SET autocommit = 1;

注意:

  • MySQL默认开启自动提交
  • 每条SQL语句都是一个事务
  • 关闭自动提交后需要手动COMMIT

二、隔离级别

2.1 隔离级别介绍

2.1.1 四种隔离级别

隔离级别 脏读 不可重复读 幻读 性能
读未提交(READ UNCOMMITTED) 可能 可能 可能 最高
读已提交(READ COMMITTED) 不可能 可能 可能 高
可重复读(REPEATABLE READ) 不可能 不可能 可能 中
串行化(SERIALIZABLE) 不可能 不可能 不可能 最低

2.1.2 并发问题

脏读:

  • 读取到其他事务未提交的数据

不可重复读:

  • 同一事务中,两次读取同一数据得到不同结果
  • 其他事务修改了数据

幻读:

  • 同一事务中,两次查询结果数量不同
  • 其他事务插入或删除了数据

2.2 查看和设置隔离级别

2.2.1 查看隔离级别

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

-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

2.2.2 设置隔离级别

语法格式:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;

示例:

-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

2.3 各隔离级别演示

2.3.1 读未提交

设置隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

演示脏读:

-- 会话A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 未提交

-- 会话B
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 读取到未提交的数据
COMMIT;

-- 会话A
ROLLBACK;  -- 回滚

2.3.2 读已提交

设置隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

演示不可重复读:

-- 会话A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 第一次查询

-- 会话B
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- 提交

-- 会话A
SELECT balance FROM accounts WHERE id = 1;  -- 第二次查询,结果不同
COMMIT;

2.3.3 可重复读

设置隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

演示避免不可重复读:

-- 会话A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;  -- 第一次查询

-- 会话B
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- 提交

-- 会话A
SELECT balance FROM accounts WHERE id = 1;  -- 第二次查询,结果相同
COMMIT;

注意:MySQL的REPEATABLE READ通过MVCC和Next-Key Lock避免了幻读。

2.3.4 串行化

设置隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

演示避免幻读:

-- 会话A
START TRANSACTION;
SELECT * FROM accounts WHERE id > 0;  -- 第一次查询

-- 会话B
START TRANSACTION;
INSERT INTO accounts (id, balance) VALUES (3, 1000);  -- 被阻塞
COMMIT;

-- 会话A
SELECT * FROM accounts WHERE id > 0;  -- 第二次查询,结果相同
COMMIT;

三、锁机制

3.1 锁的类型

3.1.1 表锁

特点:

  • 锁定整张表
  • 开销小,加锁快
  • 并发度低

语法格式:

LOCK TABLES 表名 READ|WRITE;
UNLOCK TABLES;

示例:

-- 加读锁
LOCK TABLES accounts READ;
SELECT * FROM accounts;
UNLOCK TABLES;

-- 加写锁
LOCK TABLES accounts WRITE;
UPDATE accounts SET balance = balance + 100 WHERE id = 1;
UNLOCK TABLES;

3.1.2 行锁

特点:

  • 锁定一行或多行
  • 开销大,加锁慢
  • 并发度高
  • InnoDB支持

示例:

-- 行锁(通过事务)
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- 加排他锁
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;

3.2 共享锁与排他锁

3.2.1 共享锁(S锁)

特点:

  • 允许多个事务同时读取
  • 不允许修改

语法格式:

SELECT * FROM 表名 WHERE 条件 LOCK IN SHARE MODE;
-- 或(MySQL 8.0+)
SELECT * FROM 表名 WHERE 条件 FOR SHARE;

示例:

-- 会话A
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;  -- 加共享锁

-- 会话B
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;  -- 成功
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 被阻塞
COMMIT;

-- 会话A
COMMIT;  -- 释放锁

3.2.2 排他锁(X锁)

特点:

  • 只允许一个事务修改
  • 其他事务不能读取或修改

语法格式:

SELECT * FROM 表名 WHERE 条件 FOR UPDATE;

示例:

-- 会话A
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- 加排他锁

-- 会话B
START TRANSACTION;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;  -- 被阻塞
COMMIT;

-- 会话A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT;  -- 释放锁

3.3 意向锁

3.3.1 意向锁概念

意向锁:表级锁,表示事务打算在表中的行上加锁。

类型:

  • 意向共享锁(IS):打算在行上加共享锁
  • 意向排他锁(IX):打算在行上加排他锁

作用:

  • 提高表锁和行锁的兼容性判断效率
  • 避免逐行检查

3.3.2 锁兼容性

锁类型 IS IX S X
IS ✅ ✅ ✅ ❌
IX ✅ ✅ ❌ ❌
S ✅ ❌ ✅ ❌
X ❌ ❌ ❌ ❌

3.4 记录锁、间隙锁、临键锁

3.4.1 记录锁(Record Lock)

特点:

  • 锁定索引记录
  • 防止其他事务修改或删除

示例:

-- 锁定id=1的记录
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;

3.4.2 间隙锁(Gap Lock)

特点:

  • 锁定索引记录之间的间隙
  • 防止其他事务插入
  • 不包含记录本身

示例:

-- 锁定id在1到3之间的间隙(不包含1和3)
SELECT * FROM accounts WHERE id > 1 AND id < 3 FOR UPDATE;

3.4.3 临键锁(Next-Key Lock)

特点:

  • 记录锁 + 间隙锁
  • 锁定记录和前面的间隙
  • 防止幻读

示例:

-- 锁定id<=3的记录和间隙
SELECT * FROM accounts WHERE id <= 3 FOR UPDATE;

四、死锁

4.1 死锁概念

4.1.1 什么是死锁

死锁:两个或多个事务互相等待对方释放锁,导致永久阻塞。

示例:

-- 会话A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 锁定id=1

-- 会话B
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 2;  -- 锁定id=2

-- 会话A
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- 等待id=2的锁

-- 会话B
UPDATE accounts SET balance = balance + 100 WHERE id = 1;  -- 等待id=1的锁
-- 死锁发生

4.2 死锁检测

4.2.1 查看死锁

-- 查看死锁信息
SHOW ENGINE INNODB STATUS;

输出:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-01 12:00:00 0x7f8a8c0b6700
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 10, OS thread handle 140234567891456, query id 100 localhost root updating
UPDATE accounts SET balance = balance + 100 WHERE id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 4 n bits 72 index PRIMARY of table `test`.`accounts` trx id 12345 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
...

4.2.2 死锁检测配置

-- 查看死锁检测配置
SHOW VARIABLES LIKE 'innodb_deadlock_detect';

-- 开启死锁检测
SET GLOBAL innodb_deadlock_detect = ON;

4.3 死锁预防

4.3.1 预防策略

1. 按固定顺序访问表:

-- 不推荐:不同事务访问顺序不同
-- 事务A:先访问表1,再访问表2
-- 事务B:先访问表2,再访问表1

-- 推荐:所有事务按相同顺序访问
-- 事务A:先访问表1,再访问表2
-- 事务B:先访问表1,再访问表2

2. 大事务拆小事务:

-- 不推荐:大事务
START TRANSACTION;
-- 大量操作
COMMIT;

-- 推荐:小事务
START TRANSACTION;
-- 少量操作
COMMIT;

3. 尽量使用索引访问:

-- 不推荐:无索引,锁全表
UPDATE accounts SET balance = balance - 100 WHERE name = '张三';

-- 推荐:有索引,锁行
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

4. 设置锁等待超时:

-- 查看锁等待超时时间
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

-- 设置锁等待超时时间(秒)
SET innodb_lock_wait_timeout = 10;

4.4 死锁处理

4.4.1 自动回滚

MySQL检测到死锁后,自动回滚其中一个事务。

查看被回滚的事务:

SHOW ENGINE INNODB STATUS;

4.4.2 手动处理

-- 查看当前运行的事务
SELECT * FROM information_schema.INNODB_TRX;

-- 查看当前的锁
SELECT * FROM information_schema.INNODB_LOCKS;

-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 杀死事务
KILL 事务ID;

五、MVCC

5.1 MVCC概念

5.1.1 什么是MVCC

MVCC(多版本并发控制):通过保存数据的历史版本,实现非阻塞读。

优点:

  • 读写不冲突
  • 提高并发性能
  • 实现快照读

5.2 实现原理

5.2.1 隐藏字段

InnoDB每行数据包含:

  • DB_TRX_ID:最后修改该行的事务ID
  • DB_ROLL_PTR:回滚指针,指向undo log
  • DB_ROW_ID:行ID(无主键时使用)

5.2.2 Undo Log

作用:

  • 保存数据的历史版本
  • 支持事务回滚
  • 支持MVCC

示例:

当前数据:
id=1, name='张三', DB_TRX_ID=100, DB_ROLL_PTR=0x123

Undo Log(旧版本):
id=1, name='李四', DB_TRX_ID=99, DB_ROLL_PTR=0x456

5.2.3 Read View

Read View:事务进行快照读时产生的读视图。

包含内容:

  • m_ids:当前活跃的事务ID列表
  • min_trx_id:最小活跃事务ID
  • max_trx_id:下一个事务ID
  • creator_trx_id:创建该Read View的事务ID

可见性判断:

  1. DB_TRX_ID < min_trx_id:可见
  2. DB_TRX_ID >= max_trx_id:不可见
  3. DB_TRX_ID在m_ids中:不可见
  4. DB_TRX_ID不在m_ids中:可见

5.3 快照读与当前读

5.3.1 快照读

特点:

  • 读取历史版本
  • 不加锁
  • 提高并发性能

示例:

-- 快照读
SELECT * FROM accounts WHERE id = 1;

5.3.2 当前读

特点:

  • 读取最新版本
  • 加锁
  • 保证数据一致性

示例:

-- 当前读
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE;
INSERT INTO accounts VALUES (...);
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
DELETE FROM accounts WHERE id = 1;

六、实战案例

6.1 转账事务

-- 创建账户表
CREATE TABLE accounts (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10, 2)
);

INSERT INTO accounts VALUES
(1, '张三', 1000.00),
(2, '李四', 1000.00);

-- 转账事务
DELIMITER //
CREATE PROCEDURE transfer(IN from_id INT, IN to_id INT, IN amount DECIMAL(10, 2))
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT '转账失败' AS result;
    END;
    
    START TRANSACTION;
    
    -- 检查余额
    IF (SELECT balance FROM accounts WHERE id = from_id) >= amount THEN
        -- 扣款
        UPDATE accounts SET balance = balance - amount WHERE id = from_id;
        -- 到账
        UPDATE accounts SET balance = balance + amount WHERE id = to_id;
        COMMIT;
        SELECT '转账成功' AS result;
    ELSE
        ROLLBACK;
        SELECT '余额不足' AS result;
    END IF;
END //
DELIMITER ;

-- 调用存储过程
CALL transfer(1, 2, 100.00);

6.2 库存扣减

-- 创建商品表
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    stock INT
);

INSERT INTO products VALUES (1, 'iPhone', 100);

-- 扣减库存(避免超卖)
START TRANSACTION;

-- 使用排他锁锁定商品
SELECT stock FROM products WHERE id = 1 FOR UPDATE;

-- 检查库存
IF (SELECT stock FROM products WHERE id = 1) > 0 THEN
    UPDATE products SET stock = stock - 1 WHERE id = 1;
    COMMIT;
    SELECT '扣减成功' AS result;
ELSE
    ROLLBACK;
    SELECT '库存不足' AS result;
END IF;

6.3 批量插入事务

-- 批量插入数据
START TRANSACTION;

INSERT INTO orders (id, amount) VALUES (1, 100);
INSERT INTO orders (id, amount) VALUES (2, 200);
INSERT INTO orders (id, amount) VALUES (3, 300);

COMMIT;

七、本章小结

7.1 核心要点

✅ 事务具有ACID特性 ✅ 隔离级别解决并发问题 ✅ 锁机制保证数据一致性 ✅ MVCC实现非阻塞读 ✅ 死锁需要预防和处理

7.2 验证清单

完成本章学习后,请确认您能够:

  • 理解事务的ACID特性
  • 使用事务控制语句
  • 设置和理解隔离级别
  • 理解锁的类型和机制
  • 处理死锁问题