事务的特性
一批数据同时成功或者同时失败,这类需求就可以简单的理解为具有事务性,也就是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
innodb_flush_log_at_trx_commit
具体buffer中的日志何时写入file中,由innodb_flush_log_at_trx_commit参数决定。
innodb_flush_log_at_trx_commit为0、1、2时分别表示如下写入方案。
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的此时的事务还未提交
不可重复读问题测试
重复脏读的流程发现,新建的会话窗口是读不到其他会话中未提交的事务的。
但是会产生不可重复读问题
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,此时事务还未提交,版本链如下:
当前ReadView记录了trx_id = 3的事务,此时另一个事务执行select查询语句,于是从版本链中寻找,第一条trx_id = 3,已经在ReadView中,所以不能访问,于是找下一条,trx_id = 2,小于ReadView记录中的最小值,所以可以访问,于是返回age = 2的记录。现在假设刚才trx_id = 3提交了,然后又新建了一个事务,修改age = 4,trx_id = 4,并且还未提交,版本链如下:
现在关键部分来了,看看读已提交和可重复读两种不同隔离级别的做法。
读已提交:
当另一个事务再次select时,会重新生成一次ReadView,由于当前活动事务id为4,所以ReadView中记录的trx_id = 4,然后再根据之前的方式,最终会查询到age = 3的记录。
可重复读:
当另一个事务再次select时,并不会重新生成ReadView,而是继续使用上一次的ReadView,那么ReadView中记录的当前活动事务id就还是为3,那么最终还是查询到age = 2的记录。
所以说在读已提交的隔离级别下,每次select时都会重新生成一个ReadView,而在可重复读时,只会在第一次select时生成ReadView,之后事务中的每次select,都只会复用第一次生成的ReadView。
一致性的实现
对于一致性的问题,最开始也已经解释了,它是一个比较模糊的概念,原子性、持久性、隔离性都是为了保证一致性的实现,或者说一致性更侧重于业务层面由开发人员的控制。