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 负责让大多数读写并行起来,锁负责处理不能同时进行的那一小部分关键操作。

相关推荐
青山木1 小时前
一把 Redis 分布式锁,踩透四个坑:锁争抢、僵尸锁、锁过期、锁丢失
java·数据库·redis·后端
数智化管理手记1 小时前
智能财务如何减少财务加班?智能财务落地需要哪些工具支撑?
大数据·网络·数据库·数据挖掘·精益工程
IvorySQL2 小时前
深度拆解 IvorySQL 去 O 核心解决方案
数据库·人工智能·postgresql
A15362552 小时前
国内好用的WMS仓储管理系统有哪些?万里牛WMS深度评测
大数据·数据库·人工智能
SelectDB2 小时前
快手从 ClickHouse 到 Apache Doris 的百 PB 数据、200+集群迁移实践
数据库·性能优化·开源
衣乌安、2 小时前
Redis 分布式锁与任务状态
数据库·redis·分布式
踏月的造梦星球3 小时前
DMHS同步原理与DM8到DM8基础搭建
数据库·oracle
Hardworking6663 小时前
第6章 数据工程
运维·服务器·数据库·数据工程
Elastic 中国社区官方博客4 小时前
4 个英伟达人工智能任务,1 个 Elasticsearch 接口:嵌入、聊天、completion 和重排序
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai