pg事务:事务ID

news/2024/6/18 19:07:34 标签: 数据库, postgresql, 事务, 事务id

事务ID

pg中每个事务都会分配事务ID,事务ID分为虚拟事务ID和持久化事务ID(transactionID)。pg的事务ID非常重要,是理解事务、数据可见性、事务ID回卷等等的重要知识点。

虚拟事务ID

只读事务不会分配事务ID,事务ID是很宝贵的资源,比如简单的select语句不会申请事务ID。本身不需要把事务ID持久化到磁盘,但是为了在共享锁等情况下对事务进行标识,需要一种非持久化的事务ID,这个就是虚拟事务ID(vxid)
VXID由两部分组成:backendID 和backend本地计数器。
源码:src/include/storage/lock.h

 typedef struct
{
BackendId    backendId;        /* backendId from PGPROC */
LocalTransactionId localTransactionId;    /* lxid from PGPROC */
} VirtualTransactionId;

(PGPROC是一种存储进程信息的结构体,后面会介绍)

pg_locks可以看到vxid,查询pg_locks本身就是一个sql,会产生vxid

lzldb=# begin;
BEGIN
lzldb=*# select locktype,virtualxid,virtualtransaction,mode from pg_locks;
 locktype | virtualxid | virtualtransaction |   mode    
------------+------------+--------------------+-----------------
 relation		|     		 | 4/16        | AccessShareLock
 virtualxid | 4/16   	 | 4/16        | ExclusiveLock
(2 rows)
 
lzldb=*# savepoint p1;
SAVEPOINT
lzldb=*# select locktype,virtualxid,virtualtransaction,mode from pg_locks;
 locktype 	| virtualxid | virtualtransaction |   mode    
------------+------------+--------------------+-----------------
 relation  	|    		  | 4/16        | AccessShareLock
 virtualxid | 4/16    | 4/16        | ExclusiveLock
lzldb=*# rollback;
ROLLBACK
lzldb=# select locktype,virtualxid,virtualtransaction,mode from pg_locks;
 locktype 	| virtualxid | virtualtransaction |   mode    
------------+------------+--------------------+-----------------
 relation  	|     	  | 4/17        | AccessShareLock
 virtualxid | 4/17    | 4/17        | ExclusiveLock

此时\q退出会话再立即登录,计数仍然继续4/19

另开一个窗口,backendID+1

lzldb=# select locktype,virtualxid,virtualtransaction,mode from pg_locks;
 locktype   | virtualxid | virtualtransaction |   mode    
------------+------------+--------------------+-----------------
 relation   |        | 5/3        | AccessShareLock
 virtualxid | 5/3    | 5/3        | ExclusiveLock

从以上测试能看出:

  • VXID的backendID不是真正的进程号PID,也只是一个简单的递增的编号
  • VXID的bakendID和命令编号都是递增的
  • 事务没有自己的VXID,他们用父事务的VXID
  • VXID也有回卷,不过问题不严重,因为没有持久化,实例重启后VXID从头开始计数

永久事务ID

当发生数据变化的事务开始时,事务管理器会为事务分配一个唯一标识TransactionId。txid是32位无符号整型,总共可以存储
232=4294967296,42亿多个事务。32位无符号整型能存储的数据范围为:0~2^32-1

3个特殊的事务ID

src/include/access/transam.h中宏定义几个事务ID

#define InvalidTransactionId        ((TransactionId) 0)
#define BootstrapTransactionId        ((TransactionId) 1)
#define FrozenTransactionId            ((TransactionId) 2)
#define FirstNormalTransactionId    ((TransactionId) 3)
#define MaxTransactionId            ((TransactionId) 0xFFFFFFFF)
  • 0 代表无效TransactionID

  • 1 代表启动事务ID,只在初始化数据库时才会使用。比所有正常事务都旧

  • 2 代表冻结事务ID。比所有正常事务都旧

#define TransactionIdIsNormal(xid)        ((xid) >= FirstNormalTransactionId)

事务ID>=3时是正常事务id。

最大事务ID MaxTransactionId是0xFFFFFFFF=4294967295=2^32-1
所以正常事务id能分配到的范围为:3~2^32-1

事务ID分配

做几个小实验来看下事务id是怎么分配的。其中用到两个返回事务id的function

pg_current_xact_id ():返回当前事务id,如果当前事务还没有分配事务id,那么分配一个事务id。pg12及以前用txid_current ()

pg_current_xact_id_if_assigned () :返回当前事务id,如果当前事务还没有分配事务id,那么返回NULL。pg12及以前用txid_current_if_assigned ()

事务id顺序分配

lzldb=# select pg_current_xact_id();
 pg_current_xact_id 
--------------------
        612
lzldb=# select pg_current_xact_id();
 pg_current_xact_id 
--------------------
        613
lzldb=# select pg_current_xact_id();
 pg_current_xact_id 
--------------------
        614

begin不会立即分配事务id

lzldb=# begin; --显示开启事务
BEGIN
lzldb=*# select pg_current_xact_id_if_assigned () ; --begin不会立即分配事务id
 pg_current_xact_id_if_assigned 
--------------------------------           

(1 row)
lzldb=*# select * from lzl1; --begin后立即查询
 a 
---

(0 rows)
lzldb=*# select pg_current_xact_id_if_assigned () ; --查询不会分配事务id
 pg_current_xact_id_if_assigned 
--------------------------------               

(1 row)
lzldb=*# insert into lzl1 values(1); --插入数据,做一个数据变更
INSERT 0 1
lzldb=*# select pg_current_xact_id_if_assigned () ; --begin后的第一个非查询语句分配事务id
 pg_current_xact_id_if_assigned 
--------------------------------
             611
lzldb=*# commit;
COMMIT
lzldb=# select xmin, pg_current_xact_id_if_assigned () from lzl1; --insert事务写入到xmin
 xmin | pg_current_xact_id_if_assigned 
------+--------------------------------
 611 

系统表中的有些记录,在数据库初始化时分配了BootstrapTransactionId=1

postgres=# select xmin,count(*) from pg_class where xmin=1 group by xmin;
 xmin | count 
------+-------
  1 |  184

以上实验得出以下结论

  • 数据库初始化时分配特殊事务id 1,可以在系统表中看到
  • 事务id是递增分配的
  • begin不会立即分配事务id,begin后的第一个非查询语句分配事务id
  • 当一个事务插入了一tuple后,会将事务的txid写入这个tuple的xmin。

事务ID对比

pg事务新旧通过事务ID来对比。在src/backend/access/transam/transam.c定义了4种事务ID对比函数,分别是<,<=,>,>=

bool TransactionIdPrecedes()
bool TransactionIdPrecedesOrEquals()
bool TransactionIdFollows()
bool TransactionIdFollowsOrEquals()

内容都差不多,拿TransactionIdPrecedes()代表来看

bool
TransactionIdPrecedes(TransactionId id1, TransactionId id2)
{
/*
 * If either ID is a permanent XID then we can just do unsigned
 * comparison. If both are normal, do a modulo-2^32 comparison.
 */
int32        diff;
 
if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2))
return (id1 < id2);
 
diff = (int32) (id1 - id2);
return (diff < 0);
}

该段源码的知识点

  • TransactionIdIsNormal()是已经在header中宏定义了的判断正常事务的函数,FirstNormalTransactionId是常量3。也就是说正常事务ID是>=3的
#define TransactionIdIsNormal(xid)        ((xid) >= FirstNormalTransactionId)
  • int32是有符号的整型,第一位0表示正数,第一位-1表示负数,取值范围-2*31~2^31-1
  • 数值溢出,意思数值超过数据存储范围,比如2^31对于int32是刚好数值溢出的。为了保证数据在范围内,对数值加减模长

对比事务ID源码分为2段理解

非正常事务ID对比:

if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2))
return (id1 < id2);

当id1=2,id2=100时,return(2<100),precede为真,正常事务较新

当id1=100,id2=2时,return (100<2),precede为假,正常事务较新

所以,txid为1、2时比正常事务要旧

正常事务ID对比:

diff = (int32) (id1 - id2);
return (diff < 0);

id1-id2可以是负数,所以diff不能是unsign int,转换有符号型的int。然后最关键的来了

由于int32是-2*31~2^31-1,

当id1=231+99,id2=100,id1-id2=2^31-1。这没问题,int32刚好可以存放 =>大txid较新

当id1=231+100,id2=100,id1-id2=231。这有问题,刚好超出int32存储范围,此时的值为231-232=-2^31<0 =>小txid较新

当id1=100,id2=231+100,id1-id2=-2^31。这没问题,int32刚好可以存放 =>大txid较新

当id1=100,id2=231+101,id1-id2=-231-1。这有问题,刚好超出int32存储范围,此时的值为-231-1+232=2^31-1>0 =>小txid较新

以上分析可以看出,当发生数值溢出时,txid大的事务看不见更小的txid事务,本身数值溢出是一个异常事件,这无可厚非。为了解决这个问题,pg将40亿事务id分成两半,一半事务是可见的,另一半事务是不可见的。

比如,txid 100的事务,它过去的20亿事务是它可见的,它未来的20亿事务是它不可见的。所以,在pg数据库中最大事务和最小事务数据库年龄)之差最大为|-231|=2^31,20亿左右
在这里插入图片描述

事务ID回卷

什么是事务ID回卷

理解事务ID回卷本身不难,但是刚开始了解回卷时,发现了事务ID回卷有两种定义:

pg官方定义:

由于事务ID的大小有限(32位),一个长时间运行的集群(超过40亿个事务)将遭受事务ID的缠绕:XID计数器回卷到零,突然之间,过去的事务似乎在未来,这意味着它们变得不可见。简而言之,就是灾难性的数据丢失。(事实上,数据仍然存在,但如果你无法获得数据。)

interdb解释:

元组中t_xmin记录了当前元组的最小事务,如果这个元组一直没有变化,这个t_xmin不会变。假如一个元组tuple_1由txid=100事务创建,它的t_xmin=100。如果数据库事务向前推进了231个,到了231+100,此时tuple_1是可见的。此时再启动一个事务,txid推进至2^31+101,txid=100的事务属于未来,tuple_1是不可见的,此时便发生了严重的数据丢失问题,这就是事务回卷。

是的,对事物回卷的定义,官方文档与有些经典文章不太一样,不过他俩的行为是不冲突的。但是如果重新思考“回卷”(wraparound)的含义。其实它俩都发生了回卷。

不过回卷形势还是有些区别:前者是事务ID(232)全部用完,回卷到0重新计数;后者是把事务ID分成两半,“最老的事务ID“与”最新的事务ID“只差大于2^31。两者都会发生数据可见性的异常。

42亿的事务ID用完,这个问题比较严重,是个炸弹,目前没有比较好的办法解决。

21亿的事务ID差,有解决办法,也就是事务冻结机制。

42亿事务到底要跑多久

42亿个事务看上去是挺多,但是仍然可能用完。

比如一个tps为100的pg库(不算select语句,因为单纯的select不会分配事务id),1天会使用8640000个事务,只需要历时4294967296/8640000≈497天就可以把42亿个事务id耗尽发生事务回卷;如果每秒1000个事务,不到两个月时间就可以把42亿事务用完。所以事务回卷问题是pg数据库中必须要关注的。

事务id冻结

为了解决事务回卷引起严重的数据丢失问题,pg引入事务冻结的概念。

之前介绍了冻结事务id FrozenTransactionIdn=2,并且比所有正常事务都旧。也就是说txid=2对于所有正常事务(txid>=3)都是可见的。当t_xmin比当前txid-vacuum_freeze_min_age(默认5000w)更旧时,该元组将重写为冻结事务id 2。在9.4及以后的版本,用t_infomask中的xmin_frozen来表示冻结元组,而不是重写t_xmin为2。
在这里插入图片描述

事务ID回卷问题有许多优化方案,不过都绕不过事务冻结处理回卷问题,而事务冻结这个操作,会有非常大的IO消耗以及cpu消耗(所有表的所有行读一遍,重置标记)无从避免回卷,甚至数据库会拒绝所有操作,直至冻结操作结束,这也是俗称的“冻结炸弹”。业务系统越繁忙,事务越多的库,越容易触发。

64位的事务id

事务id耗尽回卷问题终极解决方案就是使用64位的事务ID。32位事务id有232个,64位事务id有2^64个。即使每秒10000个事务,每天864000000个事务,也要5849万年才能把事务id消耗光。如果拥有64位事务id,事务id几乎是取之不尽用之不竭,就不需要考虑事务id回卷问题,也不需要事务冻结操作,也就没有“冻结炸弹”的概念…

为什么还没有实现64位事务id?

xmin,xmax可以简单理解为插入事务和删除事务事务id,保存在每个元组的header中(元组结构章节将介绍该部分内容),而header空间是有限的。32位事务id有8个字节,64为事务有16个字节,存储xmin、xmax两个事务id将需要额外的16字节空间,目前header无法保存这么大的数据。社区讨论过两种实现方案

1.扩展header。直接将64位事务id存储进去

2.header大小不变。内存中保留64为事务id,增加epoch概念来位移转换两者的关系。

第一种方案已基本放弃,对比其他系统,pg的tuple header已经够大了。

第二种方案还在路上。

其实源码中已经有64位FullTransactionId的定义(pg12及以后)

/*
 *一个64位值,包含一个epoch和一个TransactionId。它被封装在一个结构中,以防止隐式转换为TransactionId。
 *并非所有值都表示有效的正常XID。
 */
typedef struct FullTransactionId
{
uint64        value;
} FullTransactionId;

不过这完全实现没有那么容易,参考社区邮件

https://www.postgresql.org/message-id/CAEYLb_UfC+HZ4RAP7XuoFZr+2_ktQmS9xqcQgE-rNf5UCqEt5A@mail.gmail.com

https://www.postgresql.org/message-id/flat/DA1E65A4-7C5A-461D-B211-2AD5F9A6F2FD%40gmail.com

2014年社区就提出了64为xid永久解决freeze问题,并于2017年开始讨论如何实践64位事务id,不过经过了多个pg版本也只是只闻其声不见其人。由于数据库对于数据的敏感性和重要性,而事务id的改造对于数据库来说牵扯的东西太多,稍微不注意可能导致数据丢失或者触发未知bug,64位事务id改造的问题pg走的很谨慎。不过社区还是在考虑这个问题,期待有一天在某个pg版本中事务id回卷问题彻底解决。

事务id参考

《Postgresql指南 内幕探索》
https://www.interdb.jp/pg/pgsql05.html
https://www.interdb.jp/pg/pgsql06.html
https://www.modb.pro/db/427012
https://www.postgresql.org/docs/13/routine-vacuuming.html
https://blog.csdn.net/weixin_30916255/article/details/112365965
https://wiki.postgresql.org/wiki/FullTransactionId
http://mysql.taobao.org/monthly/2019/08/01/
https://github.com/digoal/blog/blob/master/201605/20160520_01.md


http://www.niftyadmin.cn/n/342115.html

相关文章

PID单环控制(位置环)

今天我们来聊一聊pid如何控制轮子转动位置 前期准备调试过程 前期准备 需要准备的几个条件&#xff1a; 1.获取实时编码器的计数值 2.写好pid控制算法的函数 3.设定好时间多久执行一次pid计算&#xff0c;并设置限幅输出。 4.多久执行一次pid输出 接下来我们看看这几个部分的…

信息安全工程复习

目录 第二章 从口令系统说起 2.1 身份鉴别常见手段及例子 2.2 多因子认证 2.3 计时攻击 2.4 口令机制 2.5 假托和钓鱼 第三章 安全协议 3.1 认证协议 3.2 安全协议攻击 3.3 密钥分配协议 3.4 课后作业 第四章 访问控制 4.1 概念 4.2 控制访问三要素 4.3 控制访问…

[RapidOCRWeb] 桌面版使用教程

引言 说明&#xff1a;桌面版指的是可以直接解压&#xff0c;双击即可运行的版本。通俗来说&#xff0c;对rapidocr_web做了打包&#xff0c;将相关依赖全部放到一个zip包中&#xff0c;不需要本地有额外的环境&#xff0c;降低使用门槛。下面会以Windows版为例&#xff0c;作…

jface

JFace 是建立在 SWT 之上的 UI 部件&#xff0c;它是 SWT 的扩展并能和SWT交互。 ApplicationWindow和Action org.eclipse.jface.window.ApplicationWindow; JFace为了简化窗口的设计特别设计了类&#xff0c;比如ApplicationWindow这一个类&#xff0c;它里面包含了六个默认…

关于蒙特卡罗方法及其在信号处理中的应用

说明 最近想探讨一下毫米波雷达测量准确度及其改善的问题&#xff0c;这个话题下可供讨论的问题有很多&#xff0c;蒙特卡罗方法(或者说基于蒙特卡罗方法对测量准确度以及精度的评估)是其中之一&#xff0c;该方法是一个十分有效的工具&#xff0c;在科研(发paper)上也是不可少…

【java】Java应用程序性能优化(JVM调优)

文章目录 前言1. JVM调优概述2. 内存管理调优2.1 调整堆内存大小2.2 选择合适的垃圾回收器2.3 调整垃圾回收器参数 3. 垃圾回收调优3.1 监控和分析垃圾回收情况GC日志输出使用VisualVM进行监控使用JMX进行远程监控 3.2 调整垃圾回收参数 4. 线程管理调优4.1 调整线程池大小4.2 …

2023年值得关注的低代码平台推荐

低代码平台在数字化转型的浪潮中受到越来越多企业的青睐&#xff0c;因为它们提供了一种更容易、更快的方式来开发网络和移动应用程序。低代码平台只需要最少的编码知识&#xff0c;使公司能够在很短的时间内开发出定制的应用程序&#xff0c;而这只是使用传统的搭建手段所需时…

Windows 默认字体和 字体链接

在Windows系统中&#xff0c;不同版本的默认字体可能会有所不同。例如&#xff0c;Windows 10的默认字体是Segoe UI&#xff0c;而在之前的版本如Windows 7中&#xff0c;默认字体是Tahoma。这些字体包括了大多数常用的符号和字符。 如果需要的符号不在默认字体中&#xff0c;…