像Git一样管理数据:深入解析数据库并发控制MVCC的实现

MVCC

多版本并发控制(Multi-version Concurrency Control, MVCC)是一种通过维护数据多个版本来实现并发控制的技术。其基本思想是为每次事务生成一个新版本的数据,在读数据时选择不同版本的数据即可以实现对事务结果的完整性读取。在使用MVCC 时,每个事务都是基于一个已生效的基础版本进行更新,事务可以并行进行,从而可以产生一种图状结构。

如图所示,基础数据的版本为1,同时产生了两个事务:事务A与事务B。这两个事务都各自对数据进行了一些本地修改,这些修改只有事务自己可见,不影响真正的数据。之后事务A首先提交,生成数据版本2;基于数据版本2,又发起了事务C,事务C继续提交,生成了数据版本3;最后事务B提交,此时事务B的结果需要与事务C的结果合并,如果数据没有冲突,即事务B没有修改事务A与事务C修改过的变量,那么事务B可以提交,否则事务B提交失败。

事务在基于基础数据版本做本地修改时,为了不影响真正的数据,通常有两种做法。

1)将基础数据版本中的数据完全拷贝出来再修改;

2)每个事务中只记录更新操作,而不记录完整的数据,读取数据时再将更新操作应用到用基础版本的数据从而计算出结果。

MVCC的设计理念深受版本控制系统(如Git)的影响,其工作流程与版本控制系统的操作流程高度相似。在事务处理中,上述两种策略各有优势,完整拷贝策略简单直观,但可能占用较多存储空间;增量记录策略则更为高效,能有效减少存储开销。

MVCC的工作流程在很大程度上类似于版本控制系统(如Git)的操作流程,可以说,Git等版本控制系统的设计理念深受MVCC思想的影响。

事务处理

MVCC重点不在于并发控制,而在于实现事务(Transaction)。假设在一个关系型数据库中,更新操作以事务进行,每个事务包括对若干行数据的更新操作。更新事务必须具有原子性,即事务中的所有更新操作要么同时生效,要么都不生效。

在事务无法生效,即需要进行事务回滚时,通常会依赖于回滚日志(Undo Log)。回滚日志是一种专门用于事务恢复的日志技术,它详细记录了事务在执行过程中对数据的所有修改。若事务失败或系统崩溃,回滚日志能够用于将数据恢复至事务开始前的状态。

以MySQL为例,在MVCC中,对于每次更新操作,旧值会被保存到一条回滚日志日志中,即它是该记录的旧版本。随着更新次数的增加,所有的版本都会通过回滚指针(Roll Pointer)连接成一个链表,称之为版本链。链首就是最新的记录,链尾就是最早的旧记录。

举个例子,比如有个事务A插入了一条新记录:insert into user(id, name) values(1, '张三')。

现在来了一个事务B对该记录的name做出了修改,改为"李四"。

在事务B修改该行数据时,数据库会先对该行加锁,然后把该行数据拷贝到回滚日志中作为旧记录,即在回滚日志中有当前行的拷贝副本。

拷贝完毕后,修改该行name为"李四",并且修改该行的事务ID为当前事务B的ID, 并将回滚指针指向拷贝到回滚日志的副本记录,即表示上一个版本就是它,事务提交后,释放锁。

此时又来了个事务C修改同一个记录,将name修改为"王五"。

在事务C修改该行数据时,数据库也先为该行加锁,然后把该行数据拷贝到回滚日志中,作为旧记录,发现该行记录已经有回滚日志了,那么最新的旧数据作为链表的表头,插在该行记录的回滚日志最前面。

现在想回滚到事务B,name值为"李四"的时候,只需通过回滚日志的链表指针顺着列表找到对应的回滚日志日志,将旧值恢复到数据行即可。

隔离级别

事务隔离级别是数据库系统中用于控制并发访问的机制,以确保数据的一致性和完整性。常见的事务隔离级别包括:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、序列化(Serializable)。

1)读未提交:读未提交隔离级别允许事务读取其他事务未提交的数据,可能导致脏读(Dirty Read)。写操作可能使用锁,但读操作不等待其他事务的锁释放。

java 复制代码
transaction T1 {
    // 不使用锁
   read(data);
    // 写操作
    write(data);
    commit();
}
 
transaction T2 {
    // 不使用锁
    read(data); // 可能从T1读到未提交的数据
    // 写操作
    write(data);
    commit();
}

2)读已提交:只允许事务读取其他事务已提交的数据,防止脏读。读操作通常使用共享锁(Shared Lock),且在读取后立即释放。写操作使用排他锁(Exclusive Lock),直到事务提交时释放。

java 复制代码
transaction T1 {
    // 写操作使用排他锁
    lock(exclusive, data);
    write(data);
    commit();
    // 释放排他锁
    unlock(exclusive, data);
}
 
transaction T2 {
    // 读操作使用共享锁
    lock(shared, data);
    read(data); // 只读取已提交的数据
    // 读取数据以后,立即释放共享锁
    unlock(shared, data);

    // 写操作使用排他锁
    lock(exclusive, data);
    write(data); 
    commit();
    // 释放排他锁
    unlock(exclusive, data);
}

3)可重复读:确保事务在整个过程中读取的数据一致,防止不可重复读。读操作使用共享锁,并保持直到事务结束。写操作使用排他锁直到事务结束。此外,为防止幻读,可能还需要使用间隙锁(Gap Lock),以锁定数据间隙。

java 复制代码
transaction T1 {
    // 写操作使用排他锁
    lock(exclusive, data);
    write(data);
    commit();
    // 释放排他锁
    unlock(exclusive, data);
}
 
transaction T2 {
    // 读操作使用共享锁
   lock(shared, data);
   read(data); // 在整个事务中读取一致的数据

    // 间隙锁隐式通过查询条件锁定范围(如`WHERE id BETWEEN 1 AND 10`)
    lock(gap, range_data);
    read(range_data); // 锁定间隙,防止幻读 

    // 写操作使用排他锁
    lock(exclusive, data);
    write(data);
    commit();

    // 释放共享锁、间隙锁和排他锁
   unlock(exclusive, data);
   unlock(gap, range_data);
   unlock(shared, data);
}

3)序列化:确保事务完全隔离,防止幻读,提供最高的隔离级别。使用范围锁(Range Lock)等机制,锁定数据的范围,确保事务之间的完全隔离,防止幻读。

java 复制代码
transaction T1 {
    // 对读和写操作,使用范围锁
    lock(range, data);
    read(data);
    write(data);
    commit();
    // 释放范围锁
    unlock(range, data);
}
 
transaction T2 {
    // 对读和写操作,使用范围锁
    lock(range, data);
    read(data); // 避免幻读
    write(data);
    commit();
    // 释放范围锁
    unlock(range, data);
}

MySQL默认选用可重复读作为事务隔离级别,这主要得益于其通过多版本并发控制机制,在事务启动时即创建数据快照。这一设计确保了同一事务内多次读取操作的结果保持一致,从而有效规避了读已提交级别下可能出现的不可重复读问题。

与此同时,InnoDB存储引擎通过运用间隙锁和临键锁(Next-Key Lock)技术,对索引范围进行锁定,显著降低了幻读现象(多次读取同一数据范围时,由于其他事务的插入或删除操作,导致每次读取的结果集不同)的发生概率。这是读已提交级别所无法实现的,因为该级别无法对索引范围进行如此精细的锁定。

尽管在并发写场景下,读已提交级别的性能可能稍胜一筹,但可重复读级别通过快照读(无需加锁)与当前读(需加锁)的巧妙结合,在保障数据一致性的同时,也维持了较高的系统性能。

此外,MySQL在设计上更倾向于优先避免数据异常,特别是在处理银行账户、金融交易等关键业务场景时,数据的一致性和完整性至关重要。当然,用户仍可根据实际需求,手动将事务隔离级别切换至读已提交,以适应高并发写入场景的特殊要求。

总结:从硬抗到疏导,驾驭流量的艺术

缓存层设计需在即时响应和扩展性之间权衡:本地内存访问速度快,但容量有限;跨节点共享数据则会增加网络延迟。同时,要防范缓存穿透(过滤无效请求)、缓存击穿(对热点数据进行加锁控制)和缓存雪崩(分散缓存过期时间)等问题。

消息队列通过异步解耦机制,兼顾实时性与系统容错能力。Kafka利用分区顺序写入特性处理海量数据流,RocketMQ则借助二次确认与补偿机制,确保资金交易等场景无差错。

数据库层借助日志追加记录操作轨迹,采用版本快照隔离读写操作。查询时访问固定版本的数据,避免锁冲突;更新时生成新版本,确保事务的完整性。

并发系统设计的精髓,并非是追求单一组件的极致性能,而是一种关于"流动"与"控制"的艺术。它要求不再将压力视为需要硬抗的敌人,而是将其视为需要引导和疏解的能量。通过分层设计,将一个巨大而不可控的压力问题,分解为一系列更小、更清晰、更易于管理的子问题,并在每一层都做出最恰当的权衡与取舍。

很高兴与你相遇!如果你喜欢本文内容,记得关注哦!!!

相关推荐
小冷coding43 分钟前
【MySQL】MySQL 插入一条数据的完整流程(InnoDB 引擎)
数据库·mysql
周杰伦的稻香4 小时前
MySQL中常见的慢查询与优化
android·数据库·mysql
·云扬·6 小时前
MySQL 常见存储引擎详解及面试高频考点
数据库·mysql·面试
何以不说话7 小时前
mysql 的主从复制
运维·数据库·学习·mysql
橘子138 小时前
MySQL库的操作(二)
数据库·mysql·oracle
·云扬·9 小时前
MySQL各版本核心特性演进与主流分支深度解析
数据库·sql·mysql
田超凡10 小时前
深入理解MySQL_6 Temporary临时表
mysql·java-ee
尽兴-11 小时前
MySQL 8.0主从复制原理与实战深度解析
数据库·mysql·主从复制
YongCheng_Liang11 小时前
MySQL 高级特性深度解析:从索引优化到高可用架构
运维·数据库·mysql
<花开花落>12 小时前
MySQL 数据备份流程化
mysql·systemd