看下面的一个例子,思考一下:CURD不加控制,会有什么问题?
这是一个经典的多线程并发导致数据不一致的问题,MySQL既然提供数据存储服务,那么它也要想办法解决上面的问题。
那CURD满足什么属性,能解决上述问题?
事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务主要用于处理操作量大,复杂度高的数据。比如转账就涉及多条SQL语句,包括查询余额(select)、在当前账户上减去指定金额(update)、在指定账户上加上对应金额(update)等,将这多条SQL语句打包便构成了一个事务。
MySQL同一时刻可能存在大量事务,如果不对这些事务加以控制,在执行时就可能会出现问题。比如单个事务内部的某些SQL语句执行失败,或是多个事务同时访问同一份数据导致数据不一致的问题,以及也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
因此一个完整的事务并不是简单的SQL集合,事务还需要满足如下四个属性,这四个属性简称为ACID:
原子性(Atomicity):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
在 MySQL 中只有使用了Innodb
数据库引擎的数据库或表才支持事务,MyISAM
不支持事务的。
通过show engines
命令可以查看数据库引擎和相关说明:
show engines;
说明:
InnoDB
存储引擎支持事务,而MyISAM
存储引擎不支持事务。事务的提交方式常见的有两种,这两种提交方式互不干扰:
通过show
命令查看autocommit
全局变量,可以查看事务的自动提交是否被打开
show variables like 'autocommit';
说明一下: autocommit
的值为ON表示自动提交被打开,值为OFF表示自动提交被关闭,即事务的提交方式为手动提交。
通过set
命令设置autocommit
全局变量的值,可以打开或关闭事务的自动提交。
将autocommit
的值设置为1表示打开自动提交,设置为0表示关闭自动提交,相当于将事务提交方式设置为手动提交。
set autocommit=0;
可以看到我们已经关闭了自动提交:
start transaction;
,手动开启一个事务,由于事务两种提交方式是互不干扰的,所以我们使用此命令手动开启的事务不会受到自动提交autocommit
的影响。
savepoint 名称;
,手动设置一个保存点,方便我们进行回滚操作。
rollback to 保存点名称
,手动回滚到当前保存点,如果使用rollback;
则直接回到事务的最开始。
commit;
,提交事务,表示对该事务操作完成,提交以后事务期间所有的SQL操作都会生效,而且无法被回滚。
准备工作
我们先创建一个银行用户表,表中包含用户的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 uncommitted;
需要注意的是,设置全局隔离级别后当前会话的隔离级别不会改变,只会影响后续与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 autocommit=0;
这一次我们让客户端1使用下面的SQL进行插入数据,插入完毕以后,立即使用客户端2立即进行查看,然后让客户端1退出,最后再使用客户端2进行查看。
这一次的结果和我们想象的不一样了我们的客户端1明明已经执行了插入语句了啊,我们使用客户端2也能够查看到已经插入的数据,为什么我们的客户端1退出以后,再使用客户端2进行查看就找不到了呢?
这正是因为这一次我们设置了手动提交,我们的客户端1在退出之前没有执行commit
提交,所以客户端1退出以后,mysql自动帮我们进行了回滚rollback
。
可以看出通过这样的设计,我们mysql能够保证一个事务要么就没有执行,如果执行了就一定执行完毕了,如果中间出现了异常mysql会进行回滚,这保证了操作的原子性。
在上面的演示中我们直接进行了事务操作,这是因为InnoDB
中的每一条SQL都会默认被封装成事务。
但是我们有些时候我们需要一个由多条SQL构成的事务,于是我们可以手动开启一个事务。
- 使用
begin
或者start transaction
开启一个事务
start transaction;
说明:
从我们开启这个事务开始,向下所有的SQL都会被包含在此事务中,直到遇到commit
提交后,事务结束。
如果在这个事务中间突然出现异常情况,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继续进行查看:
- 使用
commit
提交事务
假设现在就是我们想要的表,我们这个时候客户端1不能直接退出,这是我们手动开启的事务,必须要求我们手动进行提交,不然mysql会默认给我们回滚到事务的最开始。
commit
也代表着我们这个事务结束了,如果我们还要再手动创建事务,那么我们还要使用begin
或则start transaction
结论:
对于InnoDB
每一条SQL 语言都默认封装成事务(mysql自动为其开启的事务)。
mysql自动开启的事务的持久化与是否设置set autocommit
有关,如果没有设置自动提交需要手动提交。
只要输入begin
或者start transaction
,手动开启的事务便必须要通过commit
提交,才会持久化。
事务可以手动回滚,同时当操作异常,MySQL会自动回滚
从上面的例子,我们能看到事务本身的原子性(rollback),持久性(commit)
事务操作注意事项
rollback
(前提是事务还没有提交)隔离级别
说明:
- 查看全局隔级别
使用命令:
select @@global.tx_isolation;
- 查看会话隔离级别
可以使用下面两个命令,其中第二个命令是第一个命令的简化写法。
select @@session.tx_isolation;
select @@tx_isolation;
默认情况下,我们当前会话的隔离级别是继承自全局的隔离级别,当然最终产生作用的还是当前会话的隔离级别。
设置会话隔离级别的语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}
- 设置全局隔离级别
设置为serializable
set global transaction isolation level serializable;
说明: 设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。
- 设置会话隔离级别
设置为read committed
set session transaction isolation level read committed;
说明: 设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。
- 读未提交(Read Uncommitted)
启动两个终端,将隔离级别都设置为读未提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务(模拟事务并发运行),左终端中的事务所作的CRUD操作在没有提交之前,右终端中的事务就已经能够看到了。如下:
说明:
- 读提交(Read Committed)
启动两个终端,将隔离级别都设置为读提交,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
只有当左终端中的事务提交后,右终端中的事务才能看到修改后的数据。
说明:
select
查询得到了不同的数据,这种现象叫做不可重复读。
- 可重复读(Repeatable Read)
启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。如下:
并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据。只有当右终端中的事务提交后再查看表中的数据,这时才能看到修改后的数据。如下:
说明:
在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是所谓的可重复读。
一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据可能会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本在表中是不存在的,因此一般的加锁无法屏蔽这类问题。
一个事务在执行过程中,相同的select查询得到了新增的数据,如同出现了幻觉,这种现象叫做幻读。
MySQL解决了可重复读隔离级别下的幻读问题,比如上面我们新插入数据,在右终端是没有办法看到的。
MySQL是通过Next-Key锁(GAP+行锁)来解决幻读问题的。
- 串行化(Serializable)
启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据。如下:
在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。如下:
但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞。如下:
直到访问这张表的其他事务都提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作。如下:
说明:
串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用。
串行化不是串行化的SQL,而是串行化的事务。
对MySQL中的隔离级别的特性总结如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交(read uncommitted) | √ | √ | √ | 不加锁 |
读已提交(read committed) | X | √ | √ | 不加锁 |
可重复读(repeatable read) | X | X | X | 不加锁 |
可串行化(serializable) | X | X | X | 加锁 |
说明:
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态,当数据库只包含事务成功提交的结果时,数据库就处于一致性状态。
事务在执行过程中如果发生错误,则需要自动回滚到事务最开始的状态,就像这个事务从来没有执行过一样,即一致性需要原子性来保证。
事务处理结束后,对数据的修改必须是永久的,即便系统故障也不能丢失,即一致性需要持久性来保证。
多个事务同时访问同一份数据时,必须保证这多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致,即一致性需要隔离性来保证。
此外,一致性与用户的业务逻辑强相关,如果用户本身的业务逻辑有问题,最终也会让数据库处于一种不一致的状态。所以对于一致性一般MySQL只提供技术支持,而技术上,MySQL通过AID保证C
也就是说,一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需要上层用户编写出正确的业务逻辑。