详解Mysql 事务

学习一个东西,还是先从其概念开始,首先先看看什么是事务?

数据库事务是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行。

从这个概念看,事务就是一组SQL,而且这组SQL要么全部执行,要么全部不执行。

那么第二个常见的问题,事务有哪些特性(点)呢?这是一个很常见的考题。

事务的四大特性:A(Atomicity)C(Consistency)I(Isolation)D(Durability),如何理解这四个特性呢?

1.原子性(Atomicity):事务开始后的所有操作,要么全部做完,要么全部不做,不可能做一半的情况,如果事务执行过程中出错,会回滚到事务开始前的状态,也就是事务是一个不可分割的整体。

2.一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,A扣了钱,B就一定会收到,不存在A这边减了,B那边没有加的情况。

3.隔离性(Isolation):同时发生的事务(并发事务)不应该导致数据库处于不一致的状态中。系统中每个事务都应该像唯一事务一样执行。任何事务都不应该影响其他事务的存在。假如A向B转账的同时,A完全转移所有的钱,两个事务应该独立进行,在进行转账前要确认好余额。关于隔离性,后面会详细介绍。

4.持久性(Durability)事务完成后,事务对数据库的所有更新将被保存到数据库(磁盘),不能回滚,也可以理解为:无论db或系统是否故障,数据都会永久保存在磁盘上,不会丢。

那Mysql是否支持事务,Mysql有多种存储引擎,包括MyISAM, Innodb, Memory, Merge等。这其中,Innodb和BDB是支持事务的,MyISAM不支持事务。

Mysql如何执行事务呢?

1.启动事务

mysql> start transaction;
或者
mysql> begin;

2.执行你希望在事务中运行的sql语句

3.执行commit语句,完成事务并提交数据

示例,假设有一张表student:

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  28 |
|  3 | lisi     |  28 |
+----+----------+-----+

我们来执行一个事务,将zhangsan的年龄改成22,具体语句如下:

#会话1
MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> update student set age = 22 where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

此时我们在会话1中没有执行commit,然后我们在另一个会话窗口2查询下id=2的学生记录,年龄仍然是28(尽管在当前事务会话(会话1)中能查到最新数据22),因为前一个会话并没有commit(Mysql默认的事务隔离级别是可重复读),所以我们读的仍然是事务执行完之前的数据。

#会话2
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  28 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

当我将会话窗口1执行commit后,此时会话2的查询结果如下:

#会话1
MySQL [test]> commit;
Query OK, 0 rows affected (0.00 sec)
#会话2
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

以上就是一个事务的执行过程。如果我们想中止这个事务,不提交的话,可以使用rollback:

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> update student set age = 32 where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> select * from student where id = 1;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | clark |  32 |
+----+-------+-----+
1 row in set (0.01 sec)

MySQL [test]> rollback;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> select * from student where id = 1;
+----+-------+-----+
| id | name  | age |
+----+-------+-----+
|  1 | clark |  30 |
+----+-------+-----+
1 row in set (0.00 sec)

聊聊autocommit

默认情况下,autocommit 的状态是ON,这意味着所有单独的语句一旦被执行就会被提交,除非该语句在begin….commit语句块中。查看autocommit值:

MySQL [test]> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

如果autocommit的状态为off,则需要明确发出commit语句来提交事务。 要禁用autocommit,可以用下面命令:

MySQL [test]> set autocommit = 0;

PS: DDL语句,如create, alter, drop语句,都是无法rollback的。

聊完事务的概念及事务的用法之后,关于事务,还有一个很常见的问题:事务的隔离级别,这也是个常规考题。

事务的隔离级别,描述的是两个或多个事务同时发生时,一个事务与其他事务在资源或数据修改方面的隔离程度。事务的隔离级别有4个,分别是读未提交(read uncommitted),读已提交(read committed),可重复读(repeatable read),序列化(serializable)。

前面我们提过 MYSQL默认的事务隔离级别是:可重复读(repeatable read),如何修改隔离级别呢?可以使用set @@transaction_isolation=’xxx’;来修改事务的隔离级别。

那接下来我来分别详细说说四个事务隔离级别:

1.读未提交(read uncommitted):顾名思义,事务可以读取另一个未提交的事务写入的数据,读未提交也被称为脏读(因为它会导致脏读)。

我们举个例子,

假如老板给你发工资,转的金额原本应该是2w,但是不小心手抖输错了数字,输成了2.5w,该钱已打到你的账户,但是事务还没有提交,但是此时,你去查看自己的账户,发现多了5k块,窃喜以为涨工资了,老板及时发现不对,马上回滚了事务,将数字改成了2w,再commit,你就蒙圈了,刚刚看了还是2.5w的,这就出现了脏读

具体查询举例:

#会话1--会话1没有commit
MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  30 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.00 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> update student set age = 29 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
#会话2
MySQL [test]> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> select @@tx_isolation;
+------------------+
| @@tx_isolation   |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  29 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
+----+----------+-----+
3 rows in set (0.01 sec)

在会话2中能看到会话1未提交的事务,这种情况就是读未提交。读未提交会出现脏读(一旦会话1 事务被rollback)

2.读已提交(read committed):当前事务只能读取另一个事务提交的数据,这被称为不可重复读(non-repeatable read),因为它会导致不可重复读。

具体举例:

#会话1
MySQL [test]> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  33 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> update student set age = 25 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> commit;
Query OK, 0 rows affected (0.01 sec)
#会话2
MySQL [test]> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student;  //会话(事务)1--commit前 
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  33 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1--commit后
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

可以注意到,在同一个事务会话2中,相同的select语句获取不同的结果,一次age等于30,一次age等于29。所以出现不可重复读

3.可重复读(repeatable read)

一个事务通过第一条语句只能看到相同的数据,即使另一个事务已提交数据。在同一个事务中,读取通过第一次读取建立快照是一致的。一个例外,一个事务可以读取在同一个事务中更改的数据。

当事务开始并执行第一次读取数据时,将创建读取视图并保持打开状态,直到事务结束。为了在事务结束之前提供相同的结果集,InnoDB使用行版本控制和UNDO信息。假设事务1选择了几行,另一个事务删除了这些行并提交了数据。如果事务1处于打开状态,它应该能够看到自己在开始时选择的行。已被删除的行保留在 UNDO 日志空间中以履行事务1。一旦事务1操作完成,那些行便被标记为从UNDO日志中删除。这称为多版本并发控制(MVCC)。

具体举例:

#会话1
MySQL [test]> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> 
MySQL [test]> 
MySQL [test]> 
MySQL [test]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [test]> select * from student;
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> update student set age = 27 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [test]> commit;
Query OK, 0 rows affected (0.00 sec)
#会话2
MySQL [test]> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> begin;
Query OK, 0 rows affected (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1 commit 前
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

MySQL [test]> select * from student; //会话(事务)1 commit 后
+----+----------+-----+
| id | name     | age |
+----+----------+-----+
|  1 | clark    |  25 |
|  2 | zhangsan |  22 |
|  3 | lisi     |  28 |
|  0 | wangwu   |  45 |
|  0 | zhaoliu  |  22 |
+----+----------+-----+
5 rows in set (0.01 sec)

可重复读保证了在一个事务中,无论怎么查,结果都是一致,不受其他事务是否commit影响。从上例可以看出,事务1提交前后,事务2的查询结果都不变。

这就是我们前面说的:当事务开始并执行第一次读取数据时,将创建读取视图并保持打开状态,直到事务结束。为了在事务结束之前提供相同的结果集。

从上面结果看,可重复读会产生幻读。那上面看下来,脏读,不可重复读,幻读有点像。那区别在哪呢?

脏读是指一个事务取到了其他事务没有提交的数据,
不可重复读是指一个事务内多次根据同一个查询条件查询出来的同一行记录的值不一样
幻读是指一个事务内多次根据同个条件查出来的记录行数不一样

大家弄懂了几个的区别吗?如果没懂,可以细细品上述加粗字体

由于可重复读会产生幻读,于是就有第四个事务隔离级别:序列/串行化(serializable)

通过把选定的所有行锁起来,序列化可以提供最高级别的隔离。此级别与REPEATABLE READ类似,但如果禁用autocommit,则InnoDB会将所有普通SELECT语句隐式转换为SELECT…LOCKIN SHARE MODE;如果启用autocommit,则SELECT就是它自己的事务,并且序列化会等待被锁的行,总是读取最新提交的数据。

串行化,顾名思义,是将所有读写操作完全串行。
串行化是所有隔离级别中最高的
每次读都需要获得表级共享锁,读写相互都会阻塞
串行化对资源的开销大,对并发支持不好,只在,某些场景下使用。

具体例子就不细列了,可以自己google

那么上面讲了这么多,现在来总结一下,下面是各个隔离级别的对照表:

隔离级别脏读不可重复读幻读
Read Uncommited可能可能可能
Read Commited不可能可能可能
Repeatable Read不可能不可能可能
Serializable不可能不可能不可能