前言
关于事务,可能大家和我一样知道一些基本的概念,包括4大特性等,但是对于他们理解的可能不够深入,包括MySQL是用什么样的机制去实现这样的特性,都可能很懵逼,那么本文就和大家一起来排坑,从事务的基础知识点到基本的实现,我们都需要做到心中有数。
事务的概念
事务(Transaction)是一组逻辑操作单元,使数据从一种状态变换到另一种状态。这里的逻辑操作单元是根据我们的业务场景来定的,比如银行转账的场景,A用户转账给B用户500元,会出现下面两条SQL,我们把这样一组SQL形成的数据操作看作一个事务。
# A用户转账给B用户500元 update account set money = money - 500 where name = 'A'; # 服务器宕机等等,或者业务层代码报错 update account set money = money + 500 where name = 'B';
一个事务中的SQL要么都执行,要么都不执行。比如上面的例子中,A用户账户扣了500元后,突然宕机了,第2个SQL未执行,那么导致了A用户平白无故不见了500元,这样影响的后果是非常灾难的。
MySQL中只有InnoDB
存储引擎支持事务,其他引擎不支持,这也是为什么大家都是用的InnoDB
存储引擎,因为事务的保障真的非常非常重要,它是保证业务正常、稳定运行的基础。
事务的使用
使用事务有两种方式,分别为 显式事务
和 隐式事务
。
显式事务
- ****
START TRANSACTION
或者BEGIN
,作用是显式开启一个事务。
mysql> BEGIN; #或者 mysql> START TRANSACTION;
START TRANSACTION
语句相较于 BEGIN
特别之处在于,后边能跟随几个 修饰符
:
READ ONLY
:标识当前事务是一个只读事务
,也就是属于该事务的数据库操作只能读取数据,而不能修改数据, 如START TRANSACTION READ ONLY;
。READ WRITE
:标识当前事务是一个读写事务
,也就是属于该事务的数据库操作既可以读取数据, 也可以修改数据, 默认为该模式,如START TRANSACTION READ WRITE
。WITH CONSISTENT SNAPSHOT
:立即启动一致性读视图,START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT
。
- 一系列事务中的操作
- 提交事务
# 开启事务 mysql> BEGIN; # 数据库操作 mysql> .......; # 提交事务。当提交事务后,对数据库的修改是永久性的。 mysql> COMMIT;
- 回滚事务rollback
# 开启事务 mysql> BEGIN; # 数据库操作 mysql> .......; # 回滚事务。即撤销正在进行的所有没有提交的修改 mysql> ROLLBACK;
- 回滚事务到保存点
SAVEPOINT
# 开启事务 mysql> BEGIN; # 数据库操作 mysql> .......; # 设置保存点 SAVEPOINT name mysql> SAVEPOINT s1; # 数据库操作 mysql> .......; # 将事务回滚到某个保存点 ROLLBACK TO [SAVEPOINT] mysql> ROLLBACK TO s1
其中关于SAVEPOINT相关操作有:
- 在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点。
SAVEPOINT
保存点名称; - 删除某个保存点
RELEASE SAVEPOINT 保存点名称
;
隐式事务
autocommit
引起隐式事务
MySQL中有一个系统变量 autocommit
:
mysql> SHOW VARIABLES LIKE 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.01 sec)
当然,如果我们想关闭这种 自动提交
的功能,可以使用下边两种方法之一:
- 显式的的使用
START TRANSACTION
或者BEGIN
语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。 - 把系统变量
autocommit
的值设置为OFF
,就像这样:
SET autocommit = OFF; #或 SET autocommit = 0;
- 特殊语句导致事务隐式提交
在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table
)、lock tables
语句等等。即:
BEGIN; SELECT ... # 事务中的一条语句 UPDATE ... # 事务中的一条语句 ... # 事务中的其他语句 CREATE TABLE ... # 此语句会隐式的提交前边语句所属于的事务
理解事务的ACID四大特性
为了保证业务的安全、平稳运行,比如数据不一致或者数据丢失等情况,这就需要我们的事务满足一定的特性,即原子性(atomicity
),一致性(consistency
),隔离型(isolation
)和持久性(durability
)4大特性。
实际上,在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的NDB Cluster事务不满足持久性和隔离性;InnoDB默认事务隔离级别是可重复读,不完全满足隔离性;Oracle默认的事务隔离级别为读已提交,也不完全满足隔离性……因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。
原子性(Atomicity)
说明:
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。比如A账户减去500元,而B账户增加500元时操作失败,这时候系统应该进行回滚,退回到事务开始前,也就是A账户减去500元前的状态。
实现机制:
那如何实现事务的原子性,即事务可以回滚呢?
是不是可以在事务中执行SQL的时候,在日志文件中记录下对应数据修改前的值,当事务执行update时,其生成的日中包含被修改行的主键、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
上面提到的这个日志文件就是undo log
,undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
一致性(Consistency)
说明:
一致性是指事务执行前后,数据从一个合法性状态
变换到另外一个合法性状态
。这里的合法性状态是满足一定的约束,是根据具体的业务决定的。比如:A账户有100元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时的数据是不一致的,为什么呢?因为你定义了一合法性个状态,要求A+B的总余额必须不变。
实现原理:
MySQL innoDB引擎中数据的一致性和原子性一样,也是用事务日志undo log
实现的。****
持久性(Durability)
说明:
指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
实现原理:
事务的持久性是通过另外一个事务日志redo log
实现的。
- 当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到redo log中
- 然后再对数据库中对应的行进行修改
- 即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
隔离性(Isolation)
事务的隔离性是4大特性中最复杂的一种,它主要关注的是不同事务之间的相互影响,特别是在不同事务并发执行的情况。
如果完全不做隔离性的话,并发事务会引发下面的一系列问题。
事务并发问题
- 脏读
脏读是指的一个事务读取到了另外一个事务未提交的数据。
- Session B中的事务先将
studentno
列为1的记录的name
列更新 为'张三', - 然后Session A中的事务再去查询这条
studentno
为1的记录,读到未提交的值'张三' - Session B中的事务稍后进行了回滚
- 这时Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为 脏读 。
- 不可重复读
不可重复读是指一个事务中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。
- 幻读( Phantom )
幻读是指一个事务中按照某个条件先后两次查询数据库,两次查询结果的条数不同,更加强调的是读到了之前没有读到的数据,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
事务隔离级别
事务之间严格的隔离肯定是不同的事务之间可以完全互不干扰,比如A事务"干活"的时候,B事务先等等,等A干完了再干,这样肯定不会有上面一系列并发问题,但是这会导致性能十分糟糕。
因此,我们引入了隔离级别,SQL 标准定义了4种事务隔离级别,包括读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。隔离级别越高,性能也就越差。
- 读未提交(read uncommitted)
读未提交指一个事务还没提交时,它做的变更就能被别的事务看到。该隔离级别下不能避免脏读、不可重复读、幻读。
- 事务1开启了
read uncommitted
的隔离级别 - 事务2读取到了事务1未提交的数据
- 读提交(read committed)
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。该隔离级别下可以避免脏读,但不可重复读、幻读问题仍然存在。
- 事务开启了
read committed
的隔离级别 - 事务1前后两次读取到的值不一样,发生了不可重复读问题
- 可重复读(repeatable read)
可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提 交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍 然存在。这是MySQL的默认隔离级别。
- 事务开启了
repeatable read
的隔离级别 - 事务1一开始明明没有id=3的数据,但是无法插入,发生了幻读现象
- 序列化(serializable)
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。该隔离级别下,所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
小结:
不同的隔离级别能解决的并发问题,如下图所示:
隔离级别越高,性能也会越差,这就需要我们去做取舍。
实现机制
隔离性的实现机制相对来说还是比较复杂的,根据事务的读写场景不一致,我们大致可以分为两类:
- (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据。该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
- (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
MVCC全称Multi-Version Concurrency Control
,即多版本的并发控制协议。在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)。每个事务会有一个自己的ReadView
, 所谓ReadView
,是指事务在某一时刻给整个事务系统打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见。
具体关于MVCC机制将在后面的文章深入剖析。