一、引入
看下面的一个例子,思考一下:CURD不加控制,会有什么问题?
这是一个经典的多线程并发导致数据不一致的问题,mysql既然提供数据存储服务,那么它也要想办法解决上面的问题。
那CURD满足什么属性,能解决上述问题?
- 买票的过程得是原子的吧。(原子性)
- 买票互相应该不能影响吧。 (隔离性)
- 买完票应该要永久有效吧。 (持久性)
- 买前和买后都要是确定的状态吧。(一致性)
二、事务的介绍
1、什么是事务?
-
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
-
事务主要用于处理操作量大,复杂度高的数据。比如转账就涉及多条SQL语句,包括查询余额(select)、在当前账户上减去指定金额(update)、在指定账户上加上对应金额(update)等,将这多条SQL语句打包便构成了一个事务。
-
MySQL同一时刻可能存在大量事务,如果不对这些事务加以控制,在执行时就可能会出现问题。比如单个事务内部的某些SQL语句执行失败,或是多个事务同时访问同一份数据导致数据不一致的问题,以及也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
因此一个完整的事务并不是简单的SQL集合,事务还需要满足如下四个属性,这四个属性简称为ACID:
-
原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
-
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
-
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
- 事务隔离分为不同级别,包括读未提交( Read un***mitted )、读提交( read ***mitted )、可重复读( repeatable read )和串行化( Serializable )
-
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
2、为什么会出现事务
- 事务被MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。
- 可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
3、事务的版本支持
在 MySQL 中只有使用了Innodb
数据库引擎的数据库或表才支持事务,MyISAM
不支持事务的。
通过show engines
命令可以查看数据库引擎和相关说明:
show engines;
说明:
- Engine: 表示存储引擎的名称。
- Support: 表示mysql服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用。
- ***ment: 表示存储引擎的简要说明。
-
Transactions: 表示存储引擎是否支持事务,可以看到
InnoDB
存储引擎支持事务,而MyISAM
存储引擎不支持事务。 - XA: 表示存储引擎是否支持XA事务。
- Savepoints: 表示存储引擎是否支持保存点。
三、事务的操作
1、事务的提交方式
事务的提交方式常见的有两种,这两种提交方式互不干扰:
- 自动提交
- 手动提交
通过show
命令查看auto***mit
全局变量,可以查看事务的自动提交是否被打开
show variables like 'auto***mit';
说明一下: auto***mit
的值为ON表示自动提交被打开,值为OFF表示自动提交被关闭,即事务的提交方式为手动提交。
2、设置事务的提交方式
-
通过
set
命令设置auto***mit
全局变量的值,可以打开或关闭事务的自动提交。 -
将
auto***mit
的值设置为1表示打开自动提交,设置为0表示关闭自动提交,相当于将事务提交方式设置为手动提交。
set auto***mit=0;
可以看到我们已经关闭了自动提交:
3、事务常见操作方式
-
start transaction;
,手动开启一个事务,由于事务两种提交方式是互不干扰的,所以我们使用此命令手动开启的事务不会受到自动提交auto***mit
的影响。 -
savepoint 名称;
,手动设置一个保存点,方便我们进行回滚操作。 -
rollback to 保存点名称
,手动回滚到当前保存点,如果使用rollback;
则直接回到事务的最开始。 -
***mit;
,提交事务,表示对该事务操作完成,提交以后事务期间所有的SQL操作都会生效,而且无法被回滚。
4、不同提交方式的事务的演示
准备工作
我们先创建一个银行用户表,表中包含用户的id
、姓名和账户余额。如下:
create table balance(
id int primary key auto_increment,
name varchar(20) not null,
balance decimal(10,2) not null default 0.0
);
为了便于演示,我们将MySQL的隔离级别设置成读未提交,也就是把隔离级别设置的比较低,方便看到实验现象。如下:
set global transaction isolation level read un***mitted;
需要注意的是,设置全局隔离级别后当前会话的隔离级别不会改变,只会影响后续与MySQL新建立的连接,因此需要重启终端才能看到会话的隔离级别被成功设置。如下:
select @@tx_isolation;
- 自动提交
事务是可以由一条或则多条SQL组成的,在我们开启自动提交的情况下我们每一条执行过SQL都会被提交。
例如我们打开两个客户端进行连接,客户端1使用下面的SQL进行插入数据,插入完毕以后,立即异常退出(ctrl + \ )产生abort
信号,然后客户端2立即进行查看。
insert into balance(name, balance) values('张三',1000.2);
我们发现,客户端1在执行完SQL以后,然后直接异常终止,发现其执行的SQL确实将数据进行插入到表中了,当然你会觉得这很正常,毕竟我们以前不了解事务时也知道,执行完毕的SQL最终肯定要起作用,但这是不正确的,执行完毕的SQL不一定最终肯定要起作用,这里和以前起作用是因为我们默认开启了自动提交。
- 手动提交
我们将自动提交进行关闭,然后继续进行上面的实验:
set auto***mit=0;
这一次我们让客户端1使用下面的SQL进行插入数据,插入完毕以后,立即使用客户端2立即进行查看,然后让客户端1退出,最后再使用客户端2进行查看。
这一次的结果和我们想象的不一样了我们的客户端1明明已经执行了插入语句了啊,我们使用客户端2也能够查看到已经插入的数据,为什么我们的客户端1退出以后,再使用客户端2进行查看就找不到了呢?
-
这正是因为这一次我们设置了手动提交,我们的客户端1在退出之前没有执行
***mit
提交,所以客户端1退出以后,mysql自动帮我们进行了回滚rollback
。 -
可以看出通过这样的设计,我们mysql能够保证一个事务要么就没有执行,如果执行了就一定执行完毕了,如果中间出现了异常mysql会进行回滚,这保证了操作的原子性。
5、事务的常规操作
在上面的演示中我们直接进行了事务操作,这是因为InnoDB
中的每一条SQL都会默认被封装成事务。
但是我们有些时候我们需要一个由多条SQL构成的事务,于是我们可以手动开启一个事务。
- 使用
begin
或者start transaction
开启一个事务
start transaction;
说明:
-
从我们开启这个事务开始,向下所有的SQL都会被包含在此事务中,直到遇到
***mit
提交后,事务结束。 -
如果在这个事务中间突然出现异常情况,mysql会自动回滚到事务的最开始,也就是刚创建事务时的状态。
- 使用
savepoint
创建保存点,方便我们进行回滚
然后我们在这个事务中插入一些数据,插入一条建立一个保存点:
insert into balance(name, balance) values('李四', 1523.4);
savepoint s2;
insert into balance(name, balance) values('王五', 2002.4);
savepoint s3;
最后使用客户端2进行查看:
- 使用
rollback to
进行回滚
假设我们将王五的数据插入时搞错了,这时我们可以回滚到s2
状态,即王五的信息还没有被插入时的状态。
rollback to s2;
然后我们使用客户端2继续进行查看:
- 使用
***mit
提交事务
假设现在就是我们想要的表,我们这个时候客户端1不能直接退出,这是我们手动开启的事务,必须要求我们手动进行提交,不然mysql会默认给我们回滚到事务的最开始。
- 在我们使用完了
***mit
也代表着我们这个事务结束了,如果我们还要再手动创建事务,那么我们还要使用begin
或则start transaction
结论:
-
对于
InnoDB
每一条SQL 语言都默认封装成事务(mysql自动为其开启的事务)。 -
mysql自动开启的事务的持久化与是否设置
set auto***mit
有关,如果没有设置自动提交需要手动提交。 -
只要输入
begin
或者start transaction
,手动开启的事务便必须要通过***mit
提交,才会持久化。 -
事务可以手动回滚,同时当操作异常,MySQL会自动回滚
从上面的例子,我们能看到事务本身的原子性(rollback),持久性(***mit)
事务操作注意事项
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用
rollback
(前提是事务还没有提交) - 如果一个事务被提交了(***mit),则不可以回退(rollback)
- InnoDB 支持事务, MyISAM 不支持事务。
四、事务的隔离级别
- MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务的方式进行。
- 一个事务可能由多条SQL语句构成,也就意味着任何一个事务,都有执行前、执行中和执行后三个阶段,而所谓的原子性就是让用户层要么看到执行前,要么看到执行后,执行中如果出现问题,可以随时进行回滚,所以单个事务对用户表现出来的特性就是原子性。
- 但毕竟每个事务都有一个执行的过程,在多个事务各自执行自己的多条SQL时,仍然可能会出现互相影响的情况,比如多个事务同时访问同一张表,甚至是表中的同一条记录。
- 数据库为了保证事务执行过程中尽量不受干扰,于是出现了隔离性的概念,而数据库为了允许事务在执行过程中受到不同程度的干扰,于是出现了隔离级别的概念。
隔离级别
- 读未提交【Read Un***mitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
- 读提交【Read ***mitted】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是可能会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)
说明:
- 虽然数据库事务的隔离级别有以上四种,但一个稳态的数据库只会选择这其中的一种,作为自己的默认隔离级别。只是因为数据库默认的隔离级别有时可能并不满足上层的业务需求,因此数据库提供了这四种隔离级别,可以让我们根据需要自行设置。
- 隔离级别基本上都是通过加锁的方式实现的,不同的隔离级别对锁的使用是不同的,常见的有表锁、行锁、写锁、间隙锁(GAP)、Next-Key锁(GAP+行锁)等。
1、查看与设置隔离性
- 查看全局隔级别
使用命令:
select @@global.tx_isolation;
- 查看会话隔离级别
可以使用下面两个命令,其中第二个命令是第一个命令的简化写法。
select @@session.tx_isolation;
select @@tx_isolation;
默认情况下,我们当前会话的隔离级别是继承自全局的隔离级别,当然最终产生作用的还是当前会话的隔离级别。
2、设置会话隔离级别
设置会话隔离级别的语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UN***MITTED | READ
***MITTED | REPEATABLE READ | SERIALIZABLE}
- 设置全局隔离级别
设置为serializable
set global transaction isolation level serializable;
说明: 设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。
- 设置会话隔离级别
设置为read ***mitted
set session transaction isolation level read ***mitted;
说明: 设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。
3、四种隔离级别的演示
- 读未提交(Read Un***mitted)
启动两个终端,将隔离级别都设置为读未提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务(模拟事务并发运行),左终端中的事务所作的CRUD操作在没有提交之前,右终端中的事务就已经能够看到了。如下:
说明:
- 读未提交是事务的最低隔离级别,几乎没有加锁,虽然并发的效率很高,但是问题比较多,所以严重不建议使用。
- 一个事务在执行过程中,读取到另一个执行中的事务所做的修改,而且这个修改还没有进行提交,这种现象叫做脏读。
- 读提交(Read ***mitted)
启动两个终端,将隔离级别都设置为读提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
只有当左终端中的事务提交后,右终端中的事务才能看到修改后的数据。
说明:
- 一个事务在执行过程中,两个相同的
select
查询得到了不同的数据,这种现象叫做不可重复读。 - 显然在读提交的隔离级别下,存在不可重复读现象。
- 可重复读(Repeatable Read)
启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据。只有当右终端中的事务提交后再查看表中的数据,这时才能看到修改后的数据。如下:
说明:
-
在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是所谓的可重复读。
-
一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据可能会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本在表中是不存在的,因此一般的加锁无法屏蔽这类问题。
-
一个事务在执行过程中,相同的select查询得到了新增的数据,如同出现了幻觉,这种现象叫做幻读。
-
MySQL解决了可重复读隔离级别下的幻读问题,比如上面我们新插入数据,在右终端是没有办法看到的。
-
MySQL是通过Next-Key锁(GAP+行锁)来解决幻读问题的。
- 串行化(Serializable)
启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。如下:
但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞。如下:
直到访问这张表的其他事务都提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作。如下:
说明:
-
串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用。
-
串行化不是串行化的SQL,而是串行化的事务。
4、隔离级别总结
对MySQL中的隔离级别的特性总结如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交(read un***mitted) | √ | √ | √ | 不加锁 |
读已提交(read ***mitted) | X | √ | √ | 不加锁 |
可重复读(repeatable read) | X | X | X | 不加锁 |
可串行化(serializable) | X | X | X | 加锁 |
说明:
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点。
- 表中只写出了各种隔离级别下进行读操作时是否需要加锁,因为无论哪种隔离级别,只要需要进行写操作就一定需要加锁。
5、关于一致性
-
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态,当数据库只包含事务成功提交的结果时,数据库就处于一致性状态。
-
事务在执行过程中如果发生错误,则需要自动回滚到事务最开始的状态,就像这个事务从来没有执行过一样,即一致性需要原子性来保证。
-
事务处理结束后,对数据的修改必须是永久的,即便系统故障也不能丢失,即一致性需要持久性来保证。
-
多个事务同时访问同一份数据时,必须保证这多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致,即一致性需要隔离性来保证。
-
此外,一致性与用户的业务逻辑强相关,如果用户本身的业务逻辑有问题,最终也会让数据库处于一种不一致的状态。所以对于一致性一般MySQL只提供技术支持,而技术上,MySQL通过AID保证C
也就是说,一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需要上层用户编写出正确的业务逻辑。