MySQL利用MVCC(MVCC又是依据undo log实现的),在一个可重复读的事务执行过程中,读取到的数据都是事务开始时获取的快照,实现了事务见的隔离。
而在锁部分,MySQL更新一条数据会获取当前行的写锁,防止其他事务对当前行的并发更新。
问题:如果获取到锁后,当前事务看到的结果是更新后的还是更新前的???
结合下面案例,详细了解之
背景:REPEATABLE_READ, autocommit=1
表的定义如下
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
事务A | 事务B | 事务 C |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | update t set k=k+1 where id=1; | |
update t set k=k+1 where id=1; select k from t where id=1; |
||
select k from t where id=1; commit; |
||
commit; |
在上面的流程中,事务A读到的是1;事务B读到的是3。根据MVCC,可重复读隔离级别下,事务A中读到的k是1,可以理解,同理,事务B中在更新时读到的也是1,更新后,再次读到应该是2才对呀。基于这个问题,再深入了解MVCC机制到底是怎么运作的。
在MySQL中,有两个“视图”的概念:
一个是view,用于查询时定义的虚拟表,创建视图的语法是
create view;
视图的查询方法与表的查询方法一致。另一个是InnoDB实现MVCC用到的一致性读视图,即consistent read view,用于实现在READ_COMMITED和REPEATABLE_READ隔离级别下,每次读到的都是指定的“快照”(read view)。
InnoDB中,每个事务都有一个唯一的事务ID,叫transaction id,在事务开始的时候向InnoDB的事务系统申请的,严格递增。关于事务启动的时机:
A autocommit=1
没有明确指定事务的开启,每一条SQL都有一个属于自己的完整的事务,即执行SQL的时候就启动了事务
明确指定了事务的开始,有以下两种情况
begin
或者start transaction
后面第一条SQL执行时才会开启事务
start transacton with consistent snapshot
会立即开启事务B autocommit=0
在连接建立后,就开启一个事务,直到执行了commit或者rollback,当前事务结束并自动开启一个新事务。
每行数据除了业务字段,还有三个隐藏的字段
row_id
如果指定了主键,row_id是否就是主键??
DB_TRX_ID
产生当前版本的transaction id
DB_ROLL_PTR
指向undo log的指针,undo log中这行数据应该就是产生这次变更的反向操作
细看undo log,根据官网介绍,undo log分为两部分,insert和update
对于insert型,用于事务回滚,这条undo log在事务提交后就被删除了。
对于update型,用于事务回滚和重建早期版本数据,当没有事务再需要这条log来构建早期版本数据,这条undo log会被删除
undo log 的清理是通过后台线程 定时执行purge动作来清理的
delete 在InnoDB内部当时并没有真正删除,只是打了个标记,后续在purge线程中才会被真正删除,所以delete本质上也是一条update
有了DB_TRX_ID, DF_POLL_PTR,就可以构造read view了,简单讲,事务在构建read view时,找出当前系统中活跃的事务ID,用数组有序存储。
关于read view创建的时机,
在REPEATABLE_READ级别下,事务开启第一次读创建,这样才能保证在整个事务周期内,每次看到的数据都和第一次读到的是一致的;
在READ_COMMITED级别下,每个statement都会重新创建,这样才能实现如果有其他事务更新提交,当前事务能看到
在READ_UNCOMMITED级别下,就不再创建了,每次都是读取最新版本的数据。(这样说的话,最新版本的数据并不是要提交后才生成的呢?关于这点,结合几种log buffer 详细了解一个写操作落地的完整流程)
有了上面read view的数据模型后,当一个事务的read view构造好后续真正读取数据的时候,是怎么判断数据的某个版本对自己是否可见呢????
再具体了解下read view,数组中最小的trx_id,称为低水位,记为Tidmin。最大的的trx_id, 称为高水位,记为Tidmax。数据的trx_id记为RowTid,当前事务的ID记为Tid
RowTid=Tid,说明这次更新是自己产生的,可见
RowTid
RowTid>Tidmax,说明这次更新是由创建read view后的事务产生的,不可见
-
RowTid 大于Tidmin且小于Tidmax:分以下两种情况讨论
4.1 在数组中,说明事务还未提交,不可见
4.2 如果不在数组中了,其实跟场景3类似,说明那个事务执行地很快,在创建read view时就已经提交了,所以对当前事务可见。
http://mysql.taobao.org/monthly/2015/12/01/
当数据的当前版本不可见时,根据DB_ROLL_PTR会找对应的undo log,通过计算反向SQL得到前一版的值。
拿到那条undo log的trx_id再判断,直至对当前事务可见或者没有前一版了。
4.2 搞了半天都没理解到位 其实现在也不算是很清晰 数据库太复杂了。。。。
其实理解上面的关键在于,read view的创建到后续的使用过程中,时间跨度可能会"很长",在这个期间就可能会发生一个完整的事务周期(开始到提交),这便是场景3
对于场景4.2,在read view创建的时候,可能有一个”很久“之前就开启的事务,但是它后面已经有提交过得事务,所以才会出现有比最小事务ID大,但却不在read view中的事务。
在更新SQL执行中,也会先读后写,这里的读是MySQL内部执行的逻辑,跟select读是两回事,写之前的读叫“当前读”,读的是最新值。如果不读最新的值,那就相当于忽略了其他事务的更新,就有可能会脏读。
不管什么配置前提下,每条SQL都是在某一个事务中执行的,对于更新类SQL(insert update delete等)是要获取锁的,如果有有其他事务正持有这个锁,当前事务被阻塞。对于读类的SQL(select),默认是不加锁的,根据一般的隔离级别(REPEATABLE_READ),可能是导致读取的不是“最新的数据”,select也是支持加锁的,select for update
获取的是X锁,select xxx lock in share mode
获取的是S锁。
再看“当前读”,我的理解为了读取到最新的数据,应该是要加锁。
只读事务
怎么指定只读事务,本质是什么,只读事务和不开启事务有何区别?
set session transaction read only;//只读事务
set session transaction read write;//读写事务
在只读事务中,如果执行更新类SQL,会返回'Cannot execute statement in a READ ONLY transaction.'这是理所当然的,不然还叫什么只读事务。只读事务的存在意义是什么,仅仅只是为了防止在事务中有更新操作????
如果指定事务是只读类型的,InnoDB内部会有些优化,比如不申请transaction id。
锁和事务的产生背景
MySQL采用的是One Thread per Connection(新版MySQL已支持线程池),这就产生了资源并发访问的问题,直接的解决方案就是加锁,将同一个资源的访问的访问串行化。根据实际使用经验,很多应用都是读多写少的场景,所以读和读互斥显得不是很必要。所以就将锁细化,分成读写锁,这样就避免了读和读之间的互斥,很大地提高了系统的并发能力。当然,读写、写写还是要互斥的。即便是支持并发读读了,那能不能进一步支持读写并发,即不管其他线程怎么修改,不影响当前线程的读。这就有了MVCC。
SERIALIABLE 隔离级别下的select 和 select lock in share mode 有何异同
按照SERIALIABLE串行化的隔离级别,应该是要加排它锁的才能保证串行,所以在满足功能性的前提下,优先使用共享锁
这篇笔记写了两三天 感觉头都大了。。。 复杂