PG vs MySQL mvcc机制实现的异同

news2025/1/20 15:49:44

MVCC实现方法比较

MySQL
写新数据时,把旧数据写入回滚段中,其他人读数据时,从回滚段中把旧的数据读出来

PostgreSQL
写新数据时,旧数据不删除,直接插入新数据。

MVCC实现的原理

PG的MVCC实现原理

  • 定义多版本的数据——使用元组头部信息的字段来标示元组的版本号
  • 定义数据的有效性、可见性、可更新性——通过当前的事务快照和对应元组的版本号判断
  • 实现不同的数据库隔离级别——通过在不同时机获取快照实现

PG的数据多版本实现

pg中元组由三部分组成——元组头结点、空值位图、用户数据。没一行元组,都有一个版本号。
该版本由如下几个数据组成。

t_xmin:保存插入该元组的事务txid(该元组由哪个事务插入)
t_xmax:保存更新或删除该元组的事务txid。若该元组尚未被删除或更新,则t_xmax=0,即invalid
t_cid:保存命令标识(command id,cid),指在该事务中,执行当前命令之前还执行过几条sql命令(从0开始计算)
t_ctid:一个指针,保存指向自身或新元组的元组的标识符(tid)。

当更新该元组时,t_ctid会指向新版本元组。若元组被更新多次,则该元组会存在多个版本,各版本通过t_cid串联,形成一个版本链。通过这个版本链,可以找到最新的版本。t_ctid是一个二元组(页号,页内偏移量),其中页号从0开始,页内偏移量从1开始。
元组insert时版本号规则
postgres=# CREATE TABLE test (id int);
CREATE TABLE
postgres=# begin;
BEGIN
postgres=*# SELECT txid_current();
 txid_current 
--------------
          778
(1 row)

postgres=*# insert into test values(1);
INSERT 0 1
postgres=*# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('test', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |    778 |      0 |     0 | (0,1)
(1 row)
  • t_xmin 被设置为778,表示插入该元组的txid
    (当事务开始,事务管理器会为该事务分配一个txid(transaction id)作为唯一标识符。)
  • t_xmax 被设置为0,因为该元组还未被更新或删除过
  • t_cid 被设置为0,因为这是该事务的第一条命令
  • t_ctid 指向自身,被设置为(0,1),表示该元组位于0号page的第1个位置上
元组delete时版本号规则

pg的删除只是将目标元组在逻辑上标为删除(将t_xmax设为执行delete命令的事务txid),实际该元组依然存在于数据库的存储页面,直至该元组被清理进程清理掉。

postgres=# begin;
BEGIN
postgres=*# SELECT txid_current();
 txid_current 
--------------
          779
(1 row)

postgres=*# delete from test where id=1;
DELETE 1
postgres=*# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('test', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |    778 |    779 |     0 | (0,1)
(1 row)

  • t_xmin 不变,表示插入该元组的txid
  • t_xmax 被设置为779,即删除该元组的txid
  • t_cid 被设置为0,因为这是该事务的第一条命令
  • t_ctid 指向自身,被设置为(0,1),表示该元组位于0号page的第1个位置上

当txid=779的事务提交时,tuple_1就不再需要了,称为dead tuple。但是这个tuple依然残留在页面上, 随着数据库的运行,这种死元组越来越多,它们会在VACUUM时最终被清理掉。

元组update时版本号规则

pg不会直接修改数据,而是将目标元组标记为删除,并插入一条新元组,同时修改t_ctid执行新版本元组。

postgres=# begin;
BEGIN
postgres=*# SELECT txid_current();
 txid_current 
--------------
          783
(1 row)

postgres=*# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('test', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |    778 |    779 |     0 | (0,1)
     2 |    781 |      0 |     0 | (0,2)
     3 |    782 |      0 |     0 | (0,3)
(3 rows)

postgres=*# update test set id = 8;
UPDATE 1
postgres=*# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid FROM heap_page_items(get_raw_page('test', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |    778 |    779 |     0 | (0,1)
     2 |    781 |      0 |     0 | (0,2)
     3 |    782 |    783 |     0 | (0,4)
     4 |    783 |      0 |     0 | (0,4)
(4 rows)

Tuple_3

  • t_xmin 不变,表示插入该元组的txid
  • t_xmax 被设置为783,即删除该元组的txid
  • t_cid 被设置为0,因为这是该事务的第一条命令
  • t_ctid 指向新版本元组,被设置为(0,4),表示新元组位于0号page的第4个位置上

Tuple_4

  • t_xmin 被设置为783,表示插入该元组的txid
  • t_xmax 被设置为0,因为该元组还未被更新或删除过
  • t_cid 被设置为1,因为这是该事务的第一条命令
  • t_ctid 指向自身,被设置为(0,4),表示该元组位于0号page的第4个位置上

PG的事务快照实现

事务状态

pg定义了四种事务状态——IN_PROGRESS, COMMITTED, ABORTED和SUB_COMMITTED。

事务快照

事务快照就是当一个事务执行期间,那些事务active、那些非active。即这个事务要么在执行中,要么还没开始。

postgres=*# SELECT txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 796:796:
(1 row)

快照由这样一个序列构成 xmin:xmax:xip_list

  • xmin : 最早的active的 tid,所有小于该值的事务状态为visible(commit)或dead(abort)
  • xmax: 第一个还未分配的xid,大于等于该值的事务在快照生成时都不可见
  • xip_list 快照生成时所有active事务的txid

事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:

查看当前所有的未提交并活跃的事务,存储在数组中
选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
选取所有已提交事务中最大的XID,加1后记录在xmax中
根据不同的情况,赋值不同的satisfies,创建不同的事务快照

可见性举例子

session 1:

postgres=# create table test(id int);
CREATE TABLE
postgres=# insert into test values(1);
INSERT 0 1
postgres=# begin;
BEGIN
postgres=*# insert into test values(2);
INSERT 0 1
postgres=*# select txid_current();
 txid_current 
--------------
          791
(1 row)

postgres=*# select * from heap_page_items(get_raw_page('test',0));
 lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid |   t_data   
----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------
  1 |   8160 |        1 |     28 |    790 |      0 |        0 | (0,1)  |           1 |       2304 |     24 |        |       | \x01000000
  2 |   8128 |        1 |     28 |    791 |      0 |        0 | (0,2)  |           1 |       2048 |     24 |        |       | \x02000000
(2 rows)

session 2

postgres=# select txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 791:791:
(1 row)

postgres=# select * from test;
 id 
----
  1
(1 row)

session 1的事务791在session 2中并不可见,不仅因为txid>=xmax,还因为791的事务状态是

postgres=# select txid_status('791');
 txid_status 
-------------
 in progress
(1 row)

emp2
session 1

postgres=# begin;
BEGIN
postgres=*# insert into test values(5);
INSERT 0 1
postgres=*# select txid_current();
 txid_current 
--------------
          793
(1 row)

postgres=*# rollback;

该事务回滚
在session2 中

postgres=# select * from test;
 id 
----
  1
  2
  3
(3 rows)

postgres=# select txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 794:794:
(1 row)

postgres=# select txid_status('793');
 txid_status 
-------------
 aborted
(1 row)

虽然txid<xmin 但是事务状态为aborted所以依然不可见。

PG的隔离级别实现

PostgreSQL中根据获取快照时机的不同实现了不同的数据库隔离级别

  • 读未提交/读已提交:每个query都会获取最新的快照CurrentSnapshotData
  • 重复读:所有的query 获取相同的快照都为第1个query获取的快照FirstXactSnapshot
  • 串行化:使用锁系统来实现

比如说

session 1中

postgres=# truncate table test;
TRUNCATE TABLE
postgres=# insert into test values(1);
INSERT 0 1
postgres=# begin;
BEGIN
postgres=*# insert into test values(2);
INSERT 0 1
postgres=*# commit;
COMMIT

表test中插入两条数据,再插入第二条数据的时候开启了session 2,且隔离级别为RR,即使session 1提交了第二个事务,session 2 的快照依然没有变,也就没法读取到最新的数据。


postgres=# begin transaction isolation level repeatable read ;
BEGIN
postgres=*# select * from test;
 id 
----
  1
(1 row)

postgres=*# select txid_current_snapshot();
 txid_current_snapshot 
-----------------------
 796:796:
(1 row)

postgres=*# select * from test;
 id 
----
  1
(1 row)

MySQL的MVCC实现原理

MySQL的数据多版本实现

区别于PG使用元组头部信息的字段来标示元组的版本号,MySQL 采用row trx_id来标示行数据的不同版本。同样,InnoDB 也会在事务开始的时候,申请一个顺序递增的事务 ID,叫作 transaction id。并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。

同时,旧的数据版本要保留到undo中,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

这里可以看出MySQL和PG标示不同的数据版本的差异,MySQL将旧数据写入到undo中,用row trx_id标识。而PG因为旧数据并没有删除,还在原堆表上,所以不能只用一个id标识,因此PG使用了t_xmin ,t_xmax等来多个id来和其他版本区分开。

MySQL的事务快照实现

在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。这个功能的实现依赖于UNDO。

InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。也叫快照。

这个其实和PG的实现是一样的低水位就相当于PG快照的xmin,高水位相当于PG快照的xmax。而活跃未提交的事务就相当于PG中的xip_list 。

  • 如果落在低水位之前的部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  • 如果落在高水位的,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  • 如果落在低水位和高水位之间的部分,那就包括两种情况
    a. 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见;
    b. 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

MySQL的隔离级别实现

和PG的实现原理一致,和快照的创建时间有关。

  • 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
  • 在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
  • 这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;
  • “串行化”隔离级别下直接用加锁的方式来避免并行访问。

PG vs MySQL

在MVCC实现上,PG和MySQL的原理类似,只是旧数据的处理上的差异。PG在工作负载频繁更新/删除的情况下,存储空间会过大。pg永远不用担心回滚段不够用的问题,他的rollback可以立刻执行,而对大表的DML操作MySQL回滚会很慢。同样pg会存在一些无用的垃圾数据,所以需要vacuum来定时清理。否则旧版本的数据可能会导致查询需要扫描的数据块增多,从而导致查询变慢。空间持续上涨,存储没有被有效利用的问题也需要考虑到。

参考

PgSQL· 引擎特性 · 多版本并发控制介绍及实例分析
http://mysql.taobao.org/monthly/2019/08/01/

PgSQL · 特性分析 · MVCC机制浅析
http://mysql.taobao.org/monthly/2017/10/01/

事务到底是隔离的还是不隔离的?
https://time.geekbang.org/column/article/70562

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2279426.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL - 主从同步

​​​​​​1.主从同步原理&#xff1a; MySQL 主从同步是一种数据库复制技术&#xff0c;它通过将主服务器上的数据更改复制到一个或多个从服务器&#xff0c;实现数据的自动同步。 主从同步的核心原理是将主服务器上的二进制日志复制到从服务器&#xff0c;并在从服务器上执…

[Python学习日记-78] 基于 TCP 的 socket 开发项目 —— 模拟 SSH 远程执行命令

[Python学习日记-78] 基于 TCP 的 socket 开发项目 —— 模拟 SSH 远程执行命令 简介 项目分析 如何执行系统命令并拿到结果 代码实现 简介 在Python学习日记-77中我们介绍了 socket 基于 TCP 和基于 UDP 的套接字&#xff0c;还实现了服务器端和客户端的通信&#xff0c;本…

STM32Cubemx配置RS485通信

文章目录 一、RS485协议概念讲解RS485 协议概念1. **差分信号传输**2. **半双工通信**3. **多点通信**4. **最大通信距离和速度**5. **终端电阻与偏置电阻**6. **RS485 接口的工作模式**7. **RS485 协议的数据帧结构**8. **RS485 的优点与应用** 总结 二、TTL电平和RS485的关系…

STM32使用DSP库 Keil方式添加

文章目录 前言一、添加DSP库二、使能FPU及配置1. 使能FPU2. 增加编译的宏3.增加头文件的检索路径三. 验证1. 源码中添加2.代码测试前言 添加DSP有两种方案,本文采用的是是Keil 中添加。 一、添加DSP库 在创建好的工程中添加DSP库:步骤如下: 步骤1:选择运行环境管理; 步…

Kotlin Bytedeco OpenCV 图像图像54 透视变换 图像矫正

Kotlin Bytedeco OpenCV 图像图像54 透视变换 图像矫正 1 添加依赖2 测试代码3 测试结果 在OpenCV中&#xff0c;仿射变换&#xff08;Affine Transformation&#xff09;和透视变换&#xff08;Perspective Transformation&#xff09;是两种常用的图像几何变换方法。 变换方…

【Flink系列】10. Flink SQL

10. Flink SQL Table API和SQL是最上层的API&#xff0c;在Flink中这两种API被集成在一起&#xff0c;SQL执行的对象也是Flink中的表&#xff08;Table&#xff09;&#xff0c;所以我们一般会认为它们是一体的。Flink是批流统一的处理框架&#xff0c;无论是批处理&#xff08…

【STM32-学习笔记-11-】RTC实时时钟

文章目录 RTC实时时钟一、RTC简介二、RTC框图三、RTC基本结构四、RTC操作注意事项五、RTC函数六、配置RTCMyRTC.c 七、示例&#xff1a;实时时钟①、main.c②、MyRTC.c③、MyRTC.h RTC实时时钟 一、RTC简介 RTC&#xff08;Real Time Clock&#xff09;实时时钟 RTC是一个独立…

Spring的IoC、Bean、DI的简单实现,难度:※※※

目录 场景描述 第一步&#xff1a;初始化Maven项目 第二步&#xff1a;Maven导入Spring包&#xff08;给代码&#xff09; 第三步&#xff1a;创建Spring配置文件 第四步 创建Bean 第五步 简单使用Bean &#xff08;有代码&#xff09; 第六步 通过依赖注入使用Bean&…

Tensor 基本操作1 | PyTorch 深度学习实战

目录 创建 Tensor常用操作unsqueezesqueezeSoftmax代码1代码2代码3 argmaxitem 创建 Tensor 使用 Torch 接口创建 Tensor import torch参考&#xff1a;https://pytorch.org/tutorials/beginner/basics/tensorqs_tutorial.html 常用操作 unsqueeze 将多维数组解套&#xf…

自然语言处理——自注意力机制

一、文字表示方法 在自然语言处理中&#xff0c;如何用数据表示文字是基础问题。独热编码&#xff08;One-hot Encoding &#xff09;是一种简单的方法&#xff0c;例如对于 “我”“你”“他”“猫”“狗” 等字&#xff0c;会将其编码为如 “我 [1 0 0 0 0 ……]”“你 [0 …

嵌入式硬件篇---PID控制

文章目录 前言第一部分&#xff1a;连续PID1.比例&#xff08;Proportional&#xff0c;P&#xff09;控制2.积分&#xff08;Integral&#xff0c;I&#xff09;控制3.微分&#xff08;Derivative&#xff0c;D&#xff09;控制4.PID的工作原理5..实质6.分析7.各种PID控制器P控…

学成在线_内容管理模块_创建模块工程

学成在线模块工程 1.各个微服务依赖基础工程2.每个微服务都是一个前后端分离的项目3.xuecheng-plus-content&#xff1a;内容管理模块工程xuecheng-plus-content-modelxuecheng-plus-content-servicexuecheng-plus-content-api 1.各个微服务依赖基础工程 2.每个微服务都是一个前…

免费送源码:Java+ssm+MySQL Springboot卫生院儿童预防接种系统 计算机毕业设计原创定制

摘 要 儿童预防接种工作实行网络信息化管理&#xff0c;是我国预防规划工作发展的需要。接种信息实行网络信息化不仅是预防接种工作步入了一个新的台阶&#xff0c;更重要的是解决了多年接种疫苗过程种&#xff0c;预防接种剂次不清&#xff0c;难以全程有效接种的问题&#x…

OSPF的LSA的学习研究

OSPF常见1、2、3、4、5、7类LSA的研究 1、拓扑如图&#xff0c;按照地址表配置&#xff0c;激活OSPF划分相关区域并宣告相关网段 2、1类LSA&#xff0c;每台运行了OSPF的路由器都会产生&#xff0c;描述了路由器的直连接口状况和cost 可以看到R1产生了一条router lsa&#xff0…

JAVA:MyBatis 缓存机制详解的技术指南

1、简述 MyBatis是Java开发中常用的持久层框架之一&#xff0c;通过面向对象的方式操作数据库。为了提高系统性能&#xff0c;MyBatis提供了两级缓存机制&#xff1a;一级缓存&#xff08;本地缓存&#xff09;和二级缓存&#xff08;全局缓存&#xff09;。本文将详细讲解MyB…

前后端分离的Java快速开发平台

采用SpringBoot3.x、Shiro、MyBatis-Plus、Vue3、TypeScript、Element Plus、Vue Router、Pinia、Axios、Vite框架&#xff0c;开发的一套权限系统&#xff0c;极低门槛&#xff0c;拿来即用。设计之初&#xff0c;就非常注重安全性&#xff0c;为企业系统保驾护航&#xff0c;…

数据结构:栈和队列详解(上)

一.栈 1.概念与结构&#xff1a; 栈&#xff1a;一种特殊的线性表&#xff0c;只允许在顺序表的一段插入和删除数据&#xff0c;进行插入和删除的一端叫做栈顶&#xff0c;另外一端则叫做栈底&#xff0c;而我们将在栈顶插入数据叫做压栈&#xff08;入栈或进栈&#xff09;&a…

初识go语言之指针用法

一、环境准备 安装go语言编译环境&#xff0c;官网地址&#xff1a;https://go.dev/dl/ 或者 https://golang.google.cn/dl/ 点击下载按提示安装即可 vscode 安装go语言扩展 测试 package mainimport "fmt"func main() {fmt.Println("Hello, World!") …

python(25) : 含有大模型生成的公式的文本渲染成图片并生成word文档(支持flask接口调用)

公式样例 渲染前 \[\sqrt{1904.615384} \approx 43.64\] 渲染后 安装依赖 pip install matplotlib -i https://mirrors.aliyun.com/pypi/simple/ requestspip install sympy -i https://mirrors.aliyun.com/pypi/simple/ requestspip install python-docx -i https://mirro…

国产文本编辑器EverEdit - 恢复最近的选区

1 恢复最近的选区 1.1 应用场景 如果用户选择了一些文本&#xff0c;特别是多选区选择&#xff0c;在选择的过程中出现失误&#xff0c;导致选区丢失&#xff0c;一般的做法是骂骂咧咧再选一次&#xff0c;使用EverEdit就没有这个烦恼&#xff0c;EverEdit内置了恢复最近的选区…