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

数据的多版本管理

理由或者出发点很多。

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

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

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 隔离级别下,数据的一致性和可靠性,同时提供了合理的并发控制。

后续

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

相关推荐
我爱一条柴ya17 分钟前
【AI大模型】RAG系统组件:向量数据库(ChromaDB)
数据库·人工智能·pytorch·python·ai·ai编程
北北~Simple18 分钟前
第一次搭建数据库
服务器·前端·javascript·数据库
鸢想睡觉18 分钟前
【数据库基础 1】MySQL环境部署及基本操作
数据库·mysql
没有口袋啦19 分钟前
《数据库》MySQL备份回复
数据库
c7_ln23 分钟前
MYSQL C_API使用全解
c语言·数据库·mysql
karry013026 分钟前
高并发导致重复key问题--org.springframework.dao.DuplicateKeyException
java·数据库·ide
经典19922 小时前
mysql 锁介绍
数据库·mysql
不太可爱的大白2 小时前
Mysql分片:一致性哈希算法
数据库·mysql·算法·哈希算法
~ 小团子2 小时前
每日一SQL 【游戏玩法分析 IV】
数据库·sql·游戏
零叹2 小时前
MySQL——常用程序and主从复制
数据库·mysql