MySQL——事务管理
1. 事务的概念
1. 事务由一条或者多条SQL语句组成,这些语句具有逻辑关联性,共同完成一个任务。
2. MySQL同一时刻可能存在大量事务,如果不对这些事务加以控制,在执行时就可能会出现问题。比如单个事务内部的某些SQL语句执行失败,或是多个事务同时访问同一份数据导致数据不一致的问题。
因此一个完整的事务并不是简单的SQL集合,事务还需要满足如下四个属性:
1. 原子性:一个事务中的所有操作,要么不做,要么全完成,不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
2. 持久性:事务处理结束后,对数据的修改是永久的,即使系统故障也不会丢失。
3. 隔离性:数据库允许多个事务访问同一份数据,隔离性可以保证多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致。
4. 一致性:在事务开始之前和事务结束以后,数据库的完整型没有被破坏,这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联型以及后续数据库可以自发性地完成预定的工作。
上面的四个属性简称ACID:
- 原子性(Atomicity,又称不可分割性)。
- 一致性(Consistency)。
- 隔离性(Isolation,又称独立性)。
- 持久性(Durability)。
为什么会出现事务?
1. 事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要用户自己去考虑各种各样的潜在错误和并发问题。
2. 如果MySQL只是单纯的提供数据存储服务,那么用户在访问数据库时就需要自行考虑各种潜在问题,包括网络异常、服务器宕机等。因此事务本质是为了应用服务的,而不是伴随着数据库系统天生就有的。
2. 事务的版本支持
通过show engines
命令可以查看数据库引擎。如下:
- Transactions: 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务。
3. 事务的提交方式
事务常见的提交方式有两种,分别是自动提交和手动提交。
通过show命令查看autocommit全局变量,可以查看事务的自动提交是否被打开。如下:
autocommit的值为ON表示自动提交被打开,值为OFF表示自动提交被关闭,即事务的提交方式为手动提交。
设置事务的提交方式
通过set命令设置autocommit全局变量的值,可以打开或关闭事务的自动提交。如下:
将autocommit的值设置为1表示打开自动提交,设置为0表示关闭自动提交,相当于将事务提交方式设置为手动提交。
4. 事务的相关演示
首先我们先来做一下准备工作,为了更好地看到现象,我们要把隔离级别先设置成最低级别,即读未提交;
这里我们设置的是全局隔离级别,当前会话的隔离级别不会改变,只会影响后续与MySQL新建立的连接,因此需要重启终端才能看到会话的隔离级别被成功设置。如下:
创建一个银行用户表,表中包含用户的id、姓名和账户余额。如下:
演示一:事务的常规操作
启动两个终端,左终端使用begin或start transaction命令启动一个事务,右终端查看银行用户表中的信息。如下:
左终端中的事务向表中插入一条记录,由于我们将隔离级别设置成了读未提交,因此在左终端中的事务使用commit提交之前,在右终端中就能查看到事务向表中插入的记录。如下:
左终端中的事务使用savepoint命令创建一个保存点,然后继续向表中插入一条记录,这时在右终端中也能看到新插入的这条记录。如下:
左终端中的事务使用rollback命令回滚到保存点,这时右终端在查看表中数据时就看不到刚才插入的第二条记录了。如下:
左终端中的事务使用rollback命令回滚到事务最开始,这时右终端在查看表中数据时就看不到任何记录了。如下:
说明一下:
1. 使用begin或start transaction命令,可以启动一个事务。
2. 使用“savepoint 保存点”命令,可以在事务中创建指定名称的保存点。
3. 使用”rollback to 保存点“命令,可以让事务回滚到指定保存点。
4. 使用rollback命令,可以直接让事务回滚到最开始。
5. 使用commit命令,可以提交事务,提交事务后就不能回滚了。
演示二:原子性
在左终端中启动一个事务,在右终端查看银行用户表中的信息。如下:
左终端中的事务向表中插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到插入的这条记录。如下:
如果左终端中的事务在提交之前因为某些原因与MySQL断开连接,那么MySQL会自动让事务回滚到最开始,这时右终端中就看不到之前插入的记录了。如下:
这里就是原子性的体现,因为我们没有对事务进行提交,那么就相当于没做这个事务,原子性就是要么不做,要么做完,没有中间状态,如果中间退出,那就相当于没做。
演示三:持久性
在左终端中启动一个事务,在右终端查看银行用户表中的信息。如下:
左终端中的事务向表中插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到插入的这条记录。如下:
左终端中的事务在提交后与MySQL断开连接,这时右终端中仍然可以看到之前插入的记录,因为事务提交后数据就被持久化了。如下:
演示四:begin会自动更改提交方式
通过show命令查看autocommit的值为ON,表示事务的提交方式是自动提交,此时银行用户表中有一条记录。如下:
在左终端中启动一个事务并向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中能够查询到新插入的这条记录。如下:
如果左终端中的事务在提交之前与MySQL断开连接,那么MySQL依旧会自动让事务回滚到最开始,这时右终端中就看不到之前新插入的记录了。如下:
也就是说,使用begin或start transaction命令启动的事务,都必须要使用commit命令手动提交,数据才会被持久化,与是否设置autocommit无关。我们之前一直都在使用单SQL事务,只不过autocommit默认是打开的,因此单SQL事务执行后自动就被提交了。
5. 事务的隔离级别
隔离级别有四种:读提交(Read Uncommitted)、读未提交(Read Committed)、可重复读(Repeatable Read,MySQL默认隔离级别)、串行化(Serializable)
5.1 查看与设置隔离级别
查看全局隔离级别
查看会话隔离级别
说明一下:这里因为我们上面设置了全局隔离级别为RU,所以这里我们查出来的都是RU级别。
设置全局隔离级别
通过”set global transaction isolation level 隔离级别“命令,可以设置全局隔离级别。如下:
说明一下: 设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。
设置会话隔离级别
通过”set session transaction isolation level 隔离级别“命令,可以设置当前会话的隔离级别。如下:
说明一下: 设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。也就是说重启会话后,还是采用全局隔离级别,所以一般情况下,我们直接设置全局隔离级别即可。
5.2 读未提交(Read Uncommitted)
启动两个终端,将隔离级别都设置为读未提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务就已经能够看到了。如下:
说明一下:
1. 读未提交是事务的最低隔离级别,几乎没有加锁,虽然效率高,但是问题比较多,所以严重不建议使用。
2. 一个事务在执行过程中,读取到另一个执行中的事务所做的修改,但是该事务还没有进行提交,这种现象叫做脏读。
5.3 读提交(Read Committed)
启动两个终端,将隔离级别都设置为读提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
只有当左终端中的事务提交后,右终端中的事务才能看到修改后的数据。如下:
说明一下:
一个事务在执行过程中,两个相同的select查询得到了不同的数据,这种现象叫做不可重复读。
5.4 可重复读(Repeatable Read)
启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据。如下:
只有当右终端中的事务提交后再查看表中的数据,这时才能看到修改后的数据。如下:
说明一下:
1. 在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是所谓的可重复读。
2. 一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本是不存在的,因此一般的加锁无法屏蔽这类问题。
3. 一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读。
4. MySQL是通过Next-Key锁(GAP+行锁)来解决幻读问题的。上面图中右边事务提交之前,我们是看不到表的内容发生变化的。
5.5 串行化(Serializable)
启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。如下:
但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞。如下:
直到访问这张表的其他事务都提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作。如下:
这里大家需要注意,右边commit之后,刚才左边阻塞的操作就会立即执行完成,这时我们查看表,发现数据还是没变,这是因为左边还没commit,我们隔离级别是最高级别,所以左边必须commit之后,我们才可以看到改变后的数据。
说明一下:
串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用。
5.6 隔离级别总结
说明一下:
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点。
- 表中只写出了各种隔离级别下进行读操作时是否需要加锁,因为无论哪种隔离级别,只要需要进行写操作就一定需要加锁。
6. 关于一致性
MySQL本身其实对一致性并没有做出什么措施,实际上一致性是由原子性、隔离性、持久性共同保证的。
- 事务处理结束后,对数据的修改必须是永久的,即便系统故障也不能丢失,即一致性需要持久性来保证。
- 多个事务同时访问同一份数据时,必须保证这多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致,即一致性需要隔离性来保证。
- 事务在执行过程中如果发生错误,则需要自动回滚到事务最开始的状态,就像这个事务从来没有执行过一样,即一致性需要原子性来保证。
- 此外,一致性与用户的业务逻辑强相关,如果用户本身的业务逻辑有问题,最终也会让数据库处于一种不一致的状态。
总结来说,一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需要上层用户编写出正确的业务逻辑。