什么是事务:
在关系型数据库中,事务可以是一条sql语句,或一组sql语句,也可以是一整个应用程序.是用户定义的一个操作序列;
即:作为一个逻辑单元需要执行的操作,要么全成功,要么都失败;比如扣库存和创建订单;
事务有4个基本特征:简称为ACID特性.
原子性(Atomicity):一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。
一致性(Consistency):事务必须是从一个一致性状态变为另一个一致性状态.一致性和原子性是密切相关的.
隔离性(Isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(durability)。持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
为什么会有事务:
事务的提出就是为了解决并发情况下保持数据一致性的问题
事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。
事务的四大特性是如何实现的
事务的原子性是通过undo log来实现的
undo log的生成
假设有两个表 bank和finance,表中原始数据如图所示,当进行插入,删除以及更新操作时生成的undo log如下面图所示:
从上图可以了解到数据的变更都伴随着回滚日志的产生:
(1) 产生了被修改前数据(zhangsan,1000) 的回滚日志;
(2) 产生了被修改前数据(zhangsan,0) 的回滚日志。
根据上面流程可以得出如下结论:
1.每条数据变更(insert/update/delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上;
2.所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。
根据undo log 进行回滚
为了做到同时成功或者失败,当系统发生错误或者执行rollback操作时需要根据undo log 进行回滚。
回滚操作就是要还原到原来的状态,undo log记录了数据被修改前的信息以及新增和被删除的数据信息,根据undo log生成回滚语句,比如:
(1) 如果在回滚日志里有新增数据记录,则生成删除该条的语句;
(2) 如果在回滚日志里有删除数据记录,则生成生成该条的语句;
(3) 如果在回滚日志里有修改数据记录,则生成修改到原先数据的语句。
事务的持久性是通过redo log 来实现的
事务一旦提交,其所作的修改就会永久保存到数据库中,此时即使系统崩溃修改数据也不会丢失.
mysql的数据存储机制,mysql的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的
为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;
上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!!!
因为我们的数据已经提交了,但此时是在缓冲池里头,还没来得及在磁盘持久化,所以我们急需一种机制需要存一下已提交事务的数据,为恢复数据使用。
redo log产生:
既然redo log也需要存储,也涉及磁盘IO为啥还用它
1.redo log的存储是顺序存储,而缓存同步是随机操作
2.缓存同步是以数据页为单位的,每次传输的数据大小大于redo log
事务的隔离性是通过(读写锁+MVCC)来实现的
后面主要讲这个,所以留到后面讲
而事务的一致性是通过原子性,持久性,隔离性来实现的
写与写隔离基于锁实现,读与写隔离基于MVCC实现
什么是MVCC
MVCC即多版本并发控制。MVCC是一种并发控制的方法,一般在数据管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
数据库隔离级别读已提交,可重复读都是基于MVCC实现
MVCC实现的关键点
事务的版本号:
事务每次开启前,都会从数据库中获得一个自增长的事务ID,可以从事务ID判断事务的执行先后顺序
隐式字段:
对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id ,roll_pointer如果表中没有主键和非Null唯一键时,则还会有第三个隐藏的主键列row_id
undo log
undo log ,回滚日志,用于记录数据被修改前的信息。在表记录修改前,会把数据拷贝到undo log里面,如果事务回滚,即可以通过undo log 来还原数据
undo log的用途
1.事务回滚,保证原子性和一致性
2.用于MVCC快照读
版本链
多个事务并行操作某一行数据时,不同事物对该行数据的修改会产生多个版本,然后通过回滚指针,连成一个链表,这个链表就称为版本链
其实,通过版本链,我们就可以看出事务版本号、表格隐藏的列和undo log它们之间的关系。我们再来小分析一下。
- 假设现在有一张core_user表,表里面有一条数据,id为1,名字为孙权:
- 现在开启一个事务A: 对core_user表执行update core_user set name ="曹操" where id=1,会进行如下流程操作
- 首先获得一个事务ID=100
- 把core_user表修改前的数据,拷贝到undo log
- 修改core_user表中,id=1的数据,名字改为曹操
- 把修改后的数据事务Id=101改成当前事务版本号,并把roll_pointer指向undo log数据地址。
快照读和当前读
快照读:读取的是记录数据的可见版本。不加锁,普通的select语句都是快照读
当前读:读取的是记录数据的最新版本,显示加锁都是当前读
Read View
Read View是什么
它是事物执行sql语句时,产生的读视图。实际上在innoDB中,每个sql语句执行前都会得到一个读视图
Read View有什么用
它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据
Read view中的重要属性
m_ids:当前系统中那些活跃(没有提交)的读写事务ID,数据结构为List
min_limit_id:表示在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值。
max_limit_id:表示生成ReadView时,系统中应该分配给下一个事务的id值
creator_trx_id:创建当前read view的事务ID
ReadView匹配条件规则
1.如果数据事务ID trx_id
2.如果trx_id >=max_limit_id ,表明生成该版本的事务在生成 ReadView后才生成,所以该版本不可以被当前事务访问。
3.如果min_limit_id=
- (1).如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_id等于creator_trx_id的话,表明数据是自己生成的,因此是可见的。
- (2)如果m_ids包含trx_id,并且trx_id不等于creator_trx_id,则Read View生成时,事务未提交,并且不是自己生产的,所以当前事务也是看不见的;
- (3).如果m_ids不包含trx_id,则说明你这个事务在Read View生成之前就已经提交了,修改的结果,当前事务是能看见的。
MVCC实现原理分析
查询一条记录,基于MVCC,是怎么样的流程
1.获取事务自己的版本号,即事务ID
2.获取ReadView
3.查询得到的数据,然后ReadView中的事务版本号进行比较
4.如果不符合ReadView的可见性规则,即需要UndoLog中的历史快照
5.最后返回符合规则的数据
InooDB实现MVCC,是通过Read View +Undo log实现的,Undo log保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。
搞清楚几个关键点:
视图的生成的时间:每个sql语句执行前都会生成一个视图
每个事务所匹配的视图:是本事务生成的视图
trx_id : 为版本链中的事务id,是已提交的事务id
creator_trx_id:为创建ReadView的事务id,就是ReadView属于哪个事务
m_ids:为生成该ReadView时,还没有提交的事务集合,存储的是事务的id
max_limit_id:为所有未提交的事务的id最大值+1,即要分配给下一个还未执行的事务的id
min_limit_id:为所有未提交的事务的id最小值
读已提交,在事务中的每条sql执行前都会生成新的ReadView,会导致不可重复读的现象
读已提交(RC)隔离级别,存在不可重复读问题的分析历程
- 创建core_user表,插入一条初始化数据,如下:
- 隔离级别设置为读已提交(RC),事务A和事务B同时对core_user表进行查询和修改操作。
ini复制代码事务A: select * fom core_user where id=1 事务B: update core_user set name =”曹操”
执行流程如下:
最后事务A查询到的结果是,name=曹操的记录,我们基于MVCC,来分析一下执行流程:
(1). A开启事务,首先得到一个事务ID为100
(2).B开启事务,得到事务ID为101
(3).事务A生成一个Read View,read view对应的值如下
变量 | 值 |
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然后回到版本链:开始从版本链中挑选可见的记录:
由图可以看出,最新版本的列name的内容是孙权,该版本的trx_id值为100。开始执行read view可见性规则校验:
ini复制代码min_limit_id(100)=<trx_id(100)<102; creator_trx_id = trx_id =100;
由此可得,trx_id=100的这个记录,当前事务是可见的。所以查到是name为孙权的记录。
(4). 事务B进行修改操作,把名字改为曹操。把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本在undo log的地址。
(5) 提交事务
(6) 事务A再次执行查询操作,新生成一个Read View,Read View对应的值如下
变量 | 值 |
m_ids | 100 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然后再次回到版本链:从版本链中挑选可见的记录:
从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行Read View可见性规则校验:
ini复制代码min_limit_id(100)=<trx_id(101)<max_limit_id(102); 但是,trx_id=101,不属于m_ids集合
因此,trx_id=101这个记录,对于当前事务是可见的。所以SQL查询到的是name为曹操的记录。
综上所述,在读已提交(RC)隔离级别下,同一个事务里,两个相同的查询,读取同一条记录(id=1),却返回了不同的数据(第一次查出来是孙权,第二次查出来是曹操那条记录),因此RC隔离级别,存在不可重复读并发问题。
可复读(RR)隔离级别,解决不可重复读问题
如何解决不可重复读问题嘞,就是通过不同的ReadView生成方式
在RR中一个事务只能生成一个ReadView,就是在事务生成的时候创建
这样就可以避免出现不可重复读的问题了
在RR隔离级别下,是如何解决不可重复读问题的呢?我们一起再来看下,
还是4.2小节那个流程,还是这个事务A和事务B,如下:
4.3.1 不同隔离级别下,Read view的工作方式不同
实际上,各种事务隔离级别下的Read view工作方式,是不一样的,RR可以解决不可重复读问题,就是跟Read view工作方式有关。
- 在读已提交(RC)隔离级别下,同一个事务里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务里前后读取数据可能不一致的问题(不可重复读并发问题)。
begin | |
select * from core_user where id =1 | 生成一个Read View |
/ | / |
/ | / |
select * from core_user where id =1 | 生成一个Read View |
- 在可重复读(RR)隔离级别下,一个事务里只会获取一次read view,都是副本共用的,从而保证每次查询的数据都是一样的。
begin | |
select * from core_user where id =1 | 生成一个Read View |
/ | |
/ | |
select * from core_user where id =1 | 共用一个Read View副本 |
4.3.2 实例分析
我们穿越下,回到刚4.2的例子,然后执行第2个查询的时候:
事务A再次执行查询操作,复用老的Read View副本,Read View对应的值如下
变量 | 值 |
m_ids | 100,101 |
max_limit_id | 102 |
min_limit_id | 100 |
creator_trx_id | 100 |
然后再次回到版本链:从版本链中挑选可见的记录:
从图可得,最新版本的列name的内容是曹操,该版本的trx_id值为101。开始执行read view可见性规则校验:
scss复制代码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这个记录,再次校验是否可见:
scss复制代码min_limit_id(100)=<trx_id(100)< max_limit_id(102); 因为m_ids{100,101}包含trx_id(100), 并且creator_trx_id (100) 等于trx_id(100)
所以,trx_id=100这个记录,对于当前事务是可见的。即在可重复读(RR)隔离级别下,复用老的Read View副本,解决了不可重复读的问题
参看文章: