1 事务四大特征
一般来说,事务是必须满足4个条件(ACID):原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
事务是最小的执行单位,不允许分割。
原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。(比如:A向B转账,不可能A扣了钱,B却没有收到)
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
总的来说,InnoDB存储引擎的原子性是通过undo log来保证,事务的持久性是通过redo log来实现的,事务的隔离性是通过读写锁+MVCC机制来实现的。而原子性、持久性、隔离性都只是手段,其目的是为了实现一致性,MySQL满足的是其自身内部数据的一致性,而对于具体业务的一致性,还需要应用程序本身遵守一致性规约。
MySQL事务实现的机制是WAL(Write-ahend logging,预写式日志),这是比较主流的方案。
在MySQL服务异常崩溃后,使用WAL,可以在系统重启之后,通过比较日志和系统状态来决定继续之前的操作或者是撤销之前的操作。
redo log(重做日志):每当操作时,在磁盘数据变更之前,将操作写入redo log,这样当系统奔溃重启后可以继续执行。
undo log(回滚日志):当一个事务执行一半无法继续执行时,可以根据回滚日志将之前的修改恢复到变更之前的状态。
2 undo log && redo log 的理解?
2.1 undo log
回滚日志,在数据库事务开始之前,MYSQL会去记录更新前的数据到undo log文件中。如果事务回滚或者数据库崩溃时,可以利用undo log日志中记录的日志信息进行回退。
undo log生命周期:
undo log产生: 在事务开始之前
undo log销毁: 当事务提交之后
undo log并不能立马被删除,而是放入待清理的链表,由purge线程判断是否由其他事务在使用undo log 段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。
注意: undo log也会生产redo log,undo log也要实现持久性保护。
undo log是逻辑日志,实现事务的原子性。undo log记录的是事务[开始前]的数据状态,记录的是更新之前的值。
1、undo log日志可以实现事务的回滚操作
我们在进行数据更新操作的时候,不仅会记录redo log,还会记录undo log,如果因为某些原因导致事务回滚,那么这个时候MySQL就要执行回滚(rollback)操作,利用undo log将数据恢复到事务开始之前的状态。
如我们执行下面一条删除语句:
delete from book where id = 1;
那么此时undo log会生成一条与之相反的insert 语句【反向操作的语句】,在需要进行事务回滚的时候,直接执行该条sql,可以将数据完整还原到修改前的数据,从而达到事务回滚的目的。
再比如我们执行一条update语句:
update book set name = "三国" where id = 1; ---修改之前name=西游记
此时undo log会记录一条相反的update语句,如下
update book set name = "西游记" where id = 1;
如果这个修改出现异常,可以使用undo log日志来实现回滚操作,以保证事务的一致性。
如上图所示:
当事务A进行一个update操作,将id=1修改成id=2。首先会修改buffer pool中的缓存数据,同时会将旧数据备份到undo log buffer中,记录的是还原操作的sql语句。此时如果事务B要查询修改的数据,但是事务A还没有提交,那么事务B就会从undo log buffer中,查询到事务A修改之前的数据,也就是id=1。此时undo log buffer会将数据持久化到undo log日志中(落盘操作)。undo日志持久化之后,才会将数据真正写入磁盘中,也就是写入ibd的文件中。最后才会执行事务的提交。
2.2 redo log
https://blog.csdn.net/zs18753479279/article/details/127245352
3 事务的隔离级别
READ UNCOMMITTED 读取未提交内容:在这个隔离级别,所有事务都可以"看到"未提交事务的执行结果。
READ COMMITTED 读取提交内容:大多数数据库系统的默认隔离级别(但是不是MySQL的默认隔离级别),一个事务从开始到提交前,所做的任何数据改变都是不可见的,除非已经提交。这种隔离级别会导致“不可重复读"。这意味着用户运行同一个语句两次,看到的结果是不同的。
REPEATABLE READ 可重复读:MySQL数据库默认的隔离级别。这种隔离级别可以避免“不可重复读取”,达到可重复读取,不过这会导致另外一个棘手问题”幻读"。通过多版本并发控制机制解决了幻读问题。
SERIALIZABLE可串行化:该级别是最高级别的隔离级。它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简而言之,SERIALIZABLE是在每个读的数据行上加锁。在这个级别,可能导致大量的超时Timeout和锁竞争Lock Contention现象。
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个修改后的数据。
不可重复读(前后多次读取,数据内容不一致)是指在一个事务内,多次读同一数据。在一个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
幻读(前后多次读取,数据总量不一致)幻读是一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
3.1 不可重复读和幻读到底有什么区别呢?
(1)不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
幻读和不可重复读都是指的一个事务范围内的操作受到其他事务的影响了。只不过幻读是重点在插入和删除,不可重复读重点在修改。
4 事务实现方式-MVCC(多版本并发控制)
MVCC是MySQL的的多版本并发控制即multi-Version Concurrency Controller,数据库并发场景有三种,分别为:
1、读读:不存在任何问题,也不需要并发控制
2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读
3、写写:有线程安全问题,可能存在更新丢失问题
MVCC是一种用来解决读写冲突的无锁并发控制,也就是为事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决以下问题:
1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并
发读写的性能;
2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题。
MVCC最大的好处:读不加锁,读写不冲突,极大的增加了系统的并发性能。
两种读的方式:
1快照读:读取的是历史版本的记录; SQL语句:select …
2当前读:读取的是最新版本的记录;SQL语句:1select … lock in share mode; 2select … for update; 3 update 4 delete 5 insert;
引例:
事务A | 事务B |
---|---|
select | select |
update -> commit | |
select (可以读到最新版本的记录吗?) |
RC: 是可以读到的
RR: 是读不到的 --------> 为什么?引出MVCC(多版本的并发控制机制)【可见性算法】
4.1 MVCC实现原理是什么?
mvcc的实现原理主要依赖于记录中的三个隐藏字段,undo log,readView来实现的。
4.1.1隐藏字段
行记录都会包括用户看不到隐藏字段:
{
1.DB_TRX_ID:最后一次创建修改该记录的事务ID
2.DB_ROW_ID:隐藏的主键ID
3.DB_ROLL_PTR:回滚指针+undo log
}
4.2 对undo log的理解
undo log被称之为回滚日志,数据历史版本的状态。
…依次,形成一个链表:当不同的事务对同一条数据修改,会导致该记录的undo log形成一个线性链表,其中链表首部:最新的历史版本记录。链表尾部:多个历史版本最早的历史记录。
4.3 readView
Read View是事务进行快照读操作的时候生产的读视图,Read View的最大作用是用来做可见性判断的,也就是说当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undo log中某个版本的数据。
事务进行快照读,产生的视图。主要包括以下字段:
{
1.trx_list: 系统活跃的事务ID
2.up_limit_id:列表中最小的事务ID
3.low_limit_id:系统尚未分配的下一个事务ID
}
实际场景:
事务1 | 事务2 | 事务3 | 事务4 |
---|---|---|---|
开启 | 开启 | 开启 | 开启 |
update | |||
快照读(能不能读取到修改的记录值) |
{
1.trx_list: 1,2,3
2.up_limit_id:1
3.low_limit_id:5
}
DB_TRX_ID:4
这里引入一个算法:
1、首先比较DB_TRX_ ID < up_ limit_ id,如果小于,则当前事务能看到
DB_ TRX_ ID所在的记录,如果大于等于进入下一个判断;
2、接下来判断DB_ TRX_ ID >= low_ limit id,如果大于等于则代表
DB_ TRX_ ID所在的记录在Read View生成后才出现的,那么对于当前事
务肯定不可见,如果小于,则进入下一步判断;
3、判断DB_TRX_ ID是否在活跃事务中:
如果在,则代表在Read View生成时刻,这个事务还是活跃状态,还没有commit, 修改的数据,当前
事务也是看不到;
如果不在,则说明这个事务在Read View生成之前就已经开始commit,那么修改的结果是能够看见的。
所以,快照读是可以读到最新的记录。
4.4 这里解释两个重要概念:RC&&RR
因为Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同。
1.在RR级别下的某个事务对某条记录的第一次快照读会创建一个快照即Read View:将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。
3、在RC级别下,事务每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总结:在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View.