MVCC:为什么多人同时读写数据,数据库还能不乱

没有隔离会发生什么

学习 MVCC 之前,先来看为什么需要它。

上篇文章有讲到 ACID,重点是 隔离性,如果没有隔离会出现什么问题

  1. 脏读 这里 事务2 已经基于 已扣款 也就是 余额 =1000 - 500 = 500 去做决策了,但是 A 的余额=1000

更严重的例子:

css 复制代码
事务A 初始余额1000,执行了 update 余额 = 10000,没有commit
事务B 读到余额 = 10000,允许取款8000
事务A 回滚,余额还是1000
后果:银行少8000块
  1. 不可重复读

这里,同一事务内,同一行数据前后两次读的值不同,这就是不可重复读。

更严重一点的例子:

ini 复制代码
第一步:select stock from products where id=1 ->查出来 stock=10
判断 10> 0,够卖
第二步:update stock = stock -1 where id=1

但是,这两步中间有人插进来了,给 stock 改成了 0
->你依旧基于 stock=10 做决策 -> 卖超了
  1. 幻读

这里,并不是某一行的值变了,而是符合条件的行数变了

更严重的例子:

sql 复制代码
第一步:SELECT COUNT(*) FROM admins WHERE role='super' → 1 人
判断:只有一个超级管理员

有人 insert 了一个新的 super admin

第二步:执行某个"只允许唯一超管操作"的逻辑
-> 实际有两个超管,安全假设被打破

这些现象说明了隔离性为什么重要。PostgreSQL 支持四个隔离级别:

  1. Read Uncommitted(读未提交)
  2. Read Committed(读已提交)
  3. Repeatable Read(可重复读)
  4. Serializable(串行化)

通常隔离越强,冲突、等待或重试的成本也可能越高,但它们不是简单的线性反比:

首先 PostgreSQL 的默认级别是 Read Committed(读已提交)。其次,你可以设置 Read Uncommitted,但是 PostgreSQL 会按 Read Committed 来处理,所以在 PostgreSQL 里不会发生脏读。

上面的脏读例子是数据库通用概念,用来说明"没有隔离"会有多危险,并不是 PostgreSQL 实际允许的行为。

PostgreSQL 到底怎么控制"看见什么数据"

那么 Read Committed 到底怎么做的?

规则只有一条:每条 SQL 语句执行的瞬间,只能看到在那一瞬间之前已经 COMMIT 的数据

那么 Repeatable Read 怎么防住不可重复读?

规则变了一条: 整个事务会固定使用同一份快照。你可以先理解成,事务第一次查询时看到什么,后面普通查询就继续看这份结果,无论别人后来 COMMIT 了什么。

但是 Repeatable Read 并没有把整张表锁住,也没有阻止别人修改。别的事务照样可以 insert、update、delete,完全不受影响。

它做的只是让你自己的普通读取始终看同一份快照,所以后来插入的符合条件的新行,你也不会突然看见。至于库存、唯一超级管理员这类业务规则,仍然要配合行锁、唯一约束,或者 Serializable 来保证。

那怎么做到"两次读的结果一样"的?

MVCC:让读和写各看各的版本

那么 MVCC 终于出来了,它解决了并发读写的根本矛盾。

数据库面对两个对立需求:

  • 读的想要稳定的数据 -- 别在我看的时候改
  • 写的人想要马上改 -- 别让我等

如果用 锁 来解决,写的时候锁住,读的人排队等,这是很安全,但慢的不可接受

MVCC = 多版本并发控制 (Multi-Version Concurrency Control)

解决方法就是:给数据拍快照

做法如下:

  1. 先有一个原始数据:

2. 执行 update price = 200 (旧版本标记删除 + 插入新版本)

两个版本同时存在,这就是 多版本 的意思

由快照决定你看到哪个版本

text 复制代码
事务 75(begin 在 txn 80 之前开始) 
->快照冻结点 txn 80 之前 
->看到 price = 100

事务 90(begin 在 txn80 commit 之后)
->快照冻结点 txn 80 之后
->看到price =200

MVCC 不够时,还是要用锁

MVCC 主要解决的是"读不必等写,写也不必等读"。但在 MVCC 不够用的情况下,还是要用到锁,比如:

写并发--两个事务同时要改同一行

那 PostgreSQL 就必须用 锁 了

死锁:互相等对方

那么怎么处理?难不成一直等下去?

不会。PostgreSQL 发现事务等待超过 deadlock_timeout(默认 1 秒)后,会检查有没有死锁:

  • 发现A 和 B形成等待环路
  • 选择其中一个事务中止
  • rollback 该事务,释放它持有的锁
  • 另一个事务继续执行

那我们如何避免死锁的出现?

先看死锁是怎么形成的:

bash 复制代码
事务A:先锁 id=1,再锁 id=2
事务B:先锁 id=2,再锁 id=1

可以看出,事务B反过来先给 id=2 给锁住了

如果是这样做的话

bash 复制代码
事务A:先锁 id=1,再锁 id=2
事务B:先锁 id=1,再锁 id=2

事务B会先等事务A释放 id=1 的锁,不会形成交叉等待。

所以答案就是:所有事务按固定顺序访问资源(比如按 id 从小到大)。顺序一致,等待环路就不容易形成。

总结

最后,如果只用一句话理解 MVCC,那就是:它不是阻止别人修改数据,而是让不同事务在并发时,各自看到适合自己的数据版本。

Read Committed 让每条 SQL 看执行时已经提交的数据;Repeatable Read 让同一个事务的普通读取固定在同一份快照上;当两个事务真的要改同一行,或者业务规则不能接受并发冲突时,数据库还是会回到锁和事务控制。

所以 MVCC 和锁不是谁取代谁。MVCC 负责让大多数读写并行起来,锁负责处理不能同时进行的那一小部分关键操作。

相关推荐
毛驴赶鹿6 小时前
【金仓数据库征文】KES V9性能调优实战记录:从慢SQL排查到全链路优化
数据库·sql
码农阿豪6 小时前
【金仓数据库征文】Mac开发环境|SpringBoot3 + MyBatisPlus对接KES V9 Docker实战全指南
数据库·macos·docker
杨云龙UP6 小时前
MySQL 数据库常用备份工具对比:mysqldump、mydumper 与 XtraBackup
linux·运维·服务器·数据库·mysql·dba·备份恢复
ZENERGY-众壹6 小时前
500个工商业电站压垮数据库?海量光伏时序数据存储的架构取舍
数据库·架构·influxdb·光伏·光伏运维
mubei-1237 小时前
SpringDAO的用法
java·开发语言·数据库
星蓝_starblue10 小时前
零服务器、零数据库!开源growth-board,利用GitHub自动管理刷题/学习/求职全流程
服务器·数据库·程序人生·系统架构·node.js·github·改行学it
三84412 小时前
sqli-labs1-10通关笔记
数据库
BullSmall13 小时前
Another Redis Desktop Manager(ARDM)下载指南
数据库·redis·缓存
SelectDB14 小时前
Apache Doris 倒排索引工作原理:全文检索提速 59 倍,点查提速 14 倍
数据库
码农颜14 小时前
5.1.1 原⼦性
数据库·mysql