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**或**串行化级别**可完全避免
- 业务设计时应考虑幻读对一致性的影响

浙公网安备 33010602011771号