MySQL事务管理
一、CURD的问题
1.MySQL数据安全
如果MySQL不对CURD做任何管理,就会出现类似线程安全的问题。因为同一时间可能有大量的增删查改的操作。
比如火车站售票系统,当客户端A买票就要对票数进行–操作,客户端B买票也需要对票数进行–操作。
此时就需要CURD满足一些属性来解决这一问题:
1.买票的过程需要是原子的。
2.买票者互相不影响。
3.买完票是永久有效的。
4.买前,买后的状态是确定的。
因为存在着这样的问题,MySQL一定是在底层做了解决的,那么它是如何进行解决的呢?
下面我们就围绕这四点,也是线程安全的问题来介绍MySQL的事物管理。
2.什么是事务
要理解事物管理,我们首先需要知道什么是事物:
事务就是要做的事情,主要用于处理操作量大,复杂度高的数据,假设一种场景:你毕业了,学校的教务系统后台的MySQL中不再需要你的任何数据,要删除你的全部信息,那么要删除你的基本信息的同时,也要删除与你有关的信息,比如你的成绩,表现,论文等。这样就需要多条MySQL语句使用,并且操作需要是原子性的,这些原子性的MySQL语句合起来就构成了一个事务。
再比如,老师要使用数据库来统计数学的平均分,下面在MySQL的角度和用户的角度来看这个问题:
MySQL:求表中math列的平均值->对应的是多条SQL语句
用户:计算我们班数学成绩的平均分->对应的是一个事务
对原子性的理解:如果说多条SQL语句只执行了几条,没有执行完,那么就不叫具有原子性。要么执行完要么不执行。
所有的SQL操作,一般都会被MySQL包装成事务,以事务的方式来进行提交。
3.事务的属性
事务的属性对应上文中买票的属性:
- 1.原子性:一个事务中得到所有操作,要么全部完成,要么不完成,不会结束在中间的某个环节。事务在实行的过程中发生错误,就会回滚到事务开始的状态,就像这个事务从来没有执行过一样。
- 2.一致性:在事务开始之前和事务结束之后,数据库的完整性没有被破坏。这就表示写入的资料必须符合所有的预设规则,这包含资料的精确度,串联性以及后续数据库可以自发性地完成预订的工作。
- 3.隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性防止多个事务并发执行时由于交叉执行而导致数据的不一致问题。事务隔离又分为不同的级别,包括读未提交,读提交,可重复读和串行化。
- 4.持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
以上的四个属性称为:ACID
A:原子性
C:一致性
I:隔离性
D:持久性
4.为什么会出现事务
事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们取考虑各种各样的潜在错误和并发问题。可以想象出来,当我们使用事务的时候,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办?因此事务的本质就是为了应用层程序服务的。而不是伴随着数据库系统天生就有的。
备注:我们将MySQL中的一条信息称为一条记录。
5.事务的版本支持
在MySQL中只有Innodb引擎支持事务,其他的均不支持。

6.事务的提交方式
事务的提交方式是针对于一条SQL的,在数据库中,一条SQL也会被封装成一个事务。
事务的提交方式分为两种:
自动提交
手动提交
在没有任何修改的情况下,一般使用的是自动提交。
mysql> show variables like 'autocommit';
mysql> set autocommit=0;//关闭自动提交的选项
mysql> set autocommit=1;//将自动提交的选项打开

即自动提交的选项是开着的,下面我们介绍完一些例子之后才可以对自动和手动有更好的了解。
二、事务的基本操作
事务的基本操作的顺序大致是:
- start transaction 或者使用 begin:启动一个事务
- 正常操作:进行CURD等操作
- rollback (to 断点):回滚
- commit:提交
下面我们来演示一下一个事务的处理过程:
1.创建一个测试表
mysql> create table account(
-> id int primary key,
-> name varchar(50) not null,
-> blance decimal(10,2) not null default 0.0
-> )engine=innodb default charset=utf8;
2.事务处理逻辑
mysql> start transaction;//启动一个事务(以后都使用begin来启动)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
mysql> insert into account values(1,'张三','999.99');
mysql> insert into account values(2,'李四','888.99');
mysql> savepoint s2;//设置一个回滚断点s2
mysql> insert into account values(3,'王五','777.99');
mysql> select* from account;
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 999.99 |
| 2 | 李四 | 888.99 |
| 3 | 王五 | 777.99 |
+----+--------+--------+
mysql> rollback to s2;//回滚到断点s2
mysql> select* from account;//发现s2之后插入的王五没有了
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 999.99 |
| 2 | 李四 | 888.99 |
+----+--------+--------+
mysql> rollback;//不加任何断点表示回滚到最开始
mysql> select * from account;
Empty set (0.01 sec)
mysql> insert into account values(1,'张三','999.99');
mysql> insert into account values(2,'李四','888.99');
mysql> commit;//提交
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 999.99 |
| 2 | 李四 | 888.99 |
+----+--------+--------+
3.非正常演示
我们再开启一个终端,用来观察数据库中的变化:
为了方便观察,我们将隔离类型设为读未提交:
mysql> set global transaction isolation level read uncommitted;//注意,设置好之后需要重启终端
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
(1)证明未提交事务异常退出,MySQL会自动回滚起始位置
mysql> begin;
mysql> insert into account values(3,'王五','888.88');
//ctrl+\使MySQL异常退出
观察对端:

当事务中向account中插入王五这一行的时候,在对端可以查到该行,但是当事务异常退出之后,在对端就无法查到该行了。这说明当事务异常退出之后会发生回滚。并且直接回滚到事务没发生的位置。这也体现了事务的原子性,即要么执行完,要么不执行。
(2)如果已经提交,MySQL崩溃,此时不会影响数据
mysql> begin;
mysql> insert into account values(3,'王五','9999.99');
mysql> commit;//提交事务
//ctrl+\使MySQL崩溃
此时对端发现数据没有受到任何影响:

(3)begin会自动更改提交方式,不会受MySQL是否自动提交影响
我们之前设置的方式都是自动提交,那我们现在将自动提交关闭呢?
mysql> set autocommit=0;//关闭自动提交
mysql> begin;
mysql> insert into account values(4,'赵六',99.999);
//ctrl+\退出MySQL
此时我们发现事件依然回滚了:

因此得出结论,只要是使用begin和start translation开始一个事务,与事务是否是自动提交的无关。
那么事务的自动提交有什么用呢?它主要是用来限制当个SQL的。
(4)证明单条SQL与事务的关系
mysql> insert into account values(4,'赵六',99.999);//一端添加
//ctrl+\终止MySQL
此时在对端是看不到的:

这才是自动提交真正的作用。如果将自动提交打开,即使退出也会将数据持久化。其实我们之前所写的所有的单条SQL的本质在MySQL中全部各自会被以事务的形式进行提交的,因为之前所有的提交都不使用begin等,因此会收到自动提交选项的限制。因此在没有开始事务的情况下,4,5个SQL表示的是4,5个事务而不是一个事务。
4.结论
- 只要输入begin或者start transaction,事务就必须要通过commit进行提交,才会持久化,与是否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
- 对于innodb每一条SQL语言都默认封装成事务,自动提交(select有特殊情况,因为MySQL有MVCC)
- 事务本身的原子性(回滚),持久性,commit后会持久化到MySQL中。
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始,直接使用rollback(前提是事务还没有提交)
- 如果一个事务被提交了(commit),则不可以进行回滚。
- 可以选择回滚到哪一个保存点。
- innodb支持事务,而MyISAM不支持事务。
- 开始事务可以使start transaction或者begin。
三、事务的隔离级别
1.隔离性与隔离级别
- MySQL服务可能会同时被多个客户端进程(线程)访问,访问方式以事务的方式来进行。
- 一个事务可能有多条语句来执行,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后,执行中出现了问题,可以随时进行回滚,所以单个事务对用户表现出来的属性,就是原子性。
- 但,毕竟所有事务都有一个执行过程,那么多个事务各自执行多条语句的时候,就还是可能出现相互影响的情况,比如:多个事务同时访问一张表。甚至同一行数据。
- 在数据库中,为了保证事务执行的过程中,尽量不受到干扰,就有了一个重要特性:隔离性
- 允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
2.隔离级别
- 读未提交:在隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面使用都是读未提交的隔离方式,但是这种方式弊端比较大,因此通常不会使用它。
- 读提交:该隔离级别是大多数数据库的默认隔离级别(MySQL不是),它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做出的改变,这种隔离级别会引起不可重复读,即一个事务在执行时,如果进行多次select可能读到不同的结果。
- 可重复读:这是MySQL默认的隔离级别,它确保一个事务,在执行中,多次读取操作数据时,会看到同样的数据行,但是会有幻读问题。
- 串行化:这是事务的最高隔离级别,它通过强制事务排序,使之不可能发生冲突,从而解决了幻读的问题,它在每个读的数据行上面加上了共享锁,但是可能会导致超时和锁竞争(这种隔离方式太极端,实际生产中基本不会使用)。
隔离级别如何实现:隔离,基本上都是通过锁实现的,不同的隔离级别,锁的使用时不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过我们目前现有这个认识就行,先关注上层应用使用。
3.设置与查看隔离性
(1)查看隔离性
mysql> select @@global.transaction_isolation;//查看全局的隔离性
mysql> select @@session.transaction_isolation;//查看当前会话的隔离性
(2)设置隔离性
set [session/global] transaction isolation level {read uncommitted|read committed||repeatable read||serializable};
//设置当前会话/全局的隔离性为,读未提交/读提交/可重复读/串行化
其中当前会话指的是仅限于当前会话使用,重新开启一个会话隔离性还是会变成默认的全局的隔离性。而全局的隔离性指的是无论开启多少会话,都是不变的。
4.四种提交方式对比
在默认的情况下,MySQL的隔离性是可重复读,当然我们也可以设置一下:
mysql> set global transaction isolation level repeatable read;//MySQL默认的全局的隔离性就是可重复读
我们只对当前会话做更改,不受全局的影响。
(1)读未提交
mysql> set session transaction isolation level read uncommitted;//将当前会话的隔离性设为读未提交
其实读未提交我们之前已经演示过了。
一个事务在执行中,读到另一个执行中的事务的更新(或者其他操作)但是未提交commit的数据,这种现象叫脏读。

此时A事务是否进行提交,是否进行回滚,B事务是不知道的,但是它却可以看到A事务在执行过程中表的信息,这种情况我们称之为脏读。显然对于B事务来说,两次读取表的信息不一样,这是有风险的,因此这种方式不值得提倡。
(2)读提交
mysql> set session transaction isolation level read committed;//注意两个会话都要进行设置
只有一个事务提交之后,另一个事务才能看到该事务修改的内容。

只有当A事务提交了之后,B事务才能看到A事务修改的内容,称为读提交,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中),读取到了不同的值,这种现象就叫做不可重复读!!!,这样的操作依然是有问题的。
假设A事务正在对表中数据做修改,B事务正在读取表中数据,当A事务修改结束之后,B事务读取的数据依然可能是两次读取不一样的,只要两次读取不一样就有可能影响决策,因此是由问题的。
(3)可重复读
mysql> set session transaction isolation level repeatable read;
一个事务提交之后,另一个事务依然看不到修改的内容,只有当另一个事务也提交,该会话才能看到那个事务修改的内容.

A事务和B事务启动,当事务A对表中数据进行修改的时候,B事务是看不到的;当事务A提交之后,B事务依然是看不到的,只有当B事务提交了,会话B才能看到A事务修改之后的数据。称为可重复读。
通常情况下,一般的数据库(非MySQL)在可重复读情况的时候,无法屏蔽其他事务的insert,因为隔离性的实现是通过加锁完成的,而insert待插入的数据本身就不在表之内,因此无法加锁解决这个问题。这样就会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找的时候,会查找出来多条新的记录,就好像出现了幻觉,这种情况称为幻读。
不过这只是针对其他的数据库,我们牛逼闪闪的MySQL解决了这一问题,是通过Next-Key锁(GAP+行锁)解决的。
(4)串行化
mysql> set session transaction isolation level serializable;
隔离性的串行化指的是事务的串行化,而不是单个SQL的串行化。但是读操作例外,可以同时执行。

但是进行增删改操作的时候就会发生阻塞:

5.总结
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来就发现值不一样了,幻读的重点在于新增;
- MySQL默认的隔离级别是可重复读,一般不要修改。
- 事务也有长短事务这样的概念,事务间相互影响,指的是事务在并行执行的时候,都没有commit的时候影响比较大。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
|---|---|---|---|---|
| 读未提交 | 有 | 有 | 有 | 无 |
| 读已提交 | 无 | 有 | 有 | 无 |
| 可重复读 | 无 | 无 | 无(MySQL) | 无 |
| 可串行化 | 无 | 无 | 无 | 有 |
| 隔离性是MySQL的内部机制,让同时启动,并发执行的各个事务看到不同的数据修改,作为一个事务可以看到不同的可见性的数据的程度不同,称为隔离级别的不同。 | ||||
| 不同隔离性的存在其实是根据用户不同的需要来设置的,使用哪种由用户来决定。 |
四、一致性
讲完了隔离性,原子性与持久性,最后来谈谈一致性。
事务的一致性更像是一种对以上三种特性的总结。
- 事务执行的结果,必须使数据库从一个一致性的状态,转为另一个一致性的状态。当数据库只包含事务提交的结果时,数据库处于一致性的状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已经被写入数据库,此时数据库就是一种不一致的状态,所以一致性是由原子性来保证的。
- 其实一致性和用户的业务逻辑相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支持,也就是一致性,是由用户决定的。

浙公网安备 33010602011771号