专栏系列文章:MySQL系列专栏mysql
事务(Transaction
)是数据库系统执行过程当中的一个逻辑处理单元,可由一条简单的SQL语句组成,也能够由一组复杂的SQL语句组成。在事务中的操做,要么都作修改,要么都不作,这就是事务的目的。web
先准备下面一张帐户表来供后面测试使用:sql
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`card` varchar(60) NOT NULL COMMENT '卡号',
`balance` int(11) NOT NULL DEFAULT '0' COMMENT '余额',
PRIMARY KEY (`id`),
UNIQUE KEY `account_u1` (`card`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帐户表';
复制代码
标准上,事务必须同时知足四个特性,也就是事务的ACID
特性。正是这些特性,才保证了数据库事务的安全性。不过数据库厂商出于各类目的,可能并不会严格知足事务的ACID标准。数据库
在MySQL中,MyISAM 存储引擎不支持事务,InnoDB 存储引擎在默认的READ REPEATABLE(RR)
隔离级别下,彻底遵循和知足事务的ACID
特性,因此后续对事务的研究是基于 InnoDB 存储引擎的。安全
原子性 指一个数据库事务中的全部操做是不可分割的单元,只有事务中全部的数据库操做都执行成功,才算整个事务成功。事务中任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。服务器
好比有下面一个转帐操做,A 向 B 转帐 100
:markdown
BEGIN;
UPDATE account SET balance = balance - 100 WHERE card = "A";
UPDATE account SET balance = balance + 100 WHERE card = "B";
COMMIT;
复制代码
这个转帐操做就必须是一个原子操做,A 减去 100,B 加上 100,要么都成功,要么都回滚,不能有中间状态,任何一个SQL失败,都要回滚到执行事务前的状态。网络
在咱们看来,就是两条SQL更新语句,其实在数据库层面,这两条SQL语句会涉及不少操做。在前面学习Buffer Pool
的时候,就知道了首先须要将所在的数据页从磁盘加载到 Buffer Pool,而后更新内存中的页,再把页加入Flush链表
,而后在某个时刻将脏页刷盘,其中任何一个步骤失败(好比数据库宕机)都须要回滚。多线程
因此原子性是要保证数据库的整个操做过程都是原子的,其中任何一步失败都要撤销。InnoDB底层有一套复杂的机制来保证数据库操做的原子性,把已经作了的操做恢复成执行以前的样子,这块咱们后面会用一篇文章来讲明。并发
一致性 指事务将数据库从一种状态转变为下一种一致的状态。在事务开始以前和事务结束之后,数据库的完整性约束没有被破坏。
例如,account 表中 card 字段是惟一的,无论如何修改这个字段,在事务提交或事务回滚后,card 字段的数据都仍是惟一。若是变得非惟一了,这就破坏了事务的一致性要求。
mysql> INSERT INTO account(card, balance) values ("A", 1000);
1062 - Duplicate entry 'A' for key 'account_u1'
复制代码
要保证数据库中数据的一致性,主要有两个方面:
数据库自己保证一部分一致性:MySQL数据库自己能够创建一些约束,例如为表创建主键、惟一索引、外键、声明某个列为NOT NULL等。
业务层保证一致性:更多的状况下,具体业务场景中的约束会比较复杂,并且数据库创建约束会对数据库性能有必定损耗。因此每每咱们会在业务代码层面来对数据作一致性校验。
隔离性还有其余的称呼,如并发控制、可串行化、锁等。事务的隔离性要求每一个读写事务的对象对其余事务的操做对象能相互分离,即该事务提交前对其余事务都不可见,一般这使用锁来实现。
当数据库上有多个事务同时执行的时候,就可能出现脏读、不可重复读、幻读的问题,这块咱们后面会再细说。
持久性要求事务一旦提交,其结果就是永久性的。即便发生宕机等故障,数据库也能将数据恢复。
须要注意的是,持久性是保证事务系统的高可靠性
,而不是高可用性。事务自己能保证结果的永久性,在事务提交后,全部的变化都是永久的。但对于一些外部因素,如磁盘损坏、天然灾害等缘由致使数据库发生问题,那么全部提交的数据可能都会丢失。对于高可用性的实现,事务自己并不能保证,须要一些系统共同配合来完成。
从事务理论的角度来讲,能够把事务分为如下几种类型:
对于InnoDB存储引擎来讲,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持。
一、扁平事务
扁平事务是事务类型中最简单的一种,也是使用最为频繁的事务。在扁平事务中,全部操做都处于同一层次,由 BEGIN/START TRANSACTION
开始,由 COMMIT
或 ROLLBACK
结束,其间的操做是原子的。
二、带有保存点的扁平事务
带有保存点的扁平事务容许在事务执行过程当中回滚到同一事务中较早的一个状态。咱们能够在事务过程当中设置一些保存点(Savepoint)
,保存点用来通知系统应该记住事务当前的状态,以便当以后发生错误时,事务能回到保存点当时的状态。
对于扁平事务来讲,其在事务开始的时候隐式地设置了一个保存点,扁平事务就只有这一个保存点,所以,回滚只能回滚到事务开始时的状态。
能够经过 SAVEPOINT
建立一个保存点,ROLLBACK TO SAVEPOINT
回滚到某个保存点。
三、链事务
链事务就是一个事务在提交的时候自动将上下文传给下一个事务,也就是说一个事务的提交和下一个事务的开始是原子性的,下一个事务能够看到上一个事务的结果,就好像在一个事务中进行的同样。
链事务可视为保存点模式的一种变种,不一样的是,带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务。
MySQL 的链式事务能够经过 SET completion_type = 1
来打开,后面会举例说明。
四、嵌套事务
嵌套事务是一个层次结构框架,由一个顶层事务控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务,其控制每个局部的变换。子事务提交后不会真的提交,而是等到父事务提交才真正的提交,父事务回滚了,会回滚全部子事务。
MySQL 不支持嵌套事务,不过咱们能够经过带有保存点的事务来模拟串行的嵌套事务。
五、分布式事务
分布式事务一般是一个在分布式环境下运行的扁平事务,须要根据数据所在位置访问网络中的不一样节点。后面会有一个专门的系列来学习分布式事务。
可使用 BEGIN [WORK]
或者 START TRANSACTION;
显示的开启一个事务。
在存储过程当中,MySQL数据库的分析器会自动将BEGIN
识别为BEGIN…END
,所以在存储过程当中只能使用START TRANSACTION
语句来开启一个事务。
START TRANSACTION
后边能够跟随几个修饰符:
READ ONLY
:标识当前事务是一个只读事务,该事务中的数据库操做只能读取数据,不能修改数据。READ WRITE
:标识当前事务是一个读写事务,该事务中的数据库操做能够读取数据,也能够修改数据。WITH CONSISTENT SNAPSHOT
:启动一致性读。若是不显式指定事务的访问模式,该事务的访问模式默认就是读写模式(READ WRITE
)。
例如开启只读事务后,就不能修改数据了:
mysql> START TRANSACTION READ ONLY;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE account SET balance = balance - 100 WHERE id = 100;
1792 - Cannot execute statement in a READ ONLY transaction.
复制代码
可使用 COMMIT [WORK]
来显示提交事务,WORK
有没有均可以。
一、自动提交事务
在MySQL命令行的默认设置下,事务都是自动提交的,即执行一条SQL语句后就会自动执行COMMIT
操做。所以要显式地开启一个事务需使用命令BEGIN
或START TRANSACTION
,或者执行命令SET autocommit=O
来禁用自动提交。
二、隐式提交事务
使用START TRANSACTION
或BEGIN
开启了一个事务,或者把系统变量autocommit
的设置为OFF时,事务就不会进行自动提交。但某些数据库操做会自动隐式的提交事务,也不须要开始一个事务。
常见的隐式提交事务的语句包括:
CREATE、ALTER、DROP、ALTER
等等。START TRANSACTION
或 BEGIN
开启事务时,会自动提交上一个事务。ANALYZE TABLE、FLUSH、OPTIMIZE TABLE、REPAIR TABLE
等语句也会隐式提交事务。三、事务提交类型
咱们能够经过参数completion_type
来控制COMMIT
后的行为,有三个值:
0/NO_CHAIN
:默认为NO_CHAIN
,表示 COMMIT 后没有任何操做。1/CHAIN
:设置为1
或CHAIN
时,COMMIT
等同于COMMIT AND CHAIN
,表示在事务提交后立刻自动开启一个相同隔离级别的新事务。2/RELEASE
:设置为2
或RELEASE
时,COMMIT
等同于COMMIT AND RELEASE
,表示在事务提交后会自动断开与服务器的链接。能够看到completion_type
默认值为 NO_CHAIN(0)
:
mysql> SHOW VARIABLES LIKE 'completion_type';
+-----------------+----------+
| Variable_name | Value |
+-----------------+----------+
| completion_type | NO_CHAIN |
+-----------------+----------+
复制代码
completion_type
参数只会影响BEGIN
或START TRANSACTION
开头,COMMIT
或ROLLBACK
结尾的事务,不会影响自动提交的事务(AUTOCOMMIT=1
)的事务。
completion_type
设置为1
时此时就变成了前面说的链事务。例以下面的操做,COMMIT
以后立马开启了一个新的事务,因此"B"这条数据才能够被回滚。设置为0
的状况下是不会回滚的。最后查询就只插入了"A"这条数据。
若是要开启链事务,能够直接使用 COMMIT [WORK] AND CHAIN;
来实现,而无需设置 completion_type=1
。
mysql> TRUNCATE account;
Query OK, 0 rows affected (0.01 sec)
mysql> SET completion_type = 1;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES ("A");
Query OK, 1 row affected (0.01 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO account(card) VALUES ("B");
Query OK, 1 row affected (0.00 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT * FROM account;
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | A | 0 |
+----+------+---------+
复制代码
completion_type
设置为2
时此时 COMMIT
以后就会断开链接,再操做就会报链接断开的错误,有些客户端会自动尝试从新链接。
mysql> SET completion_type = 2;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES ("C");
Query OK, 1 row affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account WHERE card = "C";
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id: 3
Current database: test
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | C | 0 |
+----+------+---------+
1 row in set (0.00 sec)
复制代码
可使用 ROLLBACK [WORK]
来终止事务,撤销正在进行的未提交的修改。
要注意的是,ROLLBACK
语句是手动的回滚事务时才去使用的,若是事务在执行过程当中遇到了某些错误而没法继续执行的话,事务自身会自动的回滚。
保存点的操做方法以下:
SAVEPOINT <identifier>
RELEASE SAVEPOINT <identifier>
ROLLBACK TO [SAVEPOINT] <identifier>
须要注意的是,ROLLBACK TO SAVEPOINT
,只是回滚到了指定的保存点,其并非真正地结束一个事务,最后还须要显式地运行COMMIT
或ROLLBACK
命令。
例以下面的操做,
mysql> TRUNCATE account;
Query OK, 0 rows affected (0.01 sec)
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES("A");
Query OK, 1 row affected (0.00 sec)
mysql> SAVEPOINT P1;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO account(card) VALUES("B");
Query OK, 1 row affected (0.00 sec)
mysql> ROLLBACK TO P1;
Query OK, 0 rows affected (0.00 sec)
mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM account;
+----+------+---------+
| id | card | balance |
+----+------+---------+
| 1 | A | 0 |
+----+------+---------+
1 row in set (0.00 sec)
复制代码
咱们的业务系统每每都是多线程并发执行多个事务,数据库层面也会多个事务并发执行,那么就可能会对同一条数据查询和修改。既然是并发,跟Java中的多线程同样,就会有线程安全问题。
并发事务涉及到四个问题:脏写、脏读、不可重复读、幻读。按问题的严重程度排个序就是:脏写 > 脏读 > 不可重复读 > 幻读
。
若是一个事务A读到了另外一个事务B修改过的未提交的数据,那事务A的读取就是脏读,由于事务A读取的数据是非持久性的数据。
例如按下面的时间线执行:事务B更新了数据,事务A读取了事务B未提交的数据,可是在t5时刻事务B回滚了这次操做,事务A查询到的数据就是脏数据,若是继续用这个脏数据作业务就会有问题。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查询余额为100; | 查询余额为100; |
t3 | 余额增长100; | |
t4 | 查询约为200; | |
t5 | ROLLBACK; | |
t6 | COMMIT; | |
t7 | A事务读取到B事务未提交的数据,致使读取到的是脏数据 |
在没有脏读的状况下,若是一个事务屡次读取同一个数据不一致,那说明发生了不可重复读的问题,也就是同一个数据没法重复读取,违反了数据库事务一致性的要求。
例如按下面的时间线执行:在事务A中,第一次查询为100,此时事务B修改了余额而且提交了事务,事务A再次查询就读取到事务B已提交的数据,在同一个事务中,两次查询的结果不一致,就是不可重复读取。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查询余额为100; | 查询余额为100; |
t3 | 余额增长100; | |
t4 | COMMIT; | |
t5 | 查询余额为200; | |
t6 | COMMIT; | |
t7 | A事务读取到B事务已提交的数据,屡次读取同一条数据不一致 |
其实不可重复读在一些场景下也能够认为不是问题,好比我就但愿在一个事务中,别的事务修改了数据,我立马也能读到,那就是不可重复读的。若是但愿在一个事务中屡次读取是同样的,就是可重复读。
幻读就是一个事务用一样的条件查询,因为另外一个事务新增了数据,致使看到了以前没有的数据。
例如按下面的时间线执行,事务A将全部帐户余额都改成100了,而后事务B新增了一个帐户,结果事务A再次查询发现还有一个帐户的余额为0,以前更新的数据中是没有这条记录的,这就是幻读。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 更新全部帐户余额为100; | |
t3 | 新增一个帐户,余额为0; | |
t4 | COMMIT; | |
t5 | 查询发现还有一个帐户余额为0; | |
t6 | COMMIT; | |
t7 | 因为B事务新增数据,致使A事务操做以后还有以前没看到过的数据 |
脏写也称为数据丢失、更新丢失,简单来讲就是一个事务的更新操做会被另外一个事务的更新操做所覆盖,从而致使数据的不一致。
有两类状况会致使脏写:
一、事务A回滚把事务B已提交的修改给覆盖了,就会形成事务B的修改丢失。
例如按下面的时间线执行,事务A、B开始时查询余额都为0,事务B修改余额增长了200,事务A回滚将余额变为0,事务B看起来就是修改的数据丢失了。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查询余额为0; | 查询余额为0; |
t3 | 余额增长100; | |
t4 | 余额增长200; | |
t5 | COMMIT; | |
t6 | ROLLBACK; | |
t7 | 查询余额为0; | 查询余额为0; |
t8 | 因为事务A回滚,致使事务B更新的数据没了 |
不过InnoDB存储引擎不会发生这个问题,由于InnoDB在更新数据时加了排他锁, 这样在事务A在未完成的时候, 其余事务是没法对事务A涉及到的数据作修改并提交的。例如上面的示意图中,事务B在执行余额增长200的时候,因为事务A修改了同一条数据且未提交,这时这条数据已经加了排它锁了,所以事务B修改时会阻塞住,等待加锁后才能修改。
二、事务A覆盖了事务B已提交的修改,形成事务B的修改丢失。
例如按下面的时间线执行,事务A、B一开始查询余额都为0,事务B先增长了200,并提交了事务。接着事务A在余额为0的基础上增长100,而后提交事务。最后就是余额只有100,事务B的修改丢失了。
Timeline | Session A | Session B |
---|---|---|
t1 | BEGIN; | BEGIN; |
t2 | 查询余额为0; | 查询余额为0; |
t3 | 余额增长200; | |
t4 | COMMIT; | |
t5 | 余额增长100; | |
t6 | COMMIT; | |
t7 | 查询余额为100; | 查询余额为100; |
t8 | 事务A将事务B提交的修改覆盖掉,致使事务B的修改丢失 |
这种状况有两种方式能够避免脏写发生:
一种是基于数据库悲观锁,在查询时使用 for update
实现一个排它锁,保证在该事务结束时其余事务没法更新该数据。不过这样就会致使并发更新的性能下降。
SELECT * FROM account WHERE id = 1 FOR UPDATE;
复制代码
另外一种是基于乐观锁,能够在表中增长一个版本号字段,查询时将版本号查出来,更新时带上版本号做为条件,更新成功则是同一条记录,不然就时更新失败。更新失败就能够返回“记录不存在或版本不一致”这样的错误,让用户能够从新查询再更新一次。
UPDATE account SET balance=balance+100, version=version+1 where id = 1 and version = 1
复制代码
前面说到并发事务有四个问题:脏写、脏读、不可重复度、幻读。其中,脏写能够经过乐观锁或悲观锁的方式来解决,剩下的3个问题,实际上是数据库读一致性
形成的,须要数据库提供必定的事务隔离机制来解决,也就是事务的隔离性
。
SQL标准定义了四个隔离级别:
READ UNCOMMITTED
:读未提交,简称 RU。READ COMMITTED
:读已提交,简称 RC。REPEATABLE READ
:可重复读,简称 RR。SERIALIZABLE
:可串行化。不一样的隔离级别,分别能解决一部分事务问题,具体状况可查看下面的表格。
READ UNCOMMITTED
:会发生脏读、不可重复读、幻读的问题。READ COMMITTED
:会发生不可重复读、幻读的问题,不会发生脏读的问题。REPEATABLE READ
:会发生幻读的问题,不会发生脏读、不可重复读的问题。SERIALIZABLE
:脏读、不可重复读、幻读的问题都不会发生。隔离级别越低,可能产生的问题越严重;隔离级别越高,并发的性能也会越低。不过不多有数据库厂商遵循这些SQL标准,好比Oracle数据库就不支持READ UNCOMMITTED
和REPEATABLE READ
的事务隔离级别。
InnoDB存储引擎支持SQL标准的四种隔离级别,不过InnoDB在REPEATABLE READ
隔离级别下就能避免幻读问题的发生。
MySQL的默认隔离级别为REPEATABLE READ
,能够经过下面的语句修改事务的隔离级别:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL <level>;
level 可选值:
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
复制代码
若是想在服务器启动时修改事务的默认隔离级别,能够在[mysqld]
下添加参数transaction-isolation
。
[mysqld]
transaction-isolation = READ-COMMITTED
复制代码
查看当前会话的事务隔离级别:
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
复制代码
查看全局的事务隔离级别:
mysql> SELECT @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
复制代码
数据库底层有一套复杂的机制来实现事务的ACID
特性,这节作个简单说明,接下来会用几篇单独的文章来介绍。
一、持久性(D)
事务的持久性经过数据库的redo log
来实现,redo log
称为重作日志。在更新Buffer Pool
中的数据页时,会同时记录对应的 redo log,这样就算脏页没有刷盘,在MySQL宕机重启时,也能够经过 redo log 来恢复数据。
二、原子性(A)
事务的原子性经过数据库的undo log
来实现,undo log
称为撤销日志或回滚日志。在一个事务中进行增删改操做时,都会记录对应的 undo log。
并且 undo log 造成的版本链还用于实现多版本并发控制(MVCC)
,InnoDB的RC
和RR
隔离级别就是是基于MVCC
来实现高性能事务,并且经过MVCC
来避免幻读的发生。
三、隔离性(I)
事务的隔离性由锁
来实现,不一样的加锁方式,能够实现不一样的事务隔离机制。
四、一致性(C)
事务的一致性须要两个层面来保证:
AID
三大特性,才有可能实现一致性。例如,原子性没法保证,显然一致性也没法保证。能够看到,原子性、持久性、隔离性是数据库层面保证持久性的手段。所以,咱们后面会分别针对原子性、持久性、隔离性单独用一篇文章来学习。