mysql的事务隔离级别

隔离级别脏读不可重复读幻读性能
读未提交 ❌ 可能 ❌ 可能 ❌ 可能 ⭐⭐⭐⭐⭐
读已提交 ✅ 避免 ❌ 可能 ❌ 可能 ⭐⭐⭐⭐
可重复读 ✅ 避免 ✅ 避免 ❌ 可能* ⭐⭐⭐
串行化 ✅ 避免 ✅ 避免 ✅ 避免

# 幻读详解

## 1. 基本概念

### 定义
**幻读**是指在一个事务中,**两次相同的范围查询**返回了**不同数量的行**,就像出现了"幻觉"一样。

### 与不可重复读的区别
| 对比项 | 不可重复读 | 幻读 |
|--------|-----------|------|
| **关注点** | 同一行数据内容变化 | 行数量的变化 |
| **操作类型** | UPDATE操作引起 | INSERT/DELETE操作引起 |
| **锁机制** | 行锁可解决 | 需要间隙锁或表锁 |
| **示例** | 余额从1000→900 | 多出一行或少了一行 |

## 2. 经典示例

### 场景模拟
```sql
-- 创建测试表
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
salary DECIMAL(10, 2),
department VARCHAR(50),
INDEX idx_department (department)
);

-- 初始数据
INSERT INTO employees VALUES
(1, 'Alice', 5000, 'IT'),
(2, 'Bob', 6000, 'IT'),
(3, 'Charlie', 7000, 'HR');
```

### 幻读发生过程
```sql
-- 事务A:统计IT部门人数和总工资
START TRANSACTION;

-- 第一次查询
SELECT COUNT(*) as emp_count, SUM(salary) as total_salary
FROM employees
WHERE department = 'IT';
-- 结果:emp_count=2, total_salary=11000

-- ⚠️ 此时事务B插入新员工
-- 事务B:
START TRANSACTION;
INSERT INTO employees VALUES (4, 'David', 8000, 'IT');
COMMIT;

-- 事务A再次查询
SELECT COUNT(*) as emp_count, SUM(salary) as total_salary
FROM employees
WHERE department = 'IT';
-- 结果:emp_count=3, total_salary=19000 ← 出现幻读!

-- 事务A更新数据
UPDATE employees SET salary = salary * 1.1
WHERE department = 'IT';
-- 影响了3行!包括新插入的David

COMMIT;
```

## 3. 不同隔离级别的表现

### 1. **READ COMMITTED(读已提交)**
```sql
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
```
**存在幻读**:每次查询都能看到最新提交的数据

### 2. **REPEATABLE READ(可重复读)- MySQL默认**
```sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
```
**特殊情况**:
- **快照读**:使用MVCC,避免幻读
- **当前读**:可能出现幻读

```sql
-- 示例:RR级别下的幻读
START TRANSACTION;

-- 快照读(无幻读)
SELECT * FROM employees WHERE department = 'IT'; -- 看到2条

-- 其他事务插入数据并提交...

-- 还是看到2条(无幻读)
SELECT * FROM employees WHERE department = 'IT';

-- 但是!如果执行当前读
SELECT * FROM employees WHERE department = 'IT' FOR UPDATE;
-- 或者执行UPDATE
UPDATE employees SET status = 'active' WHERE department = 'IT';
-- 这时会看到新插入的行!出现幻读
```

### 3. **SERIALIZABLE(串行化)**
```sql
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
```
**完全避免幻读**:通过锁机制阻止其他事务插入

## 4. MySQL如何解决幻读

### 方案1:Next-Key Lock(间隙锁)
```sql
-- 当执行以下语句时
SELECT * FROM employees
WHERE department = 'IT'
FOR UPDATE;

-- MySQL不仅锁定存在的行,还锁定间隙
-- 假设现有department值:'HR', 'IT', 'Sales'
-- 会锁定:
-- 1. 所有department='IT'的行(记录锁)
-- 2. ('HR', 'IT')和('IT', 'Sales')的区间(间隙锁)
-- 防止在IT部门插入新员工
```

### 方案2:使用唯一索引
```sql
-- 如果查询条件包含唯一索引或主键
SELECT * FROM employees WHERE id = 5 FOR UPDATE;
-- 只加记录锁,不加间隙锁
-- 但不会发生幻读,因为id=5要么存在要么不存在
```

### 方案3:提升隔离级别
```sql
-- 直接使用串行化级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM employees WHERE department = 'IT';
-- 其他事务的INSERT会被阻塞
```

## 5. 实际业务场景

### 场景1:库存检查
```sql
-- 有问题的代码
START TRANSACTION;

-- 检查库存
SELECT COUNT(*) FROM inventory
WHERE product_id = 100 AND quantity > 0;
-- 返回1,表示有库存

-- 在此期间,另一个事务卖出最后一件商品
-- UPDATE inventory SET quantity = 0 WHERE id = 5;

-- 继续处理(实际上已无库存)
INSERT INTO orders (product_id, quantity) VALUES (100, 1);
-- 这里可能超卖!

COMMIT;

-- 解决方案:使用悲观锁
START TRANSACTION;
SELECT * FROM inventory
WHERE product_id = 100 AND quantity > 0
FOR UPDATE; -- 加锁
-- 检查并更新...
COMMIT;
```

### 场景2:生成唯一序列号
```sql
-- 需要获取下一个可用的用户编号
START TRANSACTION;

-- 获取当前最大值
SELECT MAX(user_no) FROM users; -- 返回100

-- 在此期间,另一个事务插入user_no=101

-- 错误地插入重复值风险
INSERT INTO users (user_no, name) VALUES (101, '新用户');

-- 解决方案1:使用序列/自增
-- 解决方案2:加表锁或更高级别隔离
```

### 场景3:统计报表
```sql
-- 生成月度统计报告
START TRANSACTION;

-- 统计1月销售总额
SELECT SUM(amount) FROM sales
WHERE sale_date BETWEEN '2024-01-01' AND '2024-01-31';
-- 返回10000

-- 在此期间,另一个事务插入1月的退货记录
INSERT INTO sales (sale_date, amount, type)
VALUES ('2024-01-15', -500, 'refund');

-- 再次统计(需要一致性)
SELECT SUM(amount) FROM sales
WHERE sale_date BETWEEN '2024-01-01' AND '2024-01-31';
-- 如果出现幻读:返回9500,报表不一致

-- 解决方案:使用REPEATABLE READ隔离级别
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
```

## 6. 检测和避免幻读

### 检测方法
```sql
-- 监控锁等待情况
SHOW ENGINE INNODB STATUS\G
-- 查看 LATEST DETECTED DEADLOCK 部分

-- 查看当前锁信息
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
```

### 避免策略

#### 1. **合理设计事务**
```sql
-- 短事务,尽快提交
START TRANSACTION;
-- 业务操作...
COMMIT; -- 尽早提交

-- 长事务分拆
```

#### 2. **正确使用锁**
```sql
-- 明确需要防止幻读时使用FOR UPDATE
START TRANSACTION;
SELECT * FROM table WHERE condition FOR UPDATE;
-- 处理业务...
COMMIT;
```

#### 3. **优化查询条件**
```sql
-- 使用等值查询+唯一索引(避免间隙锁)
SELECT * FROM users WHERE id = 100 FOR UPDATE;

-- 避免全表扫描的范围查询
```

#### 4. **业务层防御**
```python
# Python伪代码 - 使用乐观锁
def update_quantity(product_id, quantity):
retry_count = 3
while retry_count > 0:
# 获取当前版本
cursor.execute("""
SELECT quantity, version
FROM products
WHERE id = %s
""", (product_id,))

current_qty, version = cursor.fetchone()

if current_qty >= quantity:
# 尝试更新
cursor.execute("""
UPDATE products
SET quantity = quantity - %s,
version = version + 1
WHERE id = %s AND version = %s
""", (quantity, product_id, version))

if cursor.rowcount > 0:
conn.commit()
return True

retry_count -= 1

return False # 更新失败
```

## 7. 性能影响

### 间隙锁的开销
```sql
-- 当使用范围查询加锁时
SELECT * FROM employees
WHERE salary BETWEEN 5000 AND 10000
FOR UPDATE;

-- 可能锁定的范围:
-- 1. salary=5000的记录(如果有)
-- 2. (上一记录, 5000)的间隙
-- 3. 5000到10000之间的所有记录和间隙
-- 4. (10000, 下一记录)的间隙

-- 影响:阻塞其他事务的INSERT/UPDATE
```

### 最佳实践建议
1. **尽量使用等值查询**
2. **合理创建索引**
3. **避免大范围加锁**
4. **考虑使用READ COMMITTED+业务逻辑控制**

## 总结

幻读是数据库并发控制中的经典问题,理解其产生原理和解决方案对于设计高并发系统至关重要。MySQL在REPEATABLE READ级别通过MVCC和Next-Key Lock机制在大多数情况下避免了幻读,但在当前读场景下仍需开发者注意。

**核心要点**:
- 幻读关注的是**行数量**的变化
- MySQL的RR级别通过**快照读**避免幻读
- **当前读**操作(FOR UPDATE、UPDATE)可能遇到幻读
- 使用**Next-Key Lock**或**串行化级别**可完全避免
- 业务设计时应考虑幻读对一致性的影响

posted @ 2026-01-07 15:32  人在代码在  阅读(1)  评论(0)    收藏  举报