事务
可以理解为一个 独立的
工作单元, 在这个独立的工作单元中, 有一组操作; 放在事务(独立工作单元)中的多个操作, 要么全部执行成功, 要么全部执行失败。不免俗套, 这还是通过最经典的银行转账
应用来解释一下
假设有两个角色 'Iron Man'(余额500), 'Wolverine'(余额15), 现在 Iron Man
通过该银行应用给 Wolverine
转账100元, 那么本次转账操作至少需要三个步骤:
检查`Iron Man`余额`>=100`元
从`Iron Man`余额中`-100`元
给`Wolverine`余额`+100`元
独立的工作单元
来执行。在这个 独立工作单元
(即事务) 中的这三个操作, 只要有任何一个操作失败, 则事务就整体就是失败的, 那就必须回滚所有的步骤。在分析高并发事务的问题前, 我们要先知道事务的几个标准特性, 因为一个运行良好的事务处理系统必须具备这些标准特性, 而且这些问题的解决离不开事务的这几个标准特性!!!
一个不可分割的最小工作单元
, 整个事务中的所有操作要么全部提交成功, 要么全部失败回滚。对于一个事务来说, 不能只成功执行其中的一部分操作, 这就是事务的原子性。Consistency 一致性
虽然可数据表中的数据可能一直在变化, 但是事务的一致性
特性会保证 数据库总是从一个一致性的状态 转换到 另一个一致性的状态;
比如在之前的转账例子:
转账前的一致性状态是: 'Iron Man'(余额500), 'Wolverine'(余额15)
转账成功后的一致性状态是: 'Iron Man'(余额400), 'Wolverine'(余额115)
转账如果失败的话, 一致性的状态应该回滚到转账前的状态: 'Iron Man'(余额500), 'Wolverine'(余额15)
Isolation 隔离性
-100
的余额才对隔离性
的 隔离级别
, 到时候就知道这里为什么说通常来说
对其他事务是不可见的; (也就是还有特例, 比如最低隔离级别 READ UNCOMMITTED
, 对其他事务的可见就造成了脏读问题
的出现)READ UNCOMMITTED
, READ COMMITTED
, REPEATABLE READ
, SERIALIZABLE
)Durability 持久性
一旦事务被最终提交, 则在事务这个独立单元中的所有操作所做的修改将会 `永久保存到数据库中`; (这里所说的`永久`应该可以理解为 被事务修改的数据 是真正存放到了表中, 而不是存放在了诸如临时表之类的地方)
在并发量比较大的时候, 很容易出现 多个事务同时进行 的情况。假设有两个事务正在同时进行, 值得注意的是: 它们两者之间是互相不知道对方的存在的, 各自都对自身所处的环境过分乐观, 从而并没有对自己所操作的数据做一定的保护处理, 所以最终导致了一些问题的出现;这些问题归结为5类,包括3类数据读问题(脏读、不可重复读、幻象读)以及两类数据更新问题(第一类丢失更新问题和第二类更新丢失问题)
接下来, 在分析高并发事务的问题时, 你可能已经了解了一些关于锁的概念, 但是在分析这些问题的时候, 先不要带入锁的概念, 本小节只会列出问题, 并直接告诉你各个问题是使用事务隔离性的哪个隔离级别解决掉的, 锁是解决方案, 如果带入锁的概念, 是无法去分析这些问题的。所以本节不需要带入锁
!
[下一篇文章]()将会分析这些解决方案(各隔离级别)具体是如何解决问题的。
如果mysql中一个事务A读取了另一个并行事务B未最终提交的写数据, 那事务A的这次读取就是脏读
。(因为事务A读取的是'脏数据', 是'非持久性'的数据)
脏数据
, 因为事务B最终会回滚这个数据, 所以如果事务A使用库存20进行后续的操作, 就会引发问题, 因为事务A拿到的数据已经和表中的真实数据不一致了。那么这个问题如何解决呢?
在MySQL中, 其实事务已经用自身特性(隔离性
的 -- READ COMMITED
或以上隔离级别)解决了这个问题;
**`READ COMMITED`级别保证了, 只要是当前语句执行前已经提交的数据都是可见的**。注意和`REPEATABLE READ`级别的区!!!
脏读问题
已经被完全解决了, 那就意味着事务中每次读取到的数据都是 持久性
的数据(被别的事务最终 提交/回滚 完成后的数据)。隔离性
的 -- REPEATABLE READ
或以上隔离级别)解决了这个问题;REPEATABLE READ
级别保证了, 只要是当前事务执行前已经提交的数据都是可见的。注意和READ COMMITED
级别的区!!!测试 不可重复读,将客户端A的隔离属性设置为read commited 读已提交的,作用域为session当前会话。
B客户端为可重复读repeatable-read
(不可重复读 是针对同一条记录的更改与删除,导致两次查询的数据导致不一致。)
(此实验证明了,事务隔离属性为可重复读的时候,保证了同一个事务的多次查询同条记录是一致的。而事务隔离属性为读已提交的时候,会导致两次读取的数据行记录不一致情况。)
mysql中,默认的事务隔离级别是可重复读(repeatable-read),为了解决不可重复读,innodb采用了mvcc(多版本并发控制)来解决这一问题。
mvcc是利用在每条数据后面加了隐藏的两列(创建版本号和删除版本号),每个事务在开始的时候都会有一个递增的版本号
新增:insert into user (id,name,age)values(1,"张三",10);
id name age create_version delete_version
1 张三 10 1
更新:update user set age = 11 where id = 1;
更新操作采用delete+add的方式来实现,首先将当前数据标志为删除
id name age create_version delete_version
1 张三 10 1 2
然后新增一条新的数据:
id name age create_version delete_version
1 张三 10 1 2
1 张三 11 2
删除:删除操作是直接将数据的删除版本号更新为当前事务的版本号
delete from user where id = 1;
id name age create_version delete_version
1 张三 11 2 3
查询操作:
select * from user where id = 1;
查询操作为了避免查询到旧数据或已经被其他事务更改过的数据,需要满足如下条件:
1、查询时当前事务的版本号需要大于或等于创建版本号
2、查询时当前事务的版本号需要小于删除的版本号
即:create_version <= current_version < delete_version
这样就可以避免查询到其他事务修改的数据
以下是来自《高性能MySQL》摘抄
由于很多人(当然也包括本人), 容易搞混 不可重复读
和 幻读
, 这两者确实非常相似。
不可重复读
主要是说多次读取一条记录, 发现该记录中某些列值被修改过。幻读
主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。幻读
, MySQL的InnoDB引擎默认的RR
级别已经通过MVCC自动帮我们解决了
, 所以该级别下, 你也模拟不出幻读的场景; 退回到 RC
隔离级别的话, 你又容易把幻读
和不可重复读
搞混淆, 所以这可能就是比较头痛的点吧!RR
隔离级别的描述, 理论上RR级别是无法解决幻读的问题, 但是由于InnoDB引擎的RR级别还使用了MVCC, 所以也就避免了幻读的出现!MVCC虽然解决了幻读
问题, 但严格来说只是解决了部分幻读问题, 接下来进行演示:
1.打开客户端1查看隔离级别及初始数据
mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
mysql>
2.打开客户端2查看隔离级别及初始数据
mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
mysql>
3.在客户端2中开启事务, 然后查询数据
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
mysql>
4.在客户端1中插入一条id为4的新数据 (直接自动提交)
mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
| 4 | 死侍 | 18 | 0 | A bad boy |
+----+-----------+-----+--------+--------------------+
4 rows in set (0.00 sec)
mysql>
5.在客户端2事务中再次查询数据, 发现数据没有变化(表示可以重复读, 并且克服了幻读)!! 但是在客户端2事务中插入一条id为4的新数据, 发现提示数据已经存在!!!
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption |
+----+-----------+-----+--------+--------------------+
| 1 | 金刚狼 | 127 | 1 | 我有一双铁爪 |
| 2 | 钢铁侠 | 120 | 1 | 我有一身铁甲 |
| 3 | 绿巨人 | 0 | 2 | 我有一身肉 |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)
mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
1062 - Duplicate entry '4' for key 'PRIMARY'
mysql>
//并且, 此时`update/delete`也是可以操作这条在事务中看不到的记录的!
6.那么这是什么问题呢?
The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
个人认为应该翻译为: 数据库状态的快照适用于事务中的SELECT语句, 而不一定适用于所有DML语句。 如果您插入或修改某些行, 然后提交该事务, 则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句就可能会影响那些刚刚提交的行, 即使该事务无法查询它们。 如果事务更新或删除由不同事务提交的行, 则这些更改对当前事务变得可见。
7.不少资料将MVCC并发控制中的读操作可以分成两类: 快照读 (snapshot read)
与 当前读 (current read)
。
- 快照读, 读取专门的快照 (对于RC,快照(ReadView)会在每个语句中创建。对于RR,快照是在事务启动时创建的)
```
简单的select操作即可(不需要加锁,如: select ... lock in share mode, select ... for update)
```
针对的也是select操作
- 当前读, 读取最新版本的记录, 没有快照。 在InnoDB中,当前读取根本不会创建任何快照。
```
select ... lock in share mode
select ... for update
```
针对如下操作, 会让如下操作阻塞:
```
insert
update
delete
```
- 在RR级别下, 快照读是通过MVVC(多版本控制)和undo log来实现的, 当前读是通过手动加record lock(记录锁)和gap lock(间隙锁)来实现的。所以从上面的显示来看,如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现。
为了模拟出幻读情形,采用read-commited的事务隔离属性
针对幻读,防止读到新增的数据,往往是通过添加表级锁---将整张表锁定,防止新增数据。
高并发事务
的另一个问题 -- 丢失更新问题
, 该问题和之前几个问题需要区分开, 因为解决方案不是一类!Serializable
或者手动使用乐观锁
, 悲观锁
来解决。Serializable
在实际应用场景中并不被采用, 对于手动使用乐观锁
, 悲观锁
的方案, 将会在以后关于锁的文章中一并给出!隔离性
的最高隔离级别SERIALIZABLE
也可以解决幻读
, 但该隔离级别在实际中很少使用!