文章目录
1 数据库的事务隔离级别有那些
1.1 事务四大特性
在说事务的隔离级别之前,让我们先了解一下数据库事务的四大特性(ACID)分别是什么:
A.原子性(Atomicity):原子性指数据库事务是一个不可分割的操作,要么全部执行成功,要么全部执行失败。在mysql中原子性是通过undolog实现。
C.一致性(Consistency):官网上事务一致性的概念为:事务必须使数据库从一个一致性状态转换到另外一个一致性状态,也就是说事务按照预期生效,数据的状态是预期的状态。数据库的一致性由原子性、隔离性和持久性维护。
I.隔离性(Isolation):在多个用户并发访问数据库时,数据库要为每一个用户开启一个事务,多个并发事务之间是隔离的。mysql通过mvcc和锁实现隔离性。
D.持久化(Durability):一旦一个事务被提交,事务对数据库中的数据的修改是持久性的。mysql通过redo log实现持久化。
1.2 隔离级别
针对数据库事务的隔离性有四种隔离级别,分别是:读未提交、读已提交、可重复读和串行化,对于不同隔离级别,数据库产生的问题如下:
隔离级别 | 脏读 | 不可复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITED | √ | √ | √ |
READ-COMMITED | x | √ | √ |
REPEATABLE-READ | x | x | √ |
SERIALIZABLE | x | x | x |
mysql的默认隔离级别是可重复读,会产生幻读问题。
下面对脏读、不可复读和幻读进行解释一下:
- 脏读:事务能够读取到未提交的数据,这种情况为脏读。
- 不可复读:当一个事务在执行过程中,数据被另外一个事务修改,造成本次事务多次读取的信息不一致,这种情况为不可复读。
- 幻读:当事务A读取某一个范围的数据时,另外一个事务B在这个范围内插入新的记录并提交,事务A再次读取该范围的数据时,就会产生幻读。
2.MVCC的实现原理
MVCC全是为Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发访问数据库的控制方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC在MySQL的InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
在了解MVCC的实现是,让我们先了解一下当前读和快照读:
2.1当前读
读取的记录是最新的版本,同时读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。例如:
产生当前读的情况:
1.select * from table_1 for update;
2.update, insert ,delete(排他锁)
3.select * from table lock in share mode(共享锁)
2.2快照读
对比当前读,不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不能是串行级别,因为在串行级别下快照读会退化为当前读,因为在串行级别下读写操作都是互斥的。快照读是为了提高数据库的并发查询能力,它是基于MVCC来实现的。在并发查询时,避免了加锁操作从而降低了开销,但是,因为是快照,所以读到的数据有可能不是最新版本,而是某一个历史版本。快照读是MySQL为实现MVCC的一个非阻塞读功能。
2.3 MVCC的三个重要组件
MVCC是为了解决并发事务下对不加锁的读写操作冲突的问题,即:在并发事务下使读写操作不冲突,在MVCC模块中有三个重要的组件:
- 三个隐式字段:在MySQL中,每行数据除了原始数据外,还隐式定义了DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段,而MVCC主要使用如下三个隐式字段。
DB_TRX_ID:6 byte最新修改(增删改)的事务ID,事务ID是递增的;
DB_ROLL_PTR:7 byte的回滚指针,指向这条记录的上一个版本(存储于rollback segment里),配合undo log使用;
DB_ROW_ID: 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引,实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
- undo log:undo log被称为回滚日志,它是一个逻辑日志,例如:如果是一个insert操作,会记录一条delete操作日志;如果是一个delete操作会记录一条inset操作日志;而如果是一个update操作,则会记录一条相反update操作日志。undo log日志有两种:
1.insert undo log:事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃。
2.update undo log:事务在进行update或delete时产生的undo log不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
undo log版本链是指多个事务修改同一条数据记录时,会保留历史的undo log,并用trx_id和roll_pointer两个隐式字段将undo log串联起来。
- read view: 读试图,是事务进行快照读操作的时候产生的读试图,当该事务执行快照读的那一刻会生成一个数据系统的当前快照,记录并维护系统当前活跃的事务ID(事务ID的值是递增的)。
Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的最新老版本数据。
Read View的可见性规则如下所示:
首先要知道Read View中的三个全局属性:
trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID(1,2,3)
up_limit_id:记录trx_list列表中事务ID最小的ID(1)
low_limit_id:Read View生成时刻系统尚未分配的下一个事务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,那么修改的结果是能够看见的。
2.4 READ-COMMITED和REPEATABLE-READ在InnoDB引擎下有什么不同
在读已提交和可重复读级别下read view生成的时机是不同的:
- 在读已提交的隔离级别下:某一个事务每次的快照读操作都会新生成一个read view,如果一个事务提交之后,新的快照读中会有该事务的提交记录,这就是我们在该隔离级别下能不可复读的原因。
- 在可重复读的隔离级别下:某一个事务下的第一次快照读会创建一个快照read view,此后每一次快照读都会重复使用第一次创建的read view,所以任何其他事务在该事务期间的修改操作对该read view是不可见的,所以可重复读解决了不可复读的问题。
3 MySQL幻读问题如何解决
3.1 幻读是如何产生的
通过了解上面MVCC的实现机制,了解了当前读和快照读,以及在可重复读隔离级别下read view是快照读,对晚于该快照的事务是不可见的,那么读者可能会疑惑为什么还会产生幻读现象呢?答案是在事务期间产生了当前读,导致将其他事务提交插入的数据被读取到。例如:
时间 | 事务1 | 事务2 |
---|---|---|
begin; | ||
T1 | select * from user where age = 20;2个结果 | |
T2 | insert into user values(25,‘25’,20);commit; | |
T3 | select * from user where age =20;2个结果 | |
T4 | update user set name=‘00’ where age =20;此时看到影响的行数为3 | |
T5 | select * from user where age =20;三个结果 |
1.事务1开始事务后,在T1进行快照读操作,查询到有2个结果;
2.在T2时刻,事务2提交了新的数据;
3.在T3时刻,事务1再次进行快照读,发现查询到的数据仍是2个结果,没有幻读产生;
4.在T4时刻,事务1通过相同的查询条件对数据进行修改,发现此时受影响的行数为3,而我们快照读的结果是2,则可知发生了幻读;
5.在T5时刻,事务1再次进行快照读,则可发现查询到3个结果。
至此,通过两个事务演示了幻读的产生。
由上面的例子可以,幻读的产生是因为在可重复读的隔离级别下,事务1开启并进行了快照读生成了read view后,其他事务插入并提交了同查询范围内的新数据,在其他事务提交后,事务1发生当前读时就会产生幻读现象。
3.2 如果解决幻读
由上文可知,只要保证在可重复读的隔离级别下,不进行当前读就可以避免幻读,但是具体业务中不可能100%避免当前读的产生。 而如果将隔离级别上调至串行化将大大降低数据库的性能,这不是我们想要看到的,那么我们要如何解决幻读呢?
我们已经知道,幻读的产生原因了,那么我们如果能保证在当前读发生在其他相关新增事务提交之前,并保证两个事务互斥关系就可以解决幻读了。例如:
时间 | 事务1 | 事务2 |
---|---|---|
begin; | ||
T1 | select * from user where age =20 for update; | |
T2 | insert into user values(25,‘25’,20);此时会阻塞等待锁 | |
T3 | select * from user where age =20 for update; |
此时,可以看到事务2被阻塞了,需要等待事务1提交事务之后才能完成,其实本质上来说采用的是间隙锁的机制解决幻读问题。