锁、mvcc、隔离级别、(脏读、不可重复读、幻读)理解

长期以来,自己都或多或少听到过这些概念,但是自己了解的都很碎片,不够整体,没能形成一个整体的认知框架;今天趁此机会,梳理了下他们之间的关系,目的是把这一切串起来形成一个整体的理解。

如果在看完本文后,能帮你建立一个整体的认知,那最好不过了。由于本文的理解比较个人化,可能在某些地方不够准确,如果您追求准确性,推荐去看专业的书籍。

1. 起源

首先,这些概念都是伴随着并发情况产生 的,它们都和并发脱不了干系。

2. 锁

早期,为了保证数据一致性和隔离性,提出了锁的概念。在需要保证一致性的场景,直接加锁,不让其它事务操作就OK了,很暴力,但是很有效。

但是,使用锁也会导致很多问题,比如:

  1. 读阻塞(即使是最简单的共享锁,也不允许在更新的同时读取数据),以我们现在的眼光来看,这是不可接受的,我们想象一下,线上有一条数据在更新的时候,用户想要查看这条数据,确被阻塞了(卡住了)这是什么后果么?
  2. 锁开销太大(从感觉上来说,加锁还是比较重的)
  3. 非常容易死锁(在锁多了后,非常容易形成锁竞争,相互等待锁释放的情况,非常容易死锁)

3. mvcc

这个时候数据库的设计者,认识到了问题的严重性,后来慢慢提出了mvcc 的方案,现在主流的关系数据库都是实现了mvcc

那什么是mvcc呢? mvcc是英文缩写,全程为(Multi-Version Concurrency Control 多版本并发控制),看到了吧!并发控制。

它的具体实现很复杂,我们不会深入到底层实现,了解下它实现的大体方式就行,它的实现主要有下面几个核心点:

  1. 版本控制/版本链

    每当要更新一个数据时,并不会直接在原来的数据上做覆盖修改,而是新生成一个数据版本;如果有其它事物也要对这个数据修改,也会生成数据版本,这些数据版本之间会形成一条链条就是版本链,可以通俗的理解成和git版本差不多。

  2. 视图控制

    由于我们已经有数据的版本链,那么我们可以结合根据隔离级别,控制在什么级别下,哪些数据版本对外是可见的,做到隐藏和可见

  3. 快照

    在可重复读的隔离级别下,事务一开始就会保存一个数据的快照,从而保证在整个事务执行过程中,随时读取数据都和最初一致。

有了mvcc的加入,我们可以做到减少锁的使用,减少了死锁的发生,在更新数据的同时也能读取数据(读不阻塞)啦。从此以后,锁和mvcc并肩作战,相互补充,一起成就了现在的数据库。

在日常的开发过程中,我们对数据库的使用,可能对锁是有察觉的,但是对mvcc的功劳却没有太直观的感受。它就像一个隐秘者一样,在一个使用者注意不到角落,默默发挥自己的功能,可以说现在的关系性数据库能有现在的性能,都和mvcc的默默支持密不可分。

4. 脏读、不可重复读、幻读

那什么又是隔离级别呢? 隔离级别并不能独立来看,它必须要和并发情况下产生的问题(脏读、不可重复读、幻读)一起来看。

  • 脏读:非常好理解就是读到了还未提交的数据
  • 不可重复读:在事务中对同一数据的两次读取,读到的值可能不一样------在第二次读之前,有事务提交做了修改。
  • 幻读:在事务中根据某一条件筛选得到的数据条数,前后两次不一样,在这个过程中可能有事务插入或删除数据。

PS:

不可重复读针对的是同一条数据,而幻读针对的是一个查询范围的数据

5. mvcc

好啦!并发产生的问题就为了,下面我们再来看看引申出来的隔离级别。

  1. 读未提交(Read Uncommitted)
  2. 读已提交(Read Committed)- 解决了 脏读
  3. 可重复读(Repeatable Read)- 解决 不可重复读
  4. 序列化(Serializable)- 解决了 幻读

后面三个隔离级别,分别解决了不同的问题;一般默认情况下,数据库的隔离级别是(Read Committed),这个级别不高不低刚刚好,级别越高越能解决的问题也就越多,但是代价也就越大;在串行化隔离级别下,所有的操作都不能一起执行,可以想象那是多么慢的一件事。

6. 回到sql

前面说了一大堆概念性的东西,但是我们实际开发时,面对的还是sql语句,所以在结尾部分我还是回到sql语句结合实践做一些分析。

在默认隔离级别下,数据的写操作(创建/更新/删除)数据库都会默认的加上排斥锁。比如:update users set name = '12' where id = 15;这句更新sql

开启三个数据库连接 session1:

sql 复制代码
begin transaction;
update users set name = '12' where id = 15;

session2:

sql 复制代码
update users set name = '23' where id = 15; // 更新 此时会阻塞

session3:

sql 复制代码
select * from users where id = 15; // 读此时不会阻塞

此时读不会阻塞,写会阻塞,因为写加的互斥锁,所以会阻塞;但是在读的时候,由于有mvcc的存在,它是不会读到未提交数据的,不会造成脏读,读取的时候,实际是读到的对外可见(已提交的版本),整个过程不发生阻塞。

其它的一些思考,在业务上我们很多时候会一次更新大量的数据,很多时候会直接采用如下方式update users set name = where age > 18

如果大于18的user很多,我们会有一个直观感受就是慢,或者过了很久后,看到数据库报了一个死锁的错误;那么这个更新一次会对很多行数据加锁,如果这个时候,有其它事务也尝试对其中的某一行或多行数据做修改,那么会阻塞,非常非常容器造成死锁。

因此,如果在涉及大量数据时候,我们最好缩小范围,确保加锁的数据行不要太多,不至于阻塞或者死锁

相关推荐
Tttian62221 分钟前
基于Pycharm与数据库的新闻管理系统(2)Redis
数据库·redis·pycharm
小池先生41 分钟前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
做梦敲代码1 小时前
达梦数据库-读写分离集群部署
数据库·达梦数据库
小蜗牛慢慢爬行2 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
hanbarger2 小时前
nosql,Redis,minio,elasticsearch
数据库·redis·nosql
微服务 spring cloud2 小时前
配置PostgreSQL用于集成测试的步骤
数据库·postgresql·集成测试
先睡2 小时前
MySQL的架构设计和设计模式
数据库·mysql·设计模式
弗罗里达老大爷2 小时前
Redis
数据库·redis·缓存
wm10433 小时前
java web springboot
java·spring boot·后端