1、什么是数据库事务
事务是由一个有限的数据库操作序列构成,这些操作要么全部都执行,要么全部都不执行,通俗的说同生共死,是一个不可分割的工作单位。
事务的存在,其实就是为了保证数据库的数据一致性
2、事务的特性
ACID,分别对应原子性,一致性、隔离性、持久性
原子性:事务作为一个整体被执行,包含在其中的数据对数据库的操作,要么全部执行,要么都不会执行
一致性:事务开始之前和事务结束之后,数据不会被破坏,A给B转100元,不管成功与否,A和B的总金额是不变的
隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务所干扰,多个并发事务之间要相互隔离
持久性:事务完成提交后,改事务对象数据库所做的更改,将会永久的保存在数据库之后
3、事务并发带来的问题
事务的并发会引起"脏读","不可重复读","幻读"的并发问题
3.1 脏读
概述:如果一个事务读取到了另外一个未提交事务修改后的数据,则会导致前一个事务查询数据前后不一致的现象,即脏读
示例:A和B俩个事务
- A的余额是100,事务A正在查询tom账户的余额
- 事务B先扣减tome的余额,扣了10,但是还没有提交
- 事务A读取tom账户的余额,发现只有90
这即是脏读,因为事务A读取到了事务B未提交的数据,从而导致前后数据不一致的现象
3.2不可重复读
概述:如果事务A读取到了另外一个提交事务修改后的数据,导致事务A前后查询数据不一致的现象,即不可重复读
示例:
- 事务A查tom的数据,结果是100
- 事务B对tom账号余额进行扣减10元,然后提交事务
- 事务A再次查询tom的账号余额,发现余额变成了90
事务A被事务B干扰到了,在事务A的范围内,俩个相同的查询,读取同一条数据,返回了不同的数据,即不可重复读
3.3 幻读
概述:如果一个事务根据搜索条件查询出一批数据,在该事务未提交前提下,另外一个事务B写入了一些符合条件的数据,这样导致事务A前后查询,发现数据量有所变化,即所谓的幻读
示例:
- 事务A先查询id大于2的账号记录,得到了2条记录‘
- 事务B插入一条id=4的数据,并且提交事务
- 事务A再去查询,却得到了三条数据
事务A查询一个范围内的结果集,另一个并发事务再这个范围内插入新的数据,并提交事务,然后事务A再去查询,发现俩次读取的数据结果集是不一致的
4、事务的隔离级别
为了解决并发事务存在的脏读、幻读、不可重复读等问题,数据库设计了四种隔离级别,分别是读未提交,读已提交,可重复读,串行化
4.1 读未提交
该隔离级别限制俩个数据不能同时修改,但是修改数据的时候,即使事务未提交,都是可以被别的事务读取到,这种隔离级别有脏读,幻读,不可重复读的问题
4.2 读已提交
读已提交隔离级别,当前事务只能读取到其他事务已经提交的数据,这种隔离级别解决了脏读,但是不可重复读和幻读的现象解决不了
4.3 可重复读
该隔离级别,限制了读取数据的时候,不可以进行修改,解决了不可重复读的问题,但是读取范围数据的时候,是可以插入数据,所以依旧存在幻读的现象
4.4 串行化
事务的最高隔离级别,所有事务都是进行串行化顺序执行的,可以避免脏读,不可重复读,幻读的并发问题,但是这种隔离级别下,事务执行很耗性能
针对这四种事务的隔离级别,数据库是如何实现的
数据库是通过枷锁来实现事务的隔离性,串行化隔离级别就是通过枷锁实现的,但是频繁加锁,导致读取数据的时候,没法进行修改,修改数据的时候没法读取,大大降低了数据的性能
而数据库为了在性能和并发问题俩者之间得到一个均衡,引用了MVCC多版本并发控制,可以让读取数据的同时进行修改数据,修改数据的时候同时能够读取
5、MVCC 多版本并发控制
数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是每一条记录都具有多个版本同时存在,在某个事物对其进行操作的时候,需要查看这一条记录的隐藏列事物版本ID,比对事物id并根据事物隔离级别去判断读取哪个版本的数据
数据库的隔离级别读已提交,可重复读都是基于MVCC实现的,相对于加锁的方式,MVCC能够更好的处理读写冲突,有效的提高数据库的并发性能
5.1 事物版本号
对于InnoDB存储引擎,每一行记录都有俩个隐藏列trx_id,roll_pointer,如果表中没有主键和非NULL唯一建的时候,则还会有第三个隐藏的主键列row_id
5.2 undo log
回滚日志,用于记录数据被修改前的信息,在表记录修改之前,会先把数据拷贝到undo log中,如果事务回滚,即可以通过undo log来还原数据
undo log保证事务的原子性和一致性
用于MVCC快照读
5.3 版本链
多个事务并行操作某一行数据,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer)连成一个链表,这个链表就称为版本链
5.4 快照读、当前读
快照读:读取的是记录数据的可见版本,不加锁,普通的select语句都是快照读
当前读:读取记录数据的最新版本,显示加锁的都是当前读
select * from table where id=2 for update
select * from table where id>2 lock in share node;
5.5 Read View
事务执行SQL语句的时候,会生成读视图,在innodb中,每个sql执行前都会得到一个Read View
它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据,常见的属性:
m_ids:当前系统中活跃(未提交)的读写事务ID,它数据结构是一个List
min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值
max_limit_id:表示生成Read View ,分配给下一个事务的id值
create_trx_id:创建当前Read View的事务ID
Read View匹配条件的规则:
- 如果数据事务ID trx_id<min_limit_id,表明生成该版本的事务再生成read view前,已经提交了事务,所以该版本可以事务可见
- 如果trx_id>=max_limit_id 表明生成该版本的事务再生成ReadView之后才生成,所以该版本不可以被当前事务访问
- 如果min_limit_id =<trx_id < max_limit_id,分成3种情况
1)如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_ud等于create_trx_id的话,表明数据是自己生成的,因此是可见的
2)如果m_ids包含trx_id,并且trx_id不等于create_trx_id,则Read view生成时,事务未提交,并且不是自己生产的,所以是不可见的
3)如果m_ids不包含trx_id,则说明这个事务再Readview生成之前就已经提交了,修改的结果,当前事务是可见的
查询数据的基本流程:
1、获取事务自己的版本号,即事务ID
2、获取Read View
3、查询得到的数据,然后ReadView中的事务版本号进行比较
4、如果不符合可见性规则,就需要undo log中的历史快照
5、返回符合规则的数据
6、 读已提交(RC)隔离级别
- 创建user表,插入一条初始化数据
- 隔离级别设置成RC(读已提交),事物A和事物B同时对user表进行查询和修改操作,操作顺序如下
最后查询得到的结果是name=曹操的记录,基于MVCC我们可以很明确的知道工作的原理
1、事物A开启事物,得到一个id:100
2、事物B开启事务,得到事务ID:101
3、事务A生成一个Read View,信息如下
变量 | 值 |
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
create_trx_id | 100 |
根据读视图的可见性规则
min_limit_id(100)=<trx_id(100)<max_limt_id(102)
create_trx_id = trx_id = 100
由此可得,trx_id=100这条记录,当前事务是可见的,所以查到name的值=孙权
- 事务B进行修改操作,把name改成曹操,把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本再undo log的地址
- 提交事务
- 事务A进行查询,再生成一个读视图
变量 | 值 |
m_ids | 100 |
max_limit_id | 102 |
min_limit_id | 100 |
create_trx_id | 100 |
然后再次回到版本立链,从版本链中挑选可见的记录
从图中可以知道,最新版本的列name=操作,该版本的值trx_id=101,通过可见性规则
min_limit_id(100)=<trx_id(101)<max_limit_id(102)
但是trx_id=101,不属于m_Ids的集合 ,因此trx_id=101这个记录,对于当前事务是可见的,所以查询到的name值:操作
综上所述,在RC(读已提交)的隔离级别下,同一个事物里,相同的查询读取同一条记录,返回了不同的数据。因此RC隔离级别,存在不可重复读的并发问题
7、可重复读(RR)隔离级别
RR的隔离级别下,将能解决不可重复读的并发问题
在RC隔离级别下,同一个事物里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务前后读取数据可能不一致的问题
而在RR隔离级别下,一个事务只会获取一次read view视图,从而保证每次查询的数据都是一样的
7.1 实例分析
事务A再次查询,还是用最开始的Read View副本
从图中最新的版本name的内容是操作,该版本的trx_id:101,进行可见性规则刻制
min_limit_id(100)=<trx_id(101) <max_limit_id(102)
因为m_ids(100,101)包含trx_id(101)
并且creator_trx_id(100) 不等于trx_id(101)
所以trx_id(101)这个记录,对于当前事务是不可见的,这时候根据版本连roll_pointer跳到下一个版本,trx_id=100这个记录,再次校验可见性
min_limit_id(100)=<trx_id(100)<max_limit_id(102)
因为m_ids(100,101)包含trx_id(100)
并且create_trx_id(100)等于trx_id(100)
所以这个记录,对于当前事务是可见的,所以俩次查询的结果是一致的。即在可重复度读的隔离级别下,解决了不可重复读的问题
RR和RC本质的区别是。前者是共用一套Read View副本,后者每次查询的时候都会生成一个新的Read View。从而导致最终的结果不一致