数据的多版本管理-第一篇-数据库

数据的多版本管理

理由或者出发点很多。

为了追溯数据的变动历史,防止被最新的数据覆盖,以及提升系统并发能力,以及拥有业务系统的矛盾处理依据,我们需要对数据进行多版本管理。

最简单的是,每次变动一版,都增加一条记录,主键赋值按时间戳的方法,或者自增编号的方法排序。

mysql中的多版本管理MVCC

我来举个例子吧,一个复杂的例子,如果两个事务TA,TB,都修改了同一行记录,此时的隔离级别是可重复度,这行为 k1,v1, v1的值为100,TA先修改成了200,TB又修改成了120, TA读了一次,TB又修改值为300,TA撤销,也就是原子性,那么请问undo log如何运作的?

首先把问题拆开来看。 我们假设TA的版本号为v10,TB的版本号为v11。

假设记录k1,v1的隐藏列,我们记录其版本号为v9,也就是上一次修改是v9,TA想要修改v1的值,那么我们发现v9小于v10,我们可以直接修改这个记录。

我们记录一个undo log为 版本v10,k1, v1=100,并且指向上一次的版本号,也就是版本号为v9的那个值。

然后TB 同样修改了,然后它就分叉了,如果事务TA回退了,那么这条分支就自动一路删掉了,如果它先提交了,那么TB再提交的时候,发现当前最新值已经不是v9版本了,而是v10,则提交失败。

更加通用的解释

MVCC(多版本并发控制)可以想象成数据库处理多人同时操作的一种"聪明"办法。我用一个生活中的例子帮你理解:

假设你和同事同时编辑同一份在线文档:

  1. 传统锁机制:

    • 就像一个人编辑时把文档锁住,其他人只能排队等着。你改错别字时,同事连查看内容都不行,效率很低。
  2. MVCC的魔法:

    • 当你要修改文档时,系统不是直接改原文件,而是自动生成一个新版本(比如V2)。
    • 同事查看时,看到的还是修改前的V1版本,就像给文档拍了快照。
    • 你继续修改到V3版本时,另一个同事刚打开的页面仍然显示V1,直到他刷新才会看到最新版。

数据库里的具体表现:

  • 每条数据都有隐藏的时间标签(如创建时间和删除时间)
  • 你查询时,数据库只给你看在你开始操作时已经存在的版本
  • 修改数据就像往本子上写新的一行,旧内容仍然保留(直到不再被需要时自动清理)

好处:

  • 读操作不用等写操作(查余额不用等转账完成)
  • 写操作不用等读操作(转账不用等所有人查完余额)
  • 避免了很多锁冲突,就像高速公路同时允许多辆车通行

代价:

  • 数据库需要额外空间存储旧版本
  • 需要定期清理过期数据(类似手机清理缓存)

这就像时光机------每个操作都定格在某个时间点的数据状态,大家各看各的历史快照,互不干扰。实际在MySQL、PostgreSQL等数据库中正是用这种方式支撑高并发访问的。

ReadView

ReadView(读视图)是MVCC实现中最关键的数据结构,它定义了事务在特定时刻能看到哪些数据版本。以下是其专业解析:


ReadView核心组成

每个ReadView包含4个关键属性:

  1. creator_trx_id
    当前事务自身的ID(只有写操作的事务才有trx_id,只读事务为0)
  2. m_ids
    生成ReadView时,系统中所有活跃(未提交)事务ID的集合
  3. min_trx_id
    m_ids中的最小值(即最老的活跃事务ID)
  4. max_trx_id
    系统预分配给下一个事务的ID值(非当前最大事务ID)

可见性判断规则

当访问某行数据时,按以下逻辑判断版本可见性(假设当前行的事务ID为trx_id):

python 复制代码
if trx_id < min_trx_id: 
    # 该版本在ReadView创建前已提交 → 可见
    return VISIBLE
elif trx_id >= max_trx_id: 
    # 该版本由未来事务生成 → 不可见
    return NOT_VISIBLE
elif trx_id in m_ids: 
    # 该版本所属事务仍活跃 → 不可见
    return NOT_VISIBLE
else: 
    # 该版本已提交且不在活跃列表中 → 可见
    return VISIBLE

例外处理 :若当前事务自身修改了该行(即trx_id == creator_trx_id),则直接可见最新修改


不同隔离级别的ReadView生成策略

隔离级别 ReadView生成时机 可见性效果
Read Committed (RC) 每次执行SELECT语句时新建ReadView 总是看到最新已提交数据
Repeatable Read (RR) 事务的第一个SELECT时创建ReadView 整个事务看到相同数据快照

示例场景(RR级别):

sql 复制代码
-- 事务A(trx_id=100)启动
START TRANSACTION; 

-- 第一次查询,生成ReadView:
-- m_ids=[90,95], min_trx_id=90, max_trx_id=101
SELECT * FROM users; -- 看到版本V1

-- 事务B(trx_id=95)提交修改
COMMIT; 

-- 事务A再次查询,复用原有ReadView:
SELECT * FROM users; -- 仍然看到V1(避免不可重复读)

ReadView与Undo Log的协作

  1. 当需要访问旧版本时:

    • 通过DB_ROLL_PTR找到Undo Log中的历史版本
    • 递归判断每个历史版本是否满足当前ReadView的可见性规则
  2. 版本链遍历示例:

    text 复制代码
    当前行版本(trx_id=95)→ 不符合ReadView规则
    ↓ 通过DB_ROLL_PTR回溯
    版本V2(trx_id=90)→ 符合可见性 → 返回此版本

实现细节深度解析

  1. 快照时效性

    PostgreSQL使用xmin/xmax系统列维护版本可见性,而MySQL通过ReadView+Undo Log实现

  2. 内存优化

    InnoDB将ReadView缓存到内存,多个事务可能共享同一个ReadView(当无写事务时)

  3. 可见性判断加速

    通过min_trx_id快速过滤旧版本,避免遍历整个活跃事务列表

  4. 长事务影响

    若存在长时间未提交的事务(trx_id < min_trx_id),会导致大量旧版本无法被Purge


性能影响分析

  1. 最佳实践

    • 在RC级别,频繁更新的热点数据查询可能更高效(每次看到最新提交)
    • 在RR级别,复杂事务中的多次读取更安全(但可能读到旧数据)
  2. 监控指标

    sql 复制代码
    SHOW ENGINE INNODB STATUS\G 
    -- 观察TRX_IDS_IN_VIEW字段反映活跃事务数量
  3. 常见问题

    • ReadView过大(活跃事务过多)→ 可见性判断延迟增加
    • Undo Log链过长→ 查询性能下降(需控制事务时长)

通过ReadView机制,数据库实现了动态版本过滤:每个事务如同戴上了特制的"时间眼镜",只能看到符合其可见性规则的数据版本。这种设计在保证隔离性的同时,避免了传统锁机制的资源争用问题。

例子测试

同时修改同一条数据

MySQL Shell中,开启事务进行测试。

先看基础表

sql 复制代码
select * from sys_user; -- 测试表

部分字段结果如下:

id | username  | real_name 
2  | 010222    | 张三2 

查看事务等级,确认一下事务等级为 可重复读。

sql 复制代码
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.01 sec)

同时开启两个会话窗口(测试同时修改同一条数据),显式声明事务开启

arduino 复制代码
mysql> begin transaction;

在第一个会话里

sql 复制代码
mysql> update sys_user set real_name = '张三3' where id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from sys_user;

部分字段结果如下:

id | username  | real_name 
2  | 010222    | 张三3 

在第二个会话里,执行类似的修改

bash 复制代码
mysql> update sys_user set real_name = '张三4' where id = 2;



ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

可见,这是不行的。在可重复读级别,没法同时修改同一条数据。

条数统计

在上面的环境中,尝试 repeatable-read能否实现准确的行数统计呢?

在已经开启事务的情况下,在一个事务窗口中执行:

sql 复制代码
mysql> insert into sys_user(username, password, real_name) values(010223,'123', '王五');
Query OK, 1 row affected (0.01 sec)

mysql> select count(*) from sys_user;
+----------+
| count(*) |
+----------+
|        3 |
+----------+
1 row in set (0.00 sec)

在另外一个窗口中执行

sql 复制代码
mysql> select count(*) from sys_user;
+----------+
| count(*) |
+----------+
|        2 |
+----------+
1 row in set (0.00 sec)

可见,简单情况下,可重复读级别还是可以消除幻读的

解释

在 MySQL 的 可重复读(REPEATABLE READ) 隔离级别下,同一事务中的多个会话是否可以同时修改同一条数据 取决于具体的操作和事务的并发控制机制。以下是详细的解释:

1. 同一事务内部

同一个事务 内部,可以 对同一条数据进行多次修改。每次修改都会覆盖前一次的值,最终事务提交时,数据库会保存最后一次修改的结果。

示例:

sql 复制代码
START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 第一次修改
UPDATE accounts SET balance = balance + 50 WHERE id = 1;  -- 第二次修改

COMMIT;

在上述示例中,id = 1 的账户余额先减少 100,再增加 50,最终 balance 会比原始值少 50。

2. 不同事务之间

不同的事务 之间,是否可以同时修改同一条数据 取决于事务的隔离级别和具体的操作:

a. 并发修改(写-写冲突)

REPEATABLE READ 隔离级别下,MySQL 使用 行级锁(Row-Level Locking) 来防止 写-写冲突。这意味着:

  • 当一个事务正在修改某一行数据时,其他事务如果尝试 修改同一行 ,将会被 阻塞,直到当前事务提交或回滚。
  • 这样可以确保 数据的一致性 ,避免出现 脏写(Dirty Write) 的情况。

示例场景:

假设有两个事务 T1 和 T2 同时尝试修改 id = 1 的账户余额:

  • T1 开始事务:

    ini 复制代码
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
  • T2 尝试同时开始事务并修改同一行:

    sql 复制代码
    START TRANSACTION;
    UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 这里会被阻塞
  • T1 提交事务后,T2UPDATE 操作才会执行,并基于 T1 提交后的最新值进行修改。

b. 并发读取(读-写场景)

REPEATABLE READ 隔离级别下:

  • 当前事务 可以多次读取同一行数据,并且每次读取的结果都是一致的(即事务开始时的快照)。
  • 其他事务 对同一行数据的 修改 在当前事务提交之前是不可见的,确保了 可重复读 的特性。

示例场景:

  • T1 开始事务并读取 id = 1 的余额:

    sql 复制代码
    START TRANSACTION;
    SELECT balance FROM accounts WHERE id = 1; -- 假设返回 1000
  • T2 修改 id = 1 的余额:

    ini 复制代码
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT;
  • T1 再次读取 id = 1 的余额:

    sql 复制代码
    SELECT balance FROM accounts WHERE id = 1; -- 仍然返回 1000,直到 T1 提交或回滚
  • T1 提交事务后(T1事务关闭后!!),再次读取才会反映最新的值:

    sql 复制代码
    COMMIT;
    SELECT balance FROM accounts WHERE id = 1; -- 返回 900

3. 防止幻读

虽然 REPEATABLE READ 可以防止 不可重复读 ,但在某些情况下,仍然可能出现 幻读(Phantom Read)。幻读指的是在一个事务中多次执行相同的查询,但返回的结果集在行数上发生了变化(即新增或删除的行)。

MySQL 如何处理:

  • InnoDB 存储引擎 通过 Next-Key Locking 机制,在 REPEATABLE READ 隔离级别下 部分防止幻读 。但对于某些复杂的查询,仍可能出现幻读 。如果需要完全防止幻读,可以使用 SERIALIZABLE 隔离级别,但这会显著降低并发性能。

    这种可能还是没整明白。什么情况下可能出现?

总结

  • 同一事务内部:可以多次修改同一条数据,后一次修改会覆盖前一次。

  • 不同事务之间

    • 写-写操作:会被阻塞,直到前一个事务完成,确保数据一致性。
    • 读-写操作:读取操作基于事务开始时的快照,不受其他事务修改的影响,直到当前事务结束。

这种机制确保了在 REPEATABLE READ 隔离级别下,数据的一致性和可靠性,同时提供了合理的并发控制。

后续

下一篇,我们聊聊传统数据库以外的多版本管理。

相关推荐
Li zlun15 分钟前
MySQL 性能监控与安全管理完全指南
数据库·mysql·安全
养生技术人1 小时前
Oracle OCP认证考试题目详解082系列第48题
运维·数据库·sql·oracle·database·开闭原则·ocp
海阳宜家电脑1 小时前
Lazarus使用TSQLQuery更新的一点技巧
数据库·lazarus·tsqlquery
丨我是张先生丨2 小时前
SQLSERVER 查找存储过程中某个变量
数据库
感谢地心引力2 小时前
【Python】基于 PyQt6 和 Conda 的 PyInstaller 打包工具
数据库·python·conda·pyqt·pyinstaller
lypzcgf4 小时前
Coze源码分析-资源库-编辑数据库-后端源码-数据存储层
数据库·coze·coze源码分析·智能体平台·ai应用平台
jackaroo20204 小时前
后端_Redis 分布式锁实现指南
数据库·redis·分布式
liuy96154 小时前
迷你论坛项目
数据库
杨云龙UP4 小时前
小工具大体验:rlwrap加持下的Oracle/MySQL/SQL Server命令行交互
运维·服务器·数据库·sql·mysql·oracle·sqlserver
阿巴~阿巴~4 小时前
使用 C 语言连接 MySQL 客户端(重点)
服务器·数据库·sql·mysql·ubuntu