mysql高级进阶-innodb幻读

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

引言

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

什么是幻读

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

  • 读未提交
  • 读提交
  • 可重复读
  • 序列化

脏读

在说幻读之前有个脏读。脏读比较好理解,就是读到了别的事务还没提交的数据。
image.png
既然脏读是读了别的事务没提交的数据,那么就改下,于是就出现了读提交,规定事务里面的读只能读取别的事务已经提交的数据。

不可重复读

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

幻读

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

image.png

不可重复读和幻读的区别

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

序列化

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

innodb如何解决幻读

当前读和快照读

当前读(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 线程来清理

image.png
当我们查询数据的时候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

image.png

  1. 假设name没索引,没索引的话,mysql就会进行全表扫,同时给整张表的所有记录加行锁,然后在由server层过滤,server筛出符合条件的数据,不符合的解锁。整个过程消耗巨大,影响并发。
  2. 假设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) 那么就会出现幻读

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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享