事务隔离性与隔离级别
前言
我们在上一章节中介绍过数据库的 带你了解数据库中事务的 ACID 特性 相关用法。本章节主要介绍数据库中一个非常重要的知识点:事务的隔离级别。
本章将重点探讨以下问题:
- 事务的隔离级别有哪些?
- 如果并发事务没有进行隔离,会出现什么问题?
注:以下示例均采用 MySQL 数据库。
在多个事务并发操作数据库时,如果没有有效的隔离机制,就会引发种种数据一致性问题。大体上可分为以下几类。
一、并发事务引发的问题
在并发事务没有进行隔离的情况下,主要会发生以下三种问题。
1. 脏读(Dirty Read)
脏读 指一个事务读取了另外一个事务未提交的数据。
具体案例见后文介绍。
2. 不可重复读(Non-repeatable Read)
不可重复读 指在一个事务内读取表中的某一行数据,多次读取结果不同。
不可重复读与脏读的区别:脏读是读取了前一事务未提交的脏数据;不可重复读是重新读取了前一事务已提交的数据。
具体案例见后文介绍。
3. 幻读(Phantom Read)
幻读(又称虚读)指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
具体案例见后文介绍。
二、事务隔离级别概念
2.1 隔离级别分类
事务的隔离级别主要分为以下四种:
- Read Uncommitted(读未提交)
- Read Committed(读已提交)
- Repeatable Read(可重复读)
- Serializable(串行化)
2.2 各级别特性详解
Read Uncommitted
读未提交:隔离级别最低。在这种隔离级别下,会引发脏读、不可重复读和幻读。
Read Committed
读已提交:读到的都是别人提交后的值。这种隔离级别下,会引发不可重复读和幻读,但避免了脏读。
Repeatable Read
可重复读:这种隔离级别下,会引发幻读,但避免了脏读和不可重复读。
Serializable
串行化:最严格的隔离级别。在 Serializable 隔离级别下,所有事务按照次序依次执行。脏读、不可重复读、幻读都不会出现。
三、MySQL 事务隔离级别操作
3.1 查看事务隔离级别
查看当前会话的事务隔离级别:
SHOW VARIABLES LIKE 'tx_isolation';查看全局的事务隔离级别:
SHOW GLOBAL VARIABLES LIKE 'tx_isolation';使用系统变量查询:
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;3.2 设置 MySQL 的事务隔离级别
语法格式
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL
{
REPEATABLE READ
| READ COMMITTED
| READ UNCOMMITTED
| SERIALIZABLE
};GLOBAL:设置全局的事务隔离级别。SESSION:设置当前 Session 的事务隔离级别。如果语句没有指定GLOBAL或SESSION,默认值为SESSION。
使用系统变量设置
SET GLOBAL tx_isolation='REPEATABLE-READ';
SET SESSION tx_isolation='SERIALIZABLE';四、案例分析
下面通过实际操作演示并发控制语句,具体命令可参考上面的 操作 介绍。
演示表结构(product 表):
| productId | productName | productPrice | productCount |
|---|---|---|---|
| 1 | xiaomi | 1999 | 100 |
带着上面的数据,我们来看一下事务在没有隔离性的情况下会引发哪些问题。以下操作同时打开两个窗口模拟 2 个用户并发访问数据库。
4.1 事务隔离级别设置为 Read Uncommitted
查询事务隔离级别:
SELECT @@tx_isolation;设置隔离级别为未提交读:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;注意:需要同时修改两个窗口的事务隔离级别。
以下以两位用户抢小米手机为例:
| 时间轴 | 事务 A | 事务 B |
|---|---|---|
| T1 | start transaction; | |
| T2 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T3 | start transaction; | |
| T4 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T5 | update product set productCount = 99 where productId = 1; | |
| T6 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 99) | |
| T7 | ROLLBACK; | |
| T8 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) |
流程解析:
- T1:A 用户开启事务。
- T2:A 用户查询当前小米手机剩余数量,显示为 100。
- T3:B 用户开启事务。
- T4:B 用户查询当前小米手机剩余数量,显示为 100。
- T5:B 用户购买了一台小米手机,执行 update 操作。此时只修改数据并未提交事务。
- T6:A 用户刷新页面查询,此时数量显示为 99(读取了 B 未提交的数据)。
- T7:B 用户购买失败,回滚事务。
- T8:A 用户再次查询,此时数量显示恢复为 100。
小结:
事务 A 读取了未提交的数据,事务 B 的回滚导致了事务 A 的数据不一致,导致了事务 A 的 脏读。
4.2 事务隔离级别设置为 Read Committed
查询事务隔离级别:
SELECT @@tx_isolation;设置隔离级别为提交读:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;注意:需要同时修改两个窗口的事务隔离级别。
| 时间轴 | 事务 A | 事务 B |
|---|---|---|
| T1 | start transaction; | |
| T2 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T3 | start transaction; | |
| T4 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T5 | update product set productCount = 99 where productId = 1; | |
| T6 | commit; | |
| T7 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T8 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 99) |
小结:
可以看到避免了 脏读 现象(T7 时刻 A 读取的仍是 100,因为 B 尚未提交)。但是却出现了 不可重复读 问题:事务 A 还没有结束,再次读取时(T8),productCount 从 100 变成了 99。
4.3 事务隔离级别设置为 Repeatable Read(MySQL 默认级别)
查询事务隔离级别:
SELECT @@tx_isolation;设置隔离级别为可重复读:
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;注意:需要同时修改两个窗口的事务隔离级别。
| 时间轴 | 事务 A | 事务 B |
|---|---|---|
| T1 | start transaction; | |
| T2 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T3 | start transaction; | |
| T4 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T5 | update product set productCount = 99 where productId = 1; | |
| T6 | commit; | |
| T7 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T8 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) |
小结:
可以看到 可重复读 隔离级别避免了 脏读 和 不可重复读 的问题。事务 A 查询到的小米数量始终等于 100,即使事务 B 修改并提交了数量为 99,事务 A 读取到的值还是 100。
注:理论上该级别仍可能出现 幻读 现象。例如当事务 A 去减 1 等于 99 时,逻辑上可能产生错误(此时应该是 99-1=98 才对)。接下来我们再提高一个事务隔离级别。
4.4 事务隔离级别设置为 Serializable
查询事务隔离级别:
SELECT @@tx_isolation;设置隔离级别为串行化:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;| 时间轴 | 事务 A | 事务 B |
|---|---|---|
| T1 | start transaction; | |
| T2 | select p.productName, p.productCount from product p where p.productId=1; (productCount = 100) | |
| T3 | start transaction; | |
| T4 | update product set productCount = 99 where productId = 1; (等待中..) |
小结:
在 Serializable 隔离级别中,我们可以看到事务 B 去做修改动作时卡住了,不能向下执行。这是因为给事务 A 的 select 操作上了锁,所以事务 B 去修改值的话,就会被阻塞。只有当事务 A 操作执行完毕,才会执行事务 B 的操作。这样就避免了上述三个问题。
五、总结与建议
问题本质
回到问题的本身,其实我们并不需要将事务隔离级别提到这么高(如 Serializable),因为这会严重影响并发性能。
问题的本质是:当我们读完数据后,希望在这期间别人不能修改它。因为别人读到了 count,就会修改 count 的值并写进去。所以我们在 select 操作的时候,加上 for update。这时候就会把这行操作给锁掉。那么另外一个人也进行相同的操作,也表示 select 出来的 count 需要进行 update,需要锁住。
select p.productName, p.productCount from product p where p.productId=1 for update;通过悲观锁机制(FOR UPDATE),可以在较低的隔离级别下实现数据的一致性控制,避免过度依赖高隔离级别带来的性能损耗。
说明:本文示例基于 MySQL 5.7 及以下版本。在 MySQL 8.0 中,系统变量tx_isolation已被废弃,建议使用transaction_isolation代替。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/shi-wu-ge-li-xing-yu-ge-li-ji-bie.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。