事务的特性

一批数据同时成功或者同时失败,这类需求就可以简单的理解为具有事务性,也就是ACID

  • A (Atomicity, 原子性):一个事务中的所有操作要不全部成功,要不全部失败,不能出现部分成功,部分失败的情况。
  • C(Consistency,一致性):数据库设计上这个含义比较模糊,简单可以理解为财务的对账一样,两边数据的加加减减必须要能保持一致。
  • I(Isolation,隔离性):主要是针对在并发访问数据时要有一定的隔离性,在MySQL中隔离性也是分等级的,根据不同的业务需求选择不同的隔离性,主要依靠锁+MVCC来实现,隔离性越强,数据库的吞吐就越差。
  • D(Durability,持久性):事务一旦提交,数据将会保存到数据库中,此时如果数据库发生错误,也不会造成数据丢失。

原子性的实现

MySQL主要是利用undo日志来实现原子性。

undo log

undo log是逻辑日志,在操作数据之前,会首先将数据通过记录undo log的方式存储起来,然后再对数据进行修改,当修改时系统出现异常或者用户执行了回滚,则可以通过undo log来把数据恢复到之前的状态。

undo log记录的就是相反的日志,比如执行delete时,记录的则是对应的insert,当执行insert时,记录则是对应的delete,当执行update时,记录则是相反的update记录。

流程

假设数据库有一条记录:age=1,现在要将age修改为2,流程如下:

1、事务开启。
2、记录undo日志,update age=1。
3、执行update,修改age=2。
4、事务提交、写入磁盘。
5、事务结束。

如果在写入磁盘前,发生异常,则可以通过undo日志进行数据回滚。

持久性的实现

MySQL持久性的实现主要是通过另外一种日志记录:redo log

redo log

redo log又叫重做日志,是InnoDB存储引擎中产生的,记录了对数据库中每个页的修改,redo log分为两部分:一部分是在内存中的缓冲日志 (redo log buffer),一部分是在磁盘上的文件日志 (redo log file),内存是会丢失的,而磁盘是永久的。

流程

1、事务开始。
2、执行update,age = 2。
3、redo日志,age = 2。
4、redo日志写入磁盘。
5、事务提交,数据写入磁盘。
6、事务结束。

实际上,在MySQL中为了提高写入的性能,数据并不是直接写入磁盘的,不然也不会存在持久性的问题,每次数据的写入实际上都是先写入一个缓冲区,然后再由操作系统定时的将数据写入磁盘,这样就避免了频繁的写磁盘的操作,提升了数据写入的效率,但是这样做也就带来了数据丢失的风险,如果数据在写入磁盘前,MySQL挂了,则会导致缓冲区的数据丢失。

所以redo log就可以解决上述的问题,每次数据提交写磁盘前,总会先写入redo log,如果缓冲区数据丢失,就可以通过redo log进行找回。

redo log数据也是先写入redo log buffer中,一般再由定时任务或者事务提交触发buffer中的数据写入磁盘,也就是redo log file中。

log buffer 和 os buffer

mysql 事务ID执行的sql mysql事务id内部生成机制_持久性

innodb_flush_log_at_trx_commit

具体buffer中的日志何时写入file中,由innodb_flush_log_at_trx_commit参数决定。

innodb_flush_log_at_trx_commit为0、1、2时分别表示如下写入方案。

mysql 事务ID执行的sql mysql事务id内部生成机制_mysql 事务ID执行的sql_02

innodb_flush_log_at_trx_commit默认为1,保证数据持久性,但性能也相对最低。

隔离性的实现

隔离性的实现主要是依靠MVCC+锁来实现。

理解隔离性实现原理之前,必须要先弄清楚事务的隔离级别。

事务的隔离级别

事务的四个隔离级别(mysql默认可重复度,oracle默认读已提交)

  • read uncommitted (读未提交)
    会产生:脏读、幻读、不可重复读
  • read commited (读已提交)
    会产生:幻读、不可重复读
  • repeatable read (可重复读)
    会产生:幻读
  • seariable (串行执行)
    不会产生任何异常

不同的隔离级别产生的现象

脏读

一个事务会读到另一个未提交的事务数据。

不可重复读

一个事务内多次读取到的数据不一致,A事务第一次读到值是1, B事务把1修改成2,并且提交了,A事务第二次读到值也变成了2,针对update。

幻读

一个事务内多次读取到的数据不一致,第一次读到1条,第二次读到2条。针对insert,delete,数据行数发生了变化。

下面通过案例演示,帮助理解脏读、幻读、不可重复度具体产生的问题现象。

脏读问题测试

1、设置事务隔离级别

A:set session transaction isolation level read uncommitted;
A:start transaction;
A:insert into tran_test values(4,'zhaoliu');
A:select * from tran_test; --可以查询到最新插入的数据
B:set session transaction isolation level read uncommitted;
B:select * from tran_test; -- 也可以查询A最新插入的数据,尽管A的此时的事务还未提交

mysql 事务ID执行的sql mysql事务id内部生成机制_持久性_03

不可重复读问题测试

重复脏读的流程发现,新建的会话窗口是读不到其他会话中未提交的事务的。

mysql 事务ID执行的sql mysql事务id内部生成机制_数据_04

但是会产生不可重复读问题

A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 1; --结果为zhangsan
B:start transaction;
B:update tran_test set name = 'zs' where id = 1; --把id为1的name修改为zs
A:select * from tran_test where id = 1; -- B未提交所以A此时查询结果还是zhangsan
B:commit;
A:select * from tran_test where id = 1; -- B事务提交后,A再查询此时结果已经变成了zs

A再同一个事务中,两次查询结果不一致。

幻读问题测试

重复上限测试,不可重复读和脏读问题都不存在。

但是会产生幻读。

A:set session transaction isolation level read committed;
A:start transaction;
A:select * from tran_test where id = 4; --结果为空
B:start transaction;
B:insert into tran_test values(4,'zhaoliu'); -- B插入一条id=4的数据
A:select * from tran_test where id = 4; --结果依旧为空
B:commit; -- B提交事务
A:select * from tran_test where id = 4; --结果依旧为空
A:insert into tran_test values(4,'zhaoliu'); -- 插入失败,报主键冲突
insert into tran_test values(4,'zhaoliu')
> 1062 - Duplicate entry '4' for key 'PRIMARY'
> 时间: 0s

A事务在查询时数据明明不存在,但插入却报主键冲突,就像出现幻觉一样。

什么是MVCC?

MVCC(多版本并发控制),实现并发访问的一种方式,在mysql的innodb引擎读已提交和可重复读两种隔离级别下,事务在select时实际上读取的是版本链中的数据。

什么是版本链?

我们先来理解一下版本链的概念。在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:

trx_id
这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。

roll_pointer
每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

比如现在有一个trx_id = 1,那么当修改时,则会在undo日志中新增一条记录,trx_id = 2,roll_pointer = (trx_id = 1的地址)

ReadView

ReadView主要是用来处理读已提交和可重复读两种场景。

ReadView通过一个链表来记录当前已开启但还未提交的trx_id(当前活动事务)。

举例说明:

当前trx_id = 3,修改age=2,此时事务还未提交,版本链如下:

mysql 事务ID执行的sql mysql事务id内部生成机制_持久性_05


当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:

mysql 事务ID执行的sql mysql事务id内部生成机制_数据_06


现在关键部分来了,看看读已提交和可重复读两种不同隔离级别的做法。

读已提交:

当另一个事务再次select时,会重新生成一次ReadView,由于当前活动事务id为4,所以ReadView中记录的trx_id = 4,然后再根据之前的方式,最终会查询到age = 3的记录。

可重复读:

当另一个事务再次select时,并不会重新生成ReadView,而是继续使用上一次的ReadView,那么ReadView中记录的当前活动事务id就还是为3,那么最终还是查询到age = 2的记录。

所以说在读已提交的隔离级别下,每次select时都会重新生成一个ReadView,而在可重复读时,只会在第一次select时生成ReadView,之后事务中的每次select,都只会复用第一次生成的ReadView。

一致性的实现

对于一致性的问题,最开始也已经解释了,它是一个比较模糊的概念,原子性、持久性、隔离性都是为了保证一致性的实现,或者说一致性更侧重于业务层面由开发人员的控制。