参考于:
mp.weixin.qq.com/s/tNA_-_MoYt1fJT0icyKbMg
www.imooc.com/article/17290#
- 事务就是一系列原子性的SQL查询;是一个独立的工作单元。
- 如果数据库引擎能够成功的对数据库应用该组查询的全部查询(SQL没有报错), 就可以执行该组查询。
- 如果该组查询中有其中一条失败、崩溃、或其他原因导致SQL无法执行,那么所有语句都不会执行。
- 也就是说, 事务内的语句, 要么全部执行成功, 要么全部执行失败。
事务的用法十分简单(3个语句):
START TRANSACTION;//开启事务
sql 1;
sql 2;
ROLLBACK
sql 3;
COMMIT; //提交事务
开启一个事务,一组事务开启的标识语句。
回滚,把数据状态回滚成事务开启之前的状态。
如:
表结构(下面测试也使用此表):
mysql> desc test;
+--------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| num | int(10) | NO | | 0 | |
| data | varchar(20) | NO | | 0 | |
| status | tinyint(3) | NO | | 1 | |
+--------+------------------+------+-----+---------+----------------+
mysql> select * from test;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 12 | 123 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> start transaction;//开启一个事务
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 13 where id = 1;//更新
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0//提示数据已经改动
mysql> rollback;//回滚
Query OK, 0 rows affected (0.05 sec)
mysql> select * from test where id = 1;//可以看到已经被回滚到原来状态
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 12 | 123 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
提交一个事务,事务中的改变的数据状态将会被永久更改。
如:
mysql> select * from test where id = 1;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 12 | 123 | 1 |
+----+-----+------+--------+
mysql> start transaction;//开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 13 where id = 1;//事务中的更新操作
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update test set data = '测试' where id = 1;//事务中的更新操作
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;//提交事务
Query OK, 0 rows affected (0.05 sec)
mysql> select * from test;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
一个具备运行良好的事务处理系统, 必须具备这些标准特征————ACID。
A (atomicity) 原子性
所谓原子性, 就是一个事务就是一个不可分割的整体, 要么一次性全部成功, 要么全部失败 这就是事务的原子性。C(consistency)一致性
所谓一致性, 可以这样理解 A账户要给B账户100 那么A账户少100元B必定要增加100元,它可以说是跟原子性相互互补的一个东西。I (isolation) 隔离性
所谓隔离性, 就是当一个事务没有commit的时候,在这事务所做的数据修改在另一个事务中是不可见的。如在一个事务中修改了一个字段
的数据, 但在另一条事务这个字段的值还是未修改之前的值。但这只是通常情况下, 关系型数据库会存在隔离级别的功能, 不同隔离级别
下,需要做不用的讨论。D (durability) 持久性
一旦事务提交,则其所做的修改就会永久保存到数据库中。
查看当前会话隔离级别 select @@tx_isolation;
查看系统当前隔离级别 select @@global.tx_isolation;
设置当前会话隔离级别 set session transaction isolation level REPEATABLE READ/ READ UNCOMMITTED/ repeatable read/;
设置系统当前隔离级别 set global transaction isolation level REPEATABLE READ/ READ UNCOMMITTED/ repeatable read/;
READ UNCOMMITTED级别:事务中的修改, 即使没有提交, 对其他事务也都是可见的。事务可以读取未提交的数据, 这也被称为脏读(Dirty Read)。这个级别会导致很多问题,性能也不会比其他级别高多少,非特别情况不会使用到此级别。
测试脏读
窗口1:
mysql> start transaction;//开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.01 sec)
mysql> update test set num = 2 where id = 1; //在事务中把num更新为2。--步骤1
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test where id = 1;//可以看到在此条记录已被成功更新, 之后被窗口2查询-对应窗口2第一条查询。--步骤2
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 2 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> rollback; //回滚。记录被回滚原来的状态, 步骤3时查询的数据为脏数据--步骤4
Query OK, 0 rows affected (0.03 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
//查询1
mysql> select * from test where id = 1; //数据一致, 这条就成为了脏数据。--步骤3
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 2 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
//之后不再查询。
说明:可以看到当事务1中未COMMIT之前的更新操作也会影响到事务2中的查询, 查出来的数据我们称之为脏数据, 这种现象称之为脏读。
测试不可重复读
窗口1:
mysql> start transaction;//开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.01 sec)
mysql> update test set num = 2 where id = 1; //在事务中把num更新为2,之后窗口2查看此条记录-对应第一条查询。--步骤1
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update test set num = 3 where id = 1; //在事务中把num更新为3,之后窗口2查看此条记录-对应第二条查询。--步骤3
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
//查询1
mysql> select * from test where id = 1; --步骤2
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 2 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
//查询2
mysql> select * from test where id = 1; --步骤4
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 3 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
说明:不可重复读的重点是修改, 可以看到窗口2中同样的查询确查出了不一样的结果,所以我们称为不可重复读。
测试幻读
窗口1:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test where data = '测试'; //符合此条件下的记录有一条, 之后窗口2做insert 操作 ---步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.08 sec)
mysql> select * from test where data = '测试'; //符合此条件的记录有三条, 之后窗口2做rollback 操作 ---步骤3
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
| 2 | 0 | 测试 | 1 |
| 3 | 0 | 测试 | 1 |
+----+-----+------+--------+
3 rows in set (0.00 sec)
mysql> select * from test where data = '测试';//符合此条件下的记录有一条 ---步骤5
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test (data) values('测试'), ('测试'); ---步骤2
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0
rollback; ---步骤4
说明:幻读的重点是写, 可以看到 3次相同的查询 其中2次查询会多出2条记录, 多出来的记录我们称之为幻行, 这种现象我们称之为幻读。
READ COMMITTED级别 : 大多数数据库隔离机制使用此模式(MYSQL例外), 此级别比较符合前面定义I (isolation) 隔离性,一个事务开始时,(2条事务查询同一条记录)查看到的数据只会是commit之后的数据,换句话说, 一个事务从开始到提交前, 所做的任何操作对其他事务是不可见的, 这个级别有时候也叫做不可重复读(nonrepeattableread), 因为执行2次相同的查询会得到不相同的数据。
测试脏读
窗口1:
mysql> select * from test where id = 1; //先查看此条件下的记录 --步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.02 sec)
mysql> select * from test where id = 1; //再次查看此条件下的记录,级别一脏读的情况不会发生 --步骤3
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.02 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 11 where id = 1; //更新此条记录 --步骤2
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
说明:可以看到在此模式下,事务2的数据操作对事务1是不可见的,脏读的情况不会发生。
测试不可重复读
窗口1:
mysql> select * from test where id = 1; 先查看此条记录。 --步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 1 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> select * from test where id = 1; 再次查看记录。 --步骤4
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 11 where id = 1; 更新。 --步骤2
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; 提交。--步骤3
Query OK, 0 rows affected (0.07 sec)
说明:可以看到此模式下,不可重复读的情况还是会发生(窗口1同样条件还是会查出不同的数据)。
测试幻读
窗口1:
mysql> select * from test where data = '测试'; //此条件下的记录, --步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
| 6 | 0 | 测试 | 1 |
| 7 | 0 | 测试 | 1 |
| 8 | 0 | 测试 | 1 |
| 9 | 0 | 测试 | 1 |
| 14 | 0 | 测试 | 1 |
| 15 | 0 | 测试 | 1 |
+----+-----+------+--------+
7 rows in set (0.12 sec)
mysql> update test set num = 11 where data ='测试'; //更新次条件下的记录,可以看到6条匹配 --步骤3
Query OK, 6 rows affected (0.00 sec)
Rows matched: 7 Changed: 6 Warnings: 0
mysql> select * from test where data = '测试'; //再次按相同条件查找,发现多了2条记录。 --步骤5
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
| 6 | 11 | 测试 | 1 |
| 7 | 11 | 测试 | 1 |
| 8 | 11 | 测试 | 1 |
| 9 | 11 | 测试 | 1 |
| 14 | 11 | 测试 | 1 |
| 15 | 11 | 测试 | 1 |
| 16 | 0 | 测试 | 1 |
| 17 | 0 | 测试 | 1 |
+----+-----+------+--------+
9 rows in set (0.00 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into test (data) values ('测试'),('测试');//插入2条记录,未提交 --步骤2
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> commit;//提交 --步骤4
Query OK, 0 rows affected (0.04 sec)
说明:可以看到,明明是7条match, 但是事务2的insert操作导致事务1只更新到7 多出来的2条并没有被更新到(就像幻觉一般), 这也是幻读。
REPEATABLE READ可重复读解决了脏读的问题。该级别保证了在同一个事务中多次读取是的数据是一样的。但是理论上无法解决幻读的问题。上面实验我们可以知道, 只要事务1在2次范围查询得到的记录数不一样,我们就可以称它们为幻读。InnoDB存储引擎通过多版本并发控制(MVCC)解决了幻读的问题。
测试脏读
窗口1:
mysql> select * from test where id = 1; //步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.08 sec)
mysql> select * from test where id = 1; //步骤4
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> select * from test where id = 1;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> update test set num = 12 where id = 1; //步骤2
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> rollback; //步骤3
Query OK, 0 rows affected (0.06 sec)
可以看到此模式下不会发生脏读。
测试不可重复读
窗口1:
mysql> select * from test where id = 1; //步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> select * from test where id = 1;//步骤3
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> select * from test where id = 1; //步骤6
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> select * from test where id = 1;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 11 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> update test set num = 13 where id = 1; //步骤2
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit; //步骤4
Query OK, 0 rows affected (0.05 sec)
mysql> select * from test where id = 1; //步骤5
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
可以看到不管提交前还是提交后窗口1获取的数据都是一样的。
测试幻读
窗口1:
mysql> select count(*) from test where data = '测试'; //步骤1
+----------+
| count(*) |
+----------+
| 9 |
+----------+
1 row in set (0.04 sec)
mysql> select count(*) from test where data = '测试'; //步骤3
+----------+
| count(*) |
+----------+
| 9 |
+----------+
1 row in set (0.00 sec)
mysql> select count(*) from test where data = '测试'; //步骤6
+----------+
| count(*) |
+----------+
| 9 |
+----------+
1 row in set (0.00 sec)
窗口2:
mysql> insert into test (data) values('测试'), ('测试'); //步骤2
Query OK, 2 rows affected (0.02 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> commit; //步骤4
Query OK, 0 rows affected (0.04 sec)
mysql> select count(*) from test where data = '测试'; //步骤5
+----------+
| count(*) |
+----------+
| 11 |
+----------+
1 row in set (0.00 sec)
可以看到不管提交前还是提交后窗口1获取的记录条数都是一样的。
补充:mysql中这个隔离级别下默认使用了MVCC解决了幻读问题。
SERIALIZABLE可串行化是最高的隔离级别。它通过强制事务串行执行, 避免了前面说的幻读的问题。简单来说SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题, 只有要求数据一致性特别高和接受没有并发的情况下才使用此模式。
测试脏读
窗口1:
mysql> select * from test where id = 1;//步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 20 where id = 1; //步骤2
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到窗口2执行update语句后不能成功。
测试不可重复读
窗口1:
mysql> select * from test where id = 1;//步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
窗口2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num = 20 where id = 1; //步骤2
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到窗口2执行update语句后也不能成功。
测试幻读
窗口1:
mysql> select * from test where id = 1; //步骤1
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.08 sec)
mysql> select * from test where data ='测试'; //步骤3
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
窗口2:
mysql> select * from test where id = 1;
+----+-----+------+--------+
| id | num | data | status |
+----+-----+------+--------+
| 1 | 13 | 测试 | 1 |
+----+-----+------+--------+
1 row in set (0.00 sec)
mysql> insert into test (data) values ('测试'),('测试'); //步骤2
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
可以看到窗口2执行insert语句后 窗口1执行查询语句不能成功。
书上所说, MVCC的实现, 是通过保存数据在某个时间点的快照来实现的。也就是说, 不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同, 每个事务对同一张表, 同一时刻看到的数据都是一致的。根据事务开始的时间不同, 每个事务对同一张表, 同一个时刻看到的数据可能是不一样的。
InnoDB的MVCC, 是通过在每行记录后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存行的过期时间值, 但是存储的并不是实际的时间戳, 而是系统版本号。每开始一个新的事务, 系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号, 用来和查询到的每一行记录的版本号进行比较
SELECT
InnoDB会根据以下两个条件检查每行记录:
a:InnoDB只查找版本早于单前面事务版本的数据行(也就是, 行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行, 要么是在事务开始前已经存在, 要么是事务自身插入或者修改过的。
b:行的删除版本要么未定义, 要么大于当前版本事务号。这可以确保事务读取到的行, 在事务开始之前未被删除
只有符合上述两个条件的记录, 才能返回作为查询结果。
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
InnoDB为插入一行新记录, 保存单前系统版本号作为行版本号, 同时保存当前系统版本号到原来的行作为行删除标识。
相关概念:
过程:
update应该有的步骤
记录先加载到内存中。
用排他锁锁定该行
此时.记录num = 7这条记录到undo log的内存buffer。
在内存中修改此条记录的num = 6。
redo_log 记录num=6这条记录到undo log的内存buffer。
将undo log的buffer写到磁盘。
将redo log的buffer写到磁盘。
事务提交。
undo_log的形成过程
则判断可见性算法:
在innodb中,创建一个新事务的时候,innodb会将当前系统(但不包括这条事务)中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,innodb会将该行当前的版本号与该read view进行比较。
具体的算法如下:1.设该行的当前事务id为 trx_id_0 ,read view中最早的事务id为 trx_id_1 , 最迟的事务id为 trx_id_2, 事务创建ID** m_creator_trx_id **。
2.如果 trx_id_0 < trx_id_1 的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的, 如果trx_id_0 = m_creator_trx_id, 认为该行由这条事务创建, 所以也是可见的。跳到步骤6.
3.如果 trx_id_0>trx_id_2 的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见.跳到步骤5。
4.如果 trx_id_1<=trx_id_0<=trx_id_2 , 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从 trx_id_1 到 trx_id_2 进行遍历,如果 trx_id_0 等于他们之中的某个事务id的话,那么不可见。跳到步骤5.
5.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号,将它赋值该trx_id_0,然后跳到步骤2.
6.将该可见行的值返回。
可以去查看一下mysql的判断可见性源码,这里不做细表(可参考上边资料)
这里需要说明一点 不同的事务隔离级别,可见性的实现也不一样:
READ-COMMITTED
事务内的每个查询语句都会重新创建Read View,这样就会产生不可重复读现象发生
REPEATABLE-READ
事务内开始时创建Read View , 在事务结束这段时间内 每一次查询都不会重新重建Read View ,从而实现了可重复读。