个人理解先有问题!软件设计初期不会考虑那么完善,随着问题的出现才分出解决不通类型的隔离级别。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
未提交读READ UNCOMMITTED | × | × | × |
已提交读READ COMMITTED | √ | × | × |
可重复读REPEATABLE READS | √ | √ | × |
串行读SERIALIZABLE | √ | √ | √ |
在一个事务读取到另一个事务未提交的数据称为脏读.
在一个事务中,两次查询结果不一致.期间其它事务对数据进行insert/update/delete操作.例如在A事务.
又叫“幻象读”,可以理解为可重复读级别出现两次查询结果不一致,是“不可重复读”的一种特殊场景,具体复现下文会详细说明.
可重复读级别解决的问题,主要是不可重复读问题,验证分析如下
注意:插入数据主要看label_name字段,更新字段看label_desc;
事务A中两次查询,事务B在事务A的两次查询中间update一条数据并commit,详细过程和结果如图:
事务A中两次查询,事务B在事务A的两次查询中间delete一条数据并commit,详细过程和结果如图:
可以看到事务A中两次查询结果都是一致的,并没有拿到事务B提交的数据,说明完美解决不可重复读问题,你是否想问这是如何实现的呢?
悲观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。
但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。
何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
MySQL、ORACLE、PostgreSQL等都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免不可重复读和幻读,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。
在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。在可重读Repeatable reads事务隔离级别下:
通过MVCC,虽然每行记录都要额外的存储空间来记录version,需要更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多读操作都不用加锁,读取数据操作简单,性能好。
事务A中先查询并更新,然后在事务B中insert/update/delete一条数据,发现事务B一直执行中,直到事务A提交后B才执行成功,详细过程和结果如图:
事务A更新数据,事务B无法提交,这又是为啥呢?
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。
假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。读取的是最新的数据,需要加锁。
而在可重复度的隔离级别下,情况就完全不同了。
事务1在update后,对该数据加锁,事务B无法插入新的数据,这样事务A在update前后数据保持一致,避免了幻读,可以明确的是,update锁的肯定不只是已查询到的几条数据,因为这样无法阻止insert,有同学会说,那就是锁住了整张表呗。
还是那句话,Mysql已经是个成熟的数据库了,怎么会采用如此低效的方法呢?其实这里的锁,是通过next-key锁实现的。
在Users这张表里面,label_type是个非聚簇索引,数据库会通过B+树维护一个非聚簇索引与主键的关系,简单来说,我们先通过label_type找到这个索引所对应所有节点,这些节点存储着对应数据的主键信息,即id=1,我们再通过主键id=1找到我们要的数据,这个过程称为回表。
前往学习: https://www.cnblogs.com/sujing/p/11110292.html
此处引用上面那边文章中作者画的B+树来解释Next-key。
假设我们上面用到的User表需要对Name建立非聚簇索引,是怎么实现的呢?我们看下图:
B+树的特点是所有数据都存储在叶子节点上,以非聚簇索引的秦寿生为例,在秦寿生的右叶子节点存储着所有秦寿生对应的Id,即图中的34。
在我们对这条数据做了当前读后,就会对这条数据加行锁,对于行锁很好理解,能够防止其他事务对其进行update或delete,但为什么要加GAP锁呢?
还是那句话,B+树的所有数据存储在叶子节点上,当有一个新的叫秦寿生的数据进来,一定是排在在这条id=34的数据前面或者后面的,我们如果对前后这个范围进行加锁了,那当然新的秦寿生就插不进来了。
那如果有一个新的范统要插进行呢?因为范统的前后并没有被锁住,是能成功插入的,这样就极大地提高了数据库的并发能力。
事务A中先查询,然后事务B在事务A的insert一条数据并commit,然后事务A更新数据并查询,详细过程和结果如图:
两次查询结果不一致,事务A读到了事务B提交的数据——幻读
事务A中先查询,然后事务B在事务A的update一条数据并commit,然后事务A更新数据并查询,详细过程和结果如图:
两次查询结果一致,解决不可重重复读和幻读问题.
事务A中先查询,然后事务B在事务A的delete一条数据并commit,然后事务A更新数据并查询,详细过程和结果如图:
两次查询结果一致,但事务A中修改的数据未生效.此处有不同解读:
一种认为两次查询结果一致,解决了不可重复读的问题.这种观点对幻读的解释偏向于insert操作,同时认为不可重复读是update和delete.
另一种认为当前数据的修改应该生效,不能依赖事务B删除的数据,个人认为这也是幻读.
参考文章:
MySQL 5.6 Reference Manual
understanding InnoDB transaction isolation levels
MySQL · 源码分析 · InnoDB Repeatable Read隔离级别之大不同
不懂数据库索引的底层原理?那是因为你心里没点b树
Innodb中的事务隔离级别和锁的关系
MySQL InnoDB中的行锁 Next-Key Lock消除幻读