影子的知识库

影子的知识库

  • 知识库
  • GitHub

›Java系列

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

参考

  • https://tech.meituan.com/2014/08/20/innodb-lock.html
  • http://hedengcheng.com/?p=771
  • https://www.cnblogs.com/AlmostWasteTime/p/11466520.html
  • https://www.aneasystone.com/archives/2017/10/solving-dead-locks-one.html

事务的四大特性

  • Atomic
    • 原子性,事务的操作看做一个整体,要么全部成功,要么全部失败
  • Consistency
    • 一致性,事务在完成的时候,数据的状态保持一致。简单理解就是比如说 A 和 B 一共有 100 块钱。那么在转账事务里,不管怎么转账,他们的总和都应该是 100
  • Isolation
    • 隔离性,事务可以具有不同的隔离级别,从而具有不同的性质。
  • Durability
    • 持久性,事务一旦成功,产生的变化是持久的,不会因为断电之类的导致数据丢失

隔离级别和问题

隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能
串行不可能不可能不可能

数据库锁和隔离级别的关系

隔离级别是一种实现的效果,而数据库锁则是实现隔离级别的一种手段。

举个不是特别贴切的例子,就比如说实现进程间的方法调用,也就是 RPC 的效果,使用 HTTP 协议就是实现这个效果的一种手段,使用自定义二进制协议或者 protobuf 等协议也是实现 RPC 的一种手段,使用管道或者 FIFO 也是一种手段。

数据库想要达成隔离级别带给我们的语义,不是天生就能实现的,数据库本身的底层实现中也要通过读写锁、MVCC 等方式来实现隔离级别的语义。

也就是说:隔离级别是一种高层次上的视角,当我们使用隔离级别提交事务的时候,底层有可能使用读写锁来达成相应隔离级别的效果,也有可能通过MVCC等方式来实现相应隔离级别的效果

可重复读的两种实现原理

读写锁的实现原理

  • 读取的时候施加读锁,这时其它事务也可以读
  • 写入的时候施加写锁,这时其它事务不可以读也不可以写

这样,导致一旦有事务读取了记录,那么记录就有了读锁,其它事务无法对该记录添加写锁,也就无法修改该记录,从而就保证了事务可以重复读取(理解成可重入的读锁)

事务A事务B记录的状态
读取第一行读取第一行读锁 * 2
更新第一行:阻塞等待读锁释放读锁 * 2,事务B等待
提交事务继续执行无锁,事务 A、B 先后释放读锁
更新继续执行写锁

可以看到,事务 B 是无法更新事务 A 已经读取了的行的,所以事务 A 具有可重复读的特性,同时事务 B 也具有可重复读的特性

MVCC 的实现原理

MVCC 就是多版本并发控制,它的原理就是:事务读取的时候都是读取的自己的快照版本,这样无论其他事务做了任何修改,当前的读取事务读到的都是一样的数据,也就是可重复读

在 MVCC 里,读取分成两种读取,一种是快照读,一种是当前读。

快照读

快照读就是上述所描述的,事务仅仅读取自己事务开始的时候的数据的版本,被其它事务后来更新的新版本对于本事务是不可见的。这种特性最大的好处就是:快照读的时候不会加锁,后续的更新类的事务一样可以更新当前事务正在读取的记录,本事务由于读取的是快照,其它事务的更新不会对自己读取快照产生任何影响。

这种方式读取不加锁,只有写入才加锁,可以很大的提高并发性。

快照读的案例:select * from table where ? ; 也就是简单的 select 读取

可以理解成:一旦第一次读取,立刻生成一个视图,后续事务更新不会影响这个视图

当前读

当前读,读取的是数据的最新版本,包括有:

  • select * from table where ? lock in share mode;
  • select * from table where ? for update;
  • insert into table values (…);
  • update table set ? where ?;
  • delete from table where ?;

简单来说就是:加锁类型的读取、更新语句

针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

当前读返回的记录:查询返回的记录、更新语句操作的记录,会被加入到当前快照中,比如事务 A 第一次查询返回 8 条记录,事务 B 插入了新的 2 条记录。事务 A 使用当前读,读到了 2 条里其中的一条,那么事务 A 再次全部查找的时候,会找到 9 条记录

对于更新类语句来说,所谓当前读返回的记录就是更新语句更新的记录。

总结

现在主流的实现方式是 MVCC 实现可重复读,优点是:

  • 快照读取不加锁,提高了并发的性能
  • 一定程度上解决了幻读

MVCC 还是有可能会发生第二类丢失更新(提交覆盖),但是读写锁实现可重复读是不会发生第二类丢失更新的。

原因:

  • 读写锁实现的可重复读,读取的时候阻塞了写入。如果事务 A 读取,事务 B 写入的话,此时事务 B 被阻塞,直到事务 A 处理完成,保证事务 A 读取和写入的都是最新记录。如果事务 A 和 B 都是先读取后写入的话,要么其中一个事务,假设是 A 一次性抢到读锁和写锁,事务 B 直接阻塞。要么是事务 A 和 B 都抢到读锁,都无法写入。这两种情况都不会发生第二类丢失更新
  • MVCC 实现的可重复读,读取的时候可以被写入新版本。我们更新的时候有可能是拿到旧版本的数据去更新的。此时就可能发生第二类丢失更新。
    • 解决方法一个是给读取加锁:select for update、select lock in share mode
    • 另一个是乐观锁:给记录加个版本号字段,每次更新操作执行前先查询版本号,更新时同时校验版本号,执行更新的时候版本号同步加1

可重复读和幻读

参考:

  • https://www.cnblogs.com/hellopretty/p/5020093.html
  • https://blog.csdn.net/zcl_love_wx/article/details/82382582

定义

  • 不可重复读:多次读取的时候,记录的内容发生了改变
    • 例如读取 name = czp 对应的记录,发现第一次读取是 3,第二次读取是 5
  • 幻读:多次读取的时候,记录的数量发生了改变
    • 例如读取 name = czp 对应的记录,发现第一次读取的是 3,第二次读取的是 [3, 5](插入了一条记录)

RC

  • 不加锁的时候(快照读)是不可重复读的,也就是读取的时候,记录的内容是可以发生变化的
  • 加锁的时候(当前读)
    • 是可重复读的,也就是读取的时候,记录的内容是不会发生变化的
    • 同时是可能发生幻读的,也就是读取的时候,记录的数量是可以发生变化的
      • 原因是加锁的时候,锁住的是当前存在的数据,我们插入的时候是可以进行的,那么就会被下一次读取所读到,于是就发生了幻读

RR

  • 快照读

    • 是可重复读的,因为会在一个一致性的快照版本中读取,新事物提交的记录不会对快照读产生影响
    • 不会发生幻读,理由也是一样的,RR 的快照读在一个一致性的快照版本中读取,新提交的记录我快照读的时候是看不到的
  • 当前读

    • 是可重复读的,因为当前读会对读取的记录加锁,记录的内容无法被修改

    • 不会发生幻读,理由是:

      • 当前读会加上间隙锁,也就是对查询条件加锁,例如

        select * from user where age > 10 for update ; // 当前读才会加锁

      • 那么当另一个事务想要插入 age 大于 10 的记录的时候,将会被阻塞,这就是间隙锁的一种理解(实际上间隙锁是锁住记录本身,以及最靠近记录集合的相邻页)

      • 因此其它事务在当前事务是当前读的时候,无法进行插入当前读读到的范围的内容,也就避免了当前读场景下的幻读

RR 中存在的幻读的情况

从上面的分析中可以发现,RR 中有两种情况下,不会发生幻读:

  • 只使用快照读
  • 只使用当前读

但是,如果我们混合使用两种方式,就会发生一种所谓的幻读:

测试前数据:image-20200414002347388

事务A事务B
beginbegin
select * from dept
insert into dept(name) values("研发部")
commit
update dept set name="财务部"
commit

我们会发现结果是:image-20200414002401793

原因是:我们第一次使用的是快照读,第二次使用了当前读,两次读取就不保证可重复读了,因而也就发生了幻读。

但是如果我们在事务 A 提交之前,再次运行事务 B,将会发现事务 B 阻塞,从而保证了事务 A 后续不会发生幻读

总结

RC:

  • 快照读不可重复读,同时也有幻读
  • 当前读可以保证可重复读,但是无法避免幻读

RR:

  • 快照读可重复读,同时避免幻读
  • 当前读可重复读,同时避免幻读

上面的表中说可重复读可能发生幻读,这个针对的是普通的可重复读实现,针对 mysql mvcc 加上间隙锁的实现来说,是可以避免幻读的。

总体来说,RR 解决了幻读的问题,唯一存在幻读的情况就是先使用快照读再使用当前读,两次读取不一致。

Last updated on 11/8/2020
← springboot自动配置springboot监控 →
  • 参考
  • 事务的四大特性
  • 隔离级别和问题
  • 数据库锁和隔离级别的关系
  • 可重复读的两种实现原理
    • 读写锁的实现原理
    • MVCC 的实现原理
    • 当前读
    • 总结
  • 可重复读和幻读
    • 定义
    • RC
    • RR
    • RR 中存在的幻读的情况
    • 总结
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng