mysql随手记-innodb幻读

Posted 假装懂编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mysql随手记-innodb幻读相关的知识,希望对你有一定的参考价值。

写在前面


mysql的存储引擎本来是有不少的,随着业务和场景的迭代,现在业务基本上都只用innodb,不支持事务的引擎使用的越来越少了。
接下来的我会针对什么是幻读、innodb是如何解决幻读的来讲。


什么是幻读


我们知道innodb的事物隔离级别有四种

  • 读未提交

  • 读提交

  • 可重复读

  • 序列化


脏读


在说幻读之前有个脏读。脏读比较好理解,就是读到了别的事务还没提交的数据。

既然脏读是读了别的事务没提交的数据,那么就改下,于是就出现了读提交,规定事务里面的读只能读取别的事务已经提交的数据。


不可重复读


mysql随手记-innodb幻读

读提交是解决了脏读的问题,但是引来了另一个问题,那就是不可重复读,同一个事务里,读取同一条数据竟然得到不同的结果。


幻读

幻读的一个典型的场景就是,我在事务A查询某个范围内的数据,刚开始得倒1条,然后另一个事务B在这个范围内又插入一条,然后事务A再查同样的范围,发现多了一条

mysql随手记-innodb幻读


在说幻读之前有个不可重复读和幻读的区别脏读比

较好理解,就是读到了别的事务还没提交的数据。

乍一看,不可重复读和幻读差不多。但是从概念上来说其实还是有区别的,不可重复读偏updatedelete,幻读偏insert。不可重读读可以通过锁来实现,锁住这条数据,这样别的事务就更新不了。幻读就不能通过行锁来解决,除非锁表,锁表的话成本太高。


序列化

不管幻读还是不可重复读,通过序列化来处理,完全串行处理,那就不存在以上问题,但效率肯定是最差的。


当前读和快照读


当前读(Locking Reads):锁定读取,像update、delete、insert、select .. for update、select... in share mode。当前读本身是解决不了上述的范围查找的幻读。
快照读(Consistent Nonlocking Reads):顾名思义读取的是快照,因为mvcc多版本的原因,快照读解决了常规的select这种的幻读。


mvcc(Multi-Version Concurrency Control)


多版本并发控制,字面意思就是有多个版本的数据。
为了实现mvcc,mysql innodb是如何做的呢?首先每行数据除了我们肉眼可见的字段以外,还有默认的两列(可能三列,前提是你没设置自增的id,如果没设置自增的id,那么会有一列默认的自增id row_id)。

  • trx_id 记录当前数据insert 或update时事务id。

  • roll_ptr 回滚指针,事务的回滚rollback就是通过roll_ptr来实现的。redo log是持久化记录,undo log是回滚的日志。当在一个事务中,更新了一条记录的时候,它会这样:

  1. 对要更新的数据先加排他锁。

  2. 在更新前先把老数据写到undo log中。

  3. 然后更新新的数据,并且设置新的数据的roll_ptr指向刚刚undo log的老数据。

  4. 写redo log,更新trx_id为当前事务的id,同时也设置下roll_ptr。

  5. 释放排他锁。

这样当要回滚的时候,通过roll_ptr就可以找到之前的数据。对于删除操作,也不是真正的删除,会给数据打上删除的标签,等待purge 线程来清理

mysql随手记-innodb幻读

当我们查询数据的时候mvcc是如何做的

  1. 首先当前事务有个id:m_creator_trx_id。

  2. 然后获取正在执行的事务id集合:m_ids(升序的)。

  3. 有一个最小的事务id:m_ids[0]。

  4. 还有个接下来要分配的事务id:m_ids[len(m_ids)-1]+1。

  5. 如果别访问的数据的版本和m_creator_trx_id一样,说明是当前事务自己修改的,那么可以访问。

  6. 如果被访问的数据版本小于m_ids[0],那么说明这条数据在本事务前就已经提交了,那么可以访问。

  7. 如果被访问的数据的版本大于等于m_ids[len(m_ids)-1]+1,说明这条数据被一个高版本的事务提交了,那么就不能访问。

  8. 如果被访问的数据版本在里面m_ids[0]~m_ids[len(m_ids)-1]+1之间,那么就要判断是不是在m_ids里(由于是有序的,用二分法),如果在说明此数据的事务是活跃的,那么就不能访问,如果不在,则可以获取。

  9. 当访问了不能访问的数据时候,会通过roll_ptr一直向前找,直至找到一个能访问的版本。


当前读的幻读如何解决
假设当前有个表,表里有name字段,执行select * from xx where name=xx


  • 假设name没索引,没索引的话,mysql就会进行全表扫,同时给整张表的所有记录加行锁,然后在由server层过滤,server筛出符合条件的数据,不符合的解锁。整个过程消耗巨大,影响并发。

  • 假设name有索引且不唯一,因为name是string,当有索引时,对应的索引会根据name值自动转成数字,假设数据表里有 (1,5) (2,10) (3,15) (4,20) ,这时innodb的next key lock就发挥作用了,next key lock=gap lock+record lock,会把数据分成 左开右闭的形式 (-∞,5] (5,10] (10,15] (15,20] (20,+∞),假设name=xx对应的索引是15,执行select * from xx where name=xx for update,这时(10,15] (15,20]这个区间会被锁定,注意这个是位置区间。为什么要范围锁,因为name不是唯一索引,可能存在多条记录,那么如果此时在插入一条相同的name (5,15),索引的位置可能是变成这样了 (1,5) (2,10) (3,15) (5,15) (4,20)  那么就会出现幻读。

  •  假设name是唯一索引,那么就会降级成record lock,锁单行。因为唯一索引的本身约束,别的事务是插入不了相同的数据的。









以上是关于mysql随手记-innodb幻读的主要内容,如果未能解决你的问题,请参考以下文章

随手记:MyCAT实现MySQL集群与读写分离

《PHP, MySQL, Javascript和CSS》读书随手记----MySQL篇

mysql随手记-我没有被面试官“锁”住

随手记:mysql集群搭建之主从配置

Mock随手记

《PHP, MySQL, Javascript和CSS》读书随手记----php篇