MySQL学习(二):事务及四种隔离级别

事务

基本要素

  • 原子性

    • 事务开始后的所有操作,要么全部做完,要么都不做
  • 一致性

    • 事务开始前和开始后,数据的完整性约束没有发生破坏
  • 隔离性

    • 同一时间只允许同一事务请求同一数据,不同事务之间彼此没有任何干扰
  • 持久性

    • 事务完成后,事务对数据库的所有更新将会被保存到数据库

事务的并发问题

脏读

  • 概念

    • 事务A读取事务B未提交的数据,结果事务B回滚,那么事务A则读到的为脏数据
  • 解决办法

    • 事务A只读取其他事务已提交的数据,每个 select 语句都有自己的一份快照,而不是一个事务一份,所以在不同的时刻,查询出来的数据可能是不一致的,所以说RC级别下的快照是每个select都一份,而RR情景下是事务开始就会读取快照,持续在整个事务期间内,
  • 对应隔离级别

    • 不可重复读(read-committed)

不可重复读

  • 概念

    • 事务A对数据进行多次读取操作,事务B对事务A读取的内容进行了更新操作并提交,此时事务A在这个期间读到的数据可能出现内容不一致的情况
  • 解决办法

    • 可重复读隔离级别下,事务A发起时,事务在启动的时候就”拍了个快照“。注意,这个快照是基于整个库的,后续的读操作都是快照读,都是读己之所得,所以不会读到其他事务所修改的内容,也就无法产生不可重复读
  • 对应隔离级别

    • 可重复读(repeatable-read)

幻读

  • 概念

    • 事务A对某个范围的区间数据进行修改时,事务B新增或删除了这个范围内的数据并提交,从而导致事务A针对这个范围内的数据条数进行统计时发现多了一条未修改的数据
  • 解决办法

    • repeatable read隔离级别下,只有快照读会产生幻读现象,当前读已经通过gap锁的引入消除了幻读现象
  • 对应隔离级别

    • (1)串行化(serializable)
    • (2)repeatable read隔离级别下,一个事务中只使用当前读,或者只使用快照读都能避免幻读

不可重复读和幻读的比较

  • 总结:不可重复读针对的是重复读中数据内容的异常场景,幻读是针对的数据条数的异常场景,所以说不可重复读是针对修改的场景,幻读是针对新增和删除的场景造成的数据条数的改变

当前读和快照读的比较

  • 快照读

    • 实际场景

      - 解决了不可重复读
      
    • 概念

      • 单纯的select语句,读取专门的快照,RC下快照(ReadView)会在每条语句下生成,RR下会在事务启动时生成,此时是不会加锁的(普通的select不会加锁),其实快照读就是只读取当前事务版本之前的数据,之后的数据不予存入快照。总之,MVCC维持一个数据的多个版本,使得读写操作没有冲突” 这么一个概念。仅仅是一个理想概念,而快照读就是MySQL为我们实现MVCC理想模型的其中一个具体非阻塞读功能。而相对而言,当前读就是悲观锁的具体功能实现

        MVCC实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

    • 好处

      • 并发读写时能够做到读操作不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库的读写能力
      • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
    • MySQL锁的组合

      • MVCC+悲观锁(NextKey-LOCK):MVCC解决脏读、不可重复读、幻读,悲观锁解决写写冲突(丢失更新)
      • MVCC + 乐观锁:MVCC解决脏读、不可重复读、幻读,乐观锁解决写写冲突(丢失更新)
    • MVCC实现方式

      - 通过Read View (三个隐式字段)+undo log实现
      

      image.png

      MVCC具体是如何实现的呢?

      实际上,InnoDB会在每行记录后面增加三个隐藏字段

      (1)DB_ROW_ID:行ID,随着插入新行而单调递增,如果有主键,则不会包含该列。

      (2)DB_TRX_ID:记录插入或更新该行的事务的事务id,注意是该条记录链表的链头节点的事务id,也就是最新的版本的事务id

      img

      (3)DB_ROLL_PTR:回滚指针。指向undo.log记录,每次对某条记录进行修改时,改列会保存一个指针,可以通过这个指针找到记录修改前的信息。当某条记录被修改多次时,该行记录会存在多个版本,并通过回滚指针形成版本链

      以RR为例,每开启一个事务,会分配一个事务id,在该事务执行第一个Select语句时,会生成一个当前时间点的ReadView(快照),主要包含如下属性:

      (1)trx_ids:生成快照时,系统当前存在的活跃事务id,就是未提交的事务

      (2)up_limit_id:trx_ids的最小值,trx_id低于该值的都能被看到

      (3)low_limit_id:当前事务id的最大值,生成ReadView时候要系统分配给下一个事务的id值,例如当前事务id的最大值为5,那么low_limit_id的值就为5+1=6

      (4)creator_trx_id:生成该事务的事务id

      有了这个ReadView(快照),下次访问的时候就能够按照如下步骤判断该版本的数据是否可见

      (1)如果访问版本trx_id与当前事务id相同,则代表当前事务访问他修过的记录,允许看到

      (2)如果trx_id小于up_limit_id的值。表明生成该数据的事务在当前事务之前就已提交,允许看到

      (3)如果trx_id大于low_limit_id的值。表明生成该数据的事务在当前事务之后才开启,所以该版本下的数据不允许访问

      (4)如果trx_id的值再up_limit_id和low_limit_id之间,再看trx_id是否在trx_ids中,不存在则允许访问,存在则不允许

      判断时候,会从版本链的后往前判断,直到满足条件的trx_id并进行展示,如下图代码所示

      img

      对于删除,其实是一种特殊的更新,InnoDB用一个特殊的值delete_bit标识是否删除,判断时查看是否被标记,如果是则跳过该版本。再判断下个版本

  • 当前读

    • 实际场景

    • 解决了幻读(SQL 标准中规定的 RR 并不能消除幻读,但是 MySQL 的 RR 可以,靠的就是 Gap 锁,在 RR 级别下,Gap 锁是默认开启的,而在 RC 级别下,Gap 锁是关闭的)

    • 概念

      • 读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁,当然,获得什么锁取决于当前事务的隔离级别、语句的执行计划、查询条件等因素
      select * from .... where  ... lock in share mode
      update .... set .. where ...
      delete from. . where ..
      insert..
      
    • 实现方式

      • 通过 next-key 锁(行记录锁+间隙锁)

        这里补充下行锁的 3 种算法:
        行锁(Record Lock):锁直接加在索引记录上面。
        间隙锁(Gap Lock):是 Innodb 为了解决幻读问题时引入的锁机制,所以只有在 Read Repeatable 、Serializable 隔离级别才有。
        Next-Key Lock :Record Lock + Gap Lock,锁定一个范围并且锁定记录本身 。

  • 结论

    • 纯当前读和纯快照读都能够解决RR下的幻读,但是快照读下使用了当前读则无法解决幻读,因为当前读会更新快照数据,导致幻读的异常

    例如:

    image.png

幻读/不可重复读的危害

不可重复读

假设存在一个表A,其中包含用户信息和存款字段,现在有个业务场景需要针对表A中存款大于<1000的用户发个纸巾,针对存款在1000-2000的用户发个礼品茶杯,此时这个场景就可能出现下图这给一个用户重复发奖的场景

时刻事物A事物B
T1查询表A中存款>500的用户送纸,查出用户小明
T2 小明存了1000块
T3查询表A存款>1000的用户送茶杯,也查出小明

可以看出,我们给小明即送出了纸,也送出了茶杯,可以看出在不可重复读的场景,验证违背了数据的一致性(完整性约束),我们可以看出我们产生异常的主要原因是同一事物中,同一记录返回了不同的结果

搜索不可重复读,基本都是类似下面这个描述

不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。

这个描述有错吗?没错。错在后半句这个描述只是一个子集。正确的描述应该为
不可重复读,是指在数据库访问中,一个事务范围内两个查询的相同记录却返回了不同数据。
原文链接:https://blog.csdn.net/qq_24054301/article/details/123086098

幻读

假设存在一个业务场景,还是结合上述场景,存在一个表A,其中包含用户信息和存款字段,其中多了一个积分字段,要求送完茶杯之后扣除积分,送纸无需扣除

此时流程可能如下

时刻事物A事物B
T1查询表A中存款>500的用户送纸,查出用户小明
T2 小明存了1000块
T3查询表A存款>1000的用户,未查出用户
T4扣减update 存款>1000的用户,此时扣了小明的积分

问题很明显:只给小明发了纸,但是却扣减了小明的积分

幻读的业务问题就很明显了,当你select出了n条数据去进行业务处理,此时针对n条用户处理完成后执行更新操作,此时可能会造成更新大于或者小于n条数据,造成不符合业务预期

实际开发中,如何避免幻读?

如何避免幻读:

  1. 事物只执行快照读,不进行当前读(update/insert/delete/上锁)操作
  2. 事物如果要执行当前读操作,先进行当前读(update/insert/delete/上锁),因为这个场景可以产生间隙锁保护数据

四种隔离级别

image.png

思维导图

事务及四种隔离级别.png

参考链接:

https://zhuanlan.zhihu.com/p/106883029?from_voters_page=true

https://blog.csdn.net/w892824196/article/details/109701966

https://blog.csdn.net/qq_44766883/article/details/105879308

https://www.jianshu.com/p/8845ddca3b23

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×