MVCC 一句话理解:为了实现快照读。

1. 事务是如何实现的MVCC

(1)每个事务都有一个事务ID,叫做transaction id(严格递增)。
(2)事务在启动时,找到已提交的最大事务ID记为up_limit_id。
(3)事务在更新一条语句时,比如id=1改为了id=2.会把id=1和该行之前的row trx_id写到undo log里。
并且在数据页上把id的值改为2,并且把修改这条语句的transaction id记在该行行头。
(4)再定一个规矩,一个事务要查看一条数据时,必须先用该事务的up_limit_id与该行的transaction id做比对.
如果up_limit_id>=transaction id,那么可以看.如果up_limit_id<transaction id,则只能去undo log里去取。

MVCC写:

做两件事:将老记录写入到undolog里,更新该记录的事务id。

MVCC读:

将当前事务的id与已提交的事务id比较,当当前事务id(可以任务每次读都会生成一个事务id)大于提交事务id时,就去读取undo log,不同的隔离级别会有不同的读视图生成方式,

2. InnoDB下的当前读和快照读:

当前读:

select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时

还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

 

快照读:
不加锁的select操作就是快照读,快照都不阻塞其他事务;快照读的实现是基于多版本并发控制,即MVCC,MVCC避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

api事务 mysql mysql事务id_数据

1.隐藏字段:

  • DB_TRX_ID 最近修改事务ID
  • DB_ROLL_PTR 回滚指针,指向这条记录的上一个版本
  • DB_ROW_ID 隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID。

2.undo日志

原子性 底层就是通过 undo log 实现的。undo log主要记录了数据的逻辑变化,比如一条 INSERT 语句,对应一条DELETEundo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATEundo log ,这样在发生错误时,就能回滚到事务之前的数据状态。

同时, undo log 也是 MVCC(多版本并发控制)实现的关键。

每条数据修改(insert、update或delete)操作都伴随一条undo log的生成,并且回滚日志必须先于数据持久化到磁盘上

api事务 mysql mysql事务id_加锁_02

3.读视图

当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,别的事务可以修改这条记录,每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。

 

读已提交读和可重复读的区别就在于它们生成ReadView的策略不同。

 

ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。

如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。

如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。

如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。

 

举个例子 ,在已提交读隔离级别下:

比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是

api事务 mysql mysql事务id_api事务 mysql_03

那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。

这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。

那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务

api事务 mysql mysql事务id_加锁_04

这时候版本链就是

api事务 mysql mysql事务id_加锁_05

这时候之前那个select事务又执行了一次查询,要查询id为1的记录。

这个时候关键的地方来了

如果你是读已提交隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。

按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。

如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读!

也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。

这就是Mysql的MVCC,通过版本链,实现多版本,可并发读-写,写-读。通过ReadView生成策略的不同实现不同的隔离级别。

3. 刷盘:落盘可以从3个方向分析

(1)数据:
bufferpool中的有个change buffer,也就是”插入缓存“,缓存了二级索引的变更信息,通过合并写入二级索引减少离散IO以提高性能。
刷脏页一般发生在commit之后,redo和binlog提交之后。innodb可以根据脏页在bufferpool中的水位强制刷脏页。

在实际应用中, binlog 的主要使用场景有两个,分别是 主从复制数据恢复 。用于记录数据库执行的写入性操作(不包括查询)信息。

  1. 主从复制 :在 Master 端开启 binlog ,然后将 binlog发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
  2. 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。

binlog日志刷盘可以通过sync_binlog 参数控制 biglog 的刷盘时机。当设置为1时每次commit都需要刷盘,但是会影响数据库性能。

(3)redo log用于数据库的持久化,为了提高数据的写入和更新性能,数据不是每次insert和update都写盘,因为随机写会降低数据库性能。

为了保证数据不丢失,脏页落盘前会先顺序写redo_log,redo_log顺序写性能比较高。

 

mysql日志双写最重要的参数有2个:binlog日志可以不刷盘,但是redolog一定要刷盘。

1.innodb_flush_log_at_trx_commit刷log buffer的redo_log到log file中。0——每秒刷盘,1——每次commit都刷盘

2.sync_binlog刷binlog cache的事务到binlog文件中。0——不写binlog。1——每次提交都写binlog

2个参数的默认值都是1,表示提交既写log(redolog和binlog都写),也就是mysql日志双写。

api事务 mysql mysql事务id_数据_06

16. mysql事务

写事务会阻塞当前读,但是不会阻塞快照读。快照读是使用mvcc和undolog来实现的,不加锁,

当前读是读取最新的数据,并且需要获取记录的锁,如select * for update / lock in share mode

两个for update 会抢锁

17.隔离级别

脏读=读未提交,一个事务内部多次读取同一个记录,可以读取到另外一个事务未提交的数据。

不可重复读,一个事务多次读取同一记录,可以读取到另外一个事务已经提交的事务。

幻读:批量读取数据时,读取了另外一个事务已提交的数据。

读未提交  读已提交 可重复读  串行化