一、mysql事务的4个特性(ACID)
原子性(A):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(C):一致性是指数据处于一种语义上的有意义且正确的状态。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其他事务不可见的。因为这些中间状态,是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。
隔离性(I): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性(D): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
二、4个特性的理解
假设一个场景:小明向小强转账10元钱
原子性:转账操作是一个不可分割的操作,要么转失败,要么转成功,不能存在中间的状态,也就是转了一半的情况。我们把这种要么全做要么全不做的规则称之为原子性。
隔离性:
1.比如小明向小强转账10元钱;
2.小明向小红转帐10元钱;
隔离性表示上面两个操作是不能相互影响的。
一致性:对于上面的转账场景,一致性表示一次转账完成后,都需要保证整个系统的余额等于所有账户的收入减去所有账户的支出。
如果不遵循原子性,也就是如果小明向小强转账10元,但是只转了一半,小明账户少了10元,小强的账户没有增加,所以没有满足一致性了。
同样,不满足隔离性也可能导致破坏一致性。
所以说,数据库某些操作的原子性和隔离性也是保证一致性的一种手段,在操作执行完成后保证符合所有既定的约束则是一种结果。
持久性:对于转账的交易记录,需要永久保存。
三、事务的概念
我们把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称之为事务。
四、一些概念:
事务自动提交:
查看mysql是否开启了事务自动提交:
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
实际上一个完整的事务操作包括开启事务,数据操作和事务提交3个操作,开启自动提交,只需要进行数据库操作不用我们自己执行开启事务和事务提交的操作;
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 99 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update student set age = 10 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
注意:我们执行到这里并没有执行事务提交操作,我们这时用另外一个客户端看一下数据:并没有改变(自己查询肯定是修改后的数据)。
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 99 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
我们在第一个客户端中执行事务提交操作后,再查看数据就变化了。
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 10 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
当然我们在事务没有提交的时候,也可以执行回滚操作rollback;
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 11 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update student set age = 20 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 20 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 11 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
| 10 | 白骨精 | 23 |
| 11 | 大王 | 33 |
| 12 | 小王 | 12 |
| 13 | 孙尚香 | 25 |
| 14 | 鲁班 | 11 |
| 15 | 大鲁班 | 444 |
| 16 | 李白 | 111 |
+----+-----------+------+
13 rows in set (0.00 sec)
还有隐式提交:定义或修改数据库对象的数据定义语言的(DDL操作)还有一些针对数据库、表、视图、存储过程等当我们使用create、alter、drop等语句去修改这些所谓的数据库对象时,就会隐式提交前面语句所属的事务。
还有保存点操作等一些概念不详细介绍。
五、隔离级别
- 读未提交(READ UNCOMMITTED)
- 读已提交(READ COMMITTED)
- 可重复读(REPEATABLE READ)
- 串行化(SERIALIZABLE)
读未提交:A事务修改了数据,还没有提交时候,B事务就可读取到A事务修改后的数据。(这种隔离级别会出现脏读)
mysql> set session transaction isolation level read uncommitted; //A事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 11 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
+----+-----------+------+
6 rows in set (0.00 sec)
mysql> begin;
mysql> update student set age = 12 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from student; //B事务
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 12 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
+----+-----------+------+
6 rows in set (0.00 sec)
读已提交:A事务修改了数据,B事务在A事务没有提交时候看到的还是A修改前的数据,等A事务提交后,B事务才可以看到A事务修改后的数据。(这种隔离级别会出现不可重复读)
mysql> set session transaction isolation level read committed; //A事务
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 13 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
+----+-----------+------+
6 rows in set (0.00 sec)
mysql> update student set age = 14 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from student; //B事务
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 13 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
+----+-----------+------+
6 rows in set (0.00 sec)
mysql> commit; //A事务
Query OK, 0 rows affected (0.01 sec)
mysql> select * from student; //B事务
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 14 |
| 4 | 小红 | 16 |
| 5 | 张六 | 32 |
| 7 | 孙悟空 | 111 |
| 8 | 猪八戒 | 12 |
| 9 | 唐增 | 333 |
+----+-----------+------+
6 rows in set (0.00 sec)
可重复读:
当事务A读取了某行的数据后,事务B修改了这行的数据并且也提交了,这是事务A再去读这行的数据仍然是第一次读到的数据,而不是每次都读到不同的数据,这就是可重复读这种隔离级别解决了不可重复读,但是还是会出现幻读(mysql利用mvvc多版本控制解决了这个问题)。
mysql> set session transaction isolation level REPEATABLE READ;
mysql> begin; //A事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student;
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 38 |
| 4 | 小红 | 35 |
| 5 | 张六 | 35 |
| 7 | 孙悟空 | 35 |
| 8 | 猪八戒 | 35 |
| 9 | 唐增 | 35 |
+----+-----------+------+
6 rows in set (0.00 sec)
mysql> begin; //B事务
Query OK, 0 rows affected (0.00 sec)
mysql> update student set age = 39 where id = 3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from student; //A事务
+----+-----------+------+
| id | name | age |
+----+-----------+------+
| 3 | 小耿 | 38 |
| 4 | 小红 | 35 |
| 5 | 张六 | 35 |
| 7 | 孙悟空 | 35 |
| 8 | 猪八戒 | 35 |
| 9 | 唐增 | 35 |
+----+-----------+------+
6 rows in set (0.00 sec)
串行化:
以上3中隔离级别都允许对同一条记录同时进行读-读,读-写,写-读的操作,如果我们不允许读-写,写-读的并发操作,可以使用SERIALIZABLE隔离级别,这种隔离级别因为对同一条记录都时串行的,所以不会出现脏读,幻读等现象。
六、隔离级别的实现
版本链
对于使用InnoDB的存储引擎的表来说,它的聚簇索引中都包含2个必要的隐藏列(row_id非必要的,我们的表中有主键或者非null的唯一键时都不会包row_id。):
transcation_id(事务id): 每次对数据进行修改时,都会把对应的事务id赋值给trx_id隐藏列。
roll_pointer(回滚指针):每次对每条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到修改前的记录。
ReadView
对于使用READ UNCOMMITTED隔离级别的事务来说,直接取记录的最新版本就好;
对于使用SERIALIZABLE隔离级别的事务来说,使用锁的方式来访问记录。
对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说就需要用到上面所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。
读已提交隔离级别:
- 一行原始数据是1,1,2,事务id是80的持久化数据执行下面操作:
- 上图中的一行数据被多个事务修改(未提交),每个事务修改数据后都会保存新的数据行到一个版本链当中去如上图的结构并且会保存一个事务id到行数据中。这样每个事务就可以根据事务id读到自己修改的数据。
- 当一个新的事务比如50要读取这行数据的时候,会生成一个ReadView,它里面有一个属性是m_ids数组,这个数组中保存的是活跃的事务id即未提交的事务id和当前事务自己的事务id即50,在我们这个例子中就是数组m_ids[120,110,100,90,50],当前事务就会去版本链中找包含120,110,100,90的都不是,剩下一个80的就是已经提交的持久化数据,就找到了这个数据。
- 如果这个时候,有一个事务提交了,比如120的事务提交了,那么事务50再去查的时候,在生成的readview中m_ids数组中就没有了120这个元素变成了m_ids[110,100,90,50],当读到事务id是120的这个版本时,发现在m_ids数组中没有这个事务id,就找了这个数据返回。(注意如果多个事务都提交了,最后提交的版本会放到最上面,第一次就可以读到。)
可重复读的隔离级别:
可重复读的隔离级别和读已提交的隔离级别的差异就在于上面的第4步,这个时候如果有事务提交了,事务50再去查询的时候,还会用第一次生成的readview去做判断也就是说在m_ids数组中已经提交的事务id还保存在m_ids中,再去找数据找到的还是第一次一样的结果。
七、MVCC总结
MVCC(多版本并发控制)指的就是在使用 READ COMMITTED和REPEATABLE READ隔离级别的事务在执行select操作访问记录的版本链的过程。可以使不同事务的读-写,写-读操作并发执行,从而提升系统性能。
READ COMMITTED和REPEATABLE READ隔离级别最大的不同就是:生成ReadView的时机不同,READ COMMITTED在每一次进行普通的select的操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通的select的操作前生成一个ReadView。之后的查询操作都复用这个ReadView就好了。