文章目录
📝事务隔离级别
如何理解隔离性1
- MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行
- 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
- 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
- 就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
- 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多
- 并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)
隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。
查看与设置隔离性
以下是替换 tx_isolation
为 transaction_isolation
后的完整内容,保留原有逻辑和格式:
🌠 查看事务隔离级别
-- 查看
-- 查看全局隔离级别,替换 @@global.tx_isolation 为 @@global.transaction_isolation
mysql> SELECT @@global.transaction_isolation; --查看全局隔离级别
+-------------------------------+
| @@global.transaction_isolation |
+-------------------------------+
| REPEATABLE-READ |
+-------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 查看会话(当前)隔离级别,替换 @@session.tx_isolation 为 @@session.transaction_isolation
mysql> SELECT @@session.transaction_isolation; --查看会话(当前)隔离级别
+--------------------------------+
| @@session.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 默认同上,替换 @@tx_isolation 为 @@transaction_isolation
mysql> SELECT @@transaction_isolation; --默认同上
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
-- 设置
-- 设置当前会话 or 全局隔离级别语法,语法本身无 tx_isolation ,无需替换,保持原逻辑
-- SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
-- 设置当前会话隔离性,另起一个会话,看不多,只影响当前会话,语法本身无 tx_isolation ,无需替换,保持原逻辑
mysql> set session transaction isolation level serializable; -- 串行化
Query OK, 0 rows affected (0.00 sec)
验证会话隔离级别设置效果
-- 设置当前会话隔离性,另起一个会话,看不多,只影响当前会话,语法本身无 tx_isolation ,无需替换,保持原逻辑
mysql> set session transaction isolation level serializable; -- 串行化
Query OK, 0 rows affected (0.00 sec)
-- 查看全局隔离级别,替换 @@global.tx_isolation 为 @@global.transaction_isolation,全局隔离性还是RR
mysql> SELECT @@global.transaction_isolation; --全局隔离性还是RR
+-------------------------------+
| @@global.transaction_isolation |
+-------------------------------+
| REPEATABLE-READ |
+-------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 查看会话隔离级别,替换 @@session.tx_isolation 为 @@session.transaction_isolation,会话隔离性成为串行化
mysql> SELECT @@session.transaction_isolation; --会话隔离性成为串行化
+--------------------------------+
| @@session.transaction_isolation |
+--------------------------------+
| SERIALIZABLE |
+--------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 默认同上,替换 @@tx_isolation 为 @@transaction_isolation
mysql> SELECT @@transaction_isolation; --同上
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| SERIALIZABLE |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
设置全局隔离级别及验证
-- 设置全局隔离性,另起一个会话,会被影响,语法本身无 tx_isolation ,无需替换,保持原逻辑
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
-- 查看全局隔离级别,替换 @@global.tx_isolation 为 @@global.transaction_isolation
mysql> SELECT @@global.transaction_isolation;
+-------------------------------+
| @@global.transaction_isolation |
+-------------------------------+
| READ-UNCOMMITTED |
+-------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 查看会话隔离级别,替换 @@session.tx_isolation 为 @@session.transaction_isolation
mysql> SELECT @@session.transaction_isolation;
+--------------------------------+
| @@session.transaction_isolation |
+--------------------------------+
| READ-UNCOMMITTED |
+--------------------------------+
1 row in set, 1 warning (0.00 sec)
-- 默认同上,替换 @@tx_isolation 为 @@transaction_isolation
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set, 1 warning (0.00 sec)
-- 注意,如果没有现象,关闭mysql客户端,重新连接。
🌠 读未提交【Read Uncommitted】
--几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用
--终端A
-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
--重启客户端
mysql> select @;
+-------------------------+
| @ |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
mysql> select * from account;
ERROR 1046 (3D000): No database selected
mysql> select * from account;^C
mysql> use affairs;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=123.0 where id=1; --更新指定行
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql>
--没有commit哦!!!
mysql> select * from account; -读到终端A更新但是未commit的数据[insert,delete同样]
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
--一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
(dirty read)
--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql>
🌠 读提交【Read Committed】
环境准备(终端 A 操作)
-- 设置全局事务隔离级别为读已提交(read committed)
set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
-- 重启客户端后,查看 account 表初始数据
select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 手动开启事务,后续同步终端 B 事务操作
begin;
Query OK, 0 rows affected (0.00 sec)
-- 更新张三的账户余额(事务内操作,未提交)
update account set blance=321.0 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 提交事务,使更新对其他会话可见
commit;
Query OK, 0 rows affected (0.01 sec)
终端 B 验证“不可重复读”现象
-- 手动开启事务,与终端 A 事务前后执行
begin;
Query OK, 0 rows affected (0.00 sec)
-- 第一次查询(终端 A 未 commit 时),读取旧值
select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 终端 A commit 后,再次查询(同一事务内),读取新值
-- 同一事务内,相同查询在不同时间得到不同结果,即“不可重复读(non repeatable read)”
select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
🌠可重复读【Repeatable Read】
以下是保持内容不变的分段整理,按操作场景和终端区分,便于阅读:
一、可重复读(REPEATABLE-READ)隔离级别下的更新操作演示
终端A操作
-- 设置全局隔离级别为可重复读(RR)
mysql> set global transaction isolation level repeatable read; -- 设置全局隔离级别RR
Query OK, 0 rows affected (0.01 sec)
-- 关闭终端重启
-- 查看当前隔离级别,确认已设置为RR
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | -- 隔离级别RR
+-----------------+
1 row in set, 1 warning (0.00 sec)
-- 查看account表当前数据
mysql> select *from account; -- 查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 开启事务(终端B同步开启事务)
mysql> begin; -- 开启事务,同步的,终端B也开始事务
Query OK, 0 rows affected (0.00 sec)
-- 更新id=1的记录
mysql> update account set blance=4321.0 where id=1; -- 更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
-- 切换到终端B,查看另一个事务是否能看到
-- 提交事务
mysql> commit; -- 提交事务
Query OK, 0 rows affected (0.00 sec)
-- 切换终端到终端B,查看数据。
终端B操作
-- 开启事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- 终端A事务提交前,查看数据(未更新)
mysql> select * from account; -- 终端A中事务 commit之前,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 终端A事务提交后,查看数据(仍未更新)
mysql> select * from account; -- 终端A中事务 commit 之后,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 可重复读特性说明
-- 可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
-- 结束事务
mysql> commit; -- 结束事务
Query OK, 0 rows affected (0.00 sec)
-- 事务结束后,查看最新数据
mysql> select * from account; -- 再次查看,看到最新的更新数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
二、可重复读(REPEATABLE-READ)隔离级别下的插入操作与幻读问题
--------------------------------------------------------------------------------
-- 如果将上面的终端A中的update操作,改成insert操作,会有什么问题??
终端A操作
-- 查看account表当前数据
mysql> select *from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 开启事务(终端B同步开启)
mysql> begin; -- 开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
-- 插入新记录
mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)
-- 切换到终端B,查看另一个事务是否能看到
-- 提交事务
mysql> commit; -- 提交事务
Query OK, 0 rows affected (0.00 sec)
-- 切换终端到终端B,查看数据。
-- 终端A提交后,自身查看数据(已包含新记录)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
终端B操作
-- 开启事务
mysql> begin; -- 开启事务
Query OK, 0 rows affected (0.00 sec)
-- 终端A提交前,查看数据(无新记录)
mysql> select * from account; -- 终端A commit前 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 终端A提交后,查看数据(仍无新记录)
mysql> select * from account; -- 终端A commit后 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 多次查看及幻读问题说明
mysql> select * from account; -- 多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-key锁(GAP+行锁)解决的。这块比较难,有兴趣同学了解一下)。
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
-- 结束事务
mysql> commit; -- 结束事务
Query OK, 0 rows affected (0.00 sec)
-- 事务结束后,查看最新数据(包含新记录)
mysql> select * from account; -- 看到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
🌠串行化【serializable】
–对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
--终端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @;
+-----------------+
| @ |
+-----------------+
| SERIALIZABLE |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> update account set blance=1.00 where id=1; --终端A中有更新或者其他操作,会阻塞。直到终端B事务提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> commit; --提交之后,终端A中的update才会提交。
Query OK, 0 rows affected (0.00 sec)
总结:
- 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一样
- 说明:mysq1默认的隔离级别是可重复读,━般情况下不要修改
- 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。
一致性(Consistency) - 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
- 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
- 而技术上,通过AID保证C
🌉脏读(Dirty Read)
- 定义:在一个事务中读取到了另一个事务未提交的数据 。简单来说,就是事务A读取了事务B已经修改但尚未提交的数据,之后若事务B回滚,那么事务A读取到的数据就是无效的、“脏”的。
- 示例:假设有两个事务,事务T1和事务T2, 表
account
中有一条记录,id = 1
,balance
(余额)为100元 。- 事务T1开启,执行
UPDATE account SET balance = 200 WHERE id = 1;
,此时balance
在事务T1内被修改为200,但还未提交。 - 事务T2开启,执行
SELECT balance FROM account WHERE id = 1;
,读取到的balance
值是200。 - 接着事务T1发生错误回滚,
balance
恢复为100。但事务T2之前读取到的却是200,这就是脏读,T2读取到了未最终确定的数据。
- 事务T1开启,执行
- 影响: 脏读可能导致数据处理错误,比如在金融系统中,基于脏读的数据进行转账计算,会导致账户金额计算错误,破坏数据的一致性和准确性。
🌉幻读(Phantom Read)
- 定义:在一个事务中,多次执行相同的查询,在事务执行期间,由于其他事务插入了满足查询条件的新数据 ,导致每次查询的结果集不一样,就好像产生了幻觉,出现了“幻影”数据。幻读主要针对的是插入操作带来的数据不一致问题。
- 示例:还是以
account
表为例,假设表中有两条记录,id
分别为1和2 。- 事务T1开启,执行
SELECT * FROM account WHERE id > 0;
,得到包含两条记录的结果集。 - 事务T2开启并执行
INSERT INTO account (id, balance) VALUES (3, 300);
,然后提交事务。 - 事务T1再次执行
SELECT * FROM account WHERE id > 0;
,发现结果集中多了一条id = 3
的记录,与第一次查询结果不同,这就是幻读现象。
- 事务T1开启,执行
- 影响:幻读会破坏事务的隔离性和一致性,例如在库存管理中,事务A查询出库存数量后准备进行修改,事务B插入了新的库存记录,事务A再次查询时库存数量发生变化,可能导致超卖等错误情况。
幻读:简单来说:
会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查这类问题,会查找出全新的记录,就如同产生了幻觉,这种现象,叫做幻读(phantom read)。
- 本质:
同一事务内,多次查询相同条件,第一次查询到N
条记录,后续查询却因其他事务插入数据,多出了新的记录(这些记录符合查询条件,但之前不存在),仿佛“幻觉般出现”,这就是幻读。
MySQL 的特殊处理:RR 级别解决幻读
很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-key锁(GAP+行锁)解决的。这块比较难,有兴趣同学了解一下)。
MySQL 的与众不同:
多数数据库(如 Oracle )的 RR 级别无法解决幻读,但 MySQL 的 RR 级别通过Next-key Lock
(临键锁,包含“行锁 + 间隙锁” )解决了幻读。Next-key Lock 原理:
- 行锁:锁住已有记录,防止修改。
- 间隙锁(GAP Lock):锁住“不存在的区间”,比如表中
id
是1、3
,间隙锁会锁住(1,3)
区间,防止其他事务插入id=2
的数据。 - 结合两者,MySQL 在 RR 级别下,能阻止其他事务插入“符合查询条件”的新数据,从而避免幻读。
推荐阅读
https://www.jianshu.com/p/398d788e1083
https://tech.meituan.com/2014/08/20/innodb-lock.html
https://www.cnblogs.com/aspirant/p/9177978.html
备注:
基本上,了解了上面的知识,在MySQL事务使用上,肯定没有问题。不过,这块设计很优秀,也是面试中可能
被问到的,一般学生,如果能说出上面的内容,就已经不错了。但是如果我们能更详细,更深入的谈论这个问
题,那么对我们的面试与学习肯定是大大的有帮助。
不过接下来的内容,会比较难一些,听的不明白,也没有太大问题。
—在RR级别的时候,多个事务的update,多个事务的insert,多个事
务的delete,是否会有加锁现象。
现象结果是,update,insert,delete之间是会有加锁现象的,但是select和这些操作是不冲突的。这就
是通过读写锁(锁有行锁或者表锁)+MVCC完成隔离性。