浅谈PostgreSQL中的并发实现

2022-08-17 12:36:27 浏览数 (1)

  • 一般实现数据库的并发会采用三种方式,分别是多版本并发控制(MVCC),严格两阶段锁(S2PL),乐观并发控制(OCC).在MVCC中,每个更新操作都会创建新的一个数据版本,并保留旧版本。当事务读取数据对象时候,系统会根据一定的策略选择一个数据版本读取,这样读写都不会互相干扰。基于S2PL的数据库系统在写操作发生时会阻塞相应对象上的读操作,因为写入者获得了操作对象的互斥锁。PostgreSQL采用了基于MVCC的变体,叫做快照隔离级别(SI)
  • 目前Oracle数据使用undo来实现快照隔离级别。当新数据写入对象时,旧版本对象数据先把写入到undo回滚段中,随后用新对象数据覆盖数据区域。MySQL会记录 最新记录和历史记录的联系,每次访问根据最新记录和历史记录的版本来确定哪条记录是对自己可见。PostgreSQL使用相对比较简单的方式,将新数据对象直接插入到表的页中,读取对象时候,根据PostgreSQL可见性检查规则选择不同的版本,这样做会导致PostgreSQL新旧数据在一起,如果vacuum做的不及时,会导致表的空间无法被回收,其次也会造成表的膨胀。目前社区在研发zheap存储引擎,来替代现在默认的heap存储引擎,zheap引擎采用的思想和oracle一致,采用回滚段的方法。
    • PostgreSQL中每个普通的heap表中每行数据也存储一些信息,在MVCC实现中根据规则来选择事务应该读取哪一行数据。我们接下来了解下PG中每行数据的header,它是用HeapTupleHeaderData结构体来表示。PG中每个表创建后都会有{oid}{oid}_fsm{oid}_vm这三个文件,{oid}是存储行数据的数据文件;{oid}_fsm是空闲空间映射表文件 ,fsm文件中的page是采用tree组织,每个数据page在fsm中中占用一个字节,当往表中插入数据时候,PG使用这个表的fsm文件找到新的数据应该插入个page中,这些fsm文件一般都会加载在PG的共享内存中。可以通过pg_freespacemap来查看表或者索引的空闲空间。PG中的vacuum代价非常大,PG引入了{oid}vm文件,每个表都会有vm文件来表达每个数据page的可见性,page可见性可以判断page中是否有dead tuples.vacuum在处理时候掉过不包含dead tuples,降低vacuum扫描过程的开销。
    • postgre默认heap表格式
    • postgre zheap表格式
    • heap vs zheap
    • Postgres事务采用了事务ID(txid)作为唯一标识,这是一个4个字节的整数,最大的是txid为42亿。其中PostgreSQL中保留了txid=0代表无效的txid;txid=1代表初始化启动txid(数据库集群初始化过程中出现);txid=2代表冻结的txid.PostgreSQL中txid视为一个环,划分为2个部分,前21个txid属于过去;后21个事务id属于未来。
  • 插入数据时候,会在每行数据的header设置t_xmin=当前事务,t_xmax=0,t_cid=0,t_ctid=(0,1),插入过程中t_xmax永久设置为0.当一行数据被删除时候,PostgreSQL不会立即删除,而是打上删除的标记,后续由vacuum进程回收删除记录的空间。行数据删除会在数据行的header中设置t_xmin={开始的事务id},t_xmax={删除数据整个事务的id};PostgreSQL中的更新不是采用原地更新的模式,而是删除旧数据行,插入新的数据行模式。
    • 行数据呈现
    • 行数据插入
    • 行数据删除
    • 行数据更新
  • PostgreSQL中表中的每条记录都会记录版本信息,版本信息主要包括插入记录的事务ID(cmin)、删除记录事务ID(cmax).记录的Common Id(cid)和一些Hints信息。PG的行记录可见性函数根据这些信息、clog、快照来判断记录的可见性。PG数据采用页面方式进行存储,页面从前往后保存记录的位置信息,从后往前行数据,中间则是页面的空闲空间。
代码语言:javascript复制
typedef  struct  HeapTupleFields 
 { 
 	//数据时候的事务ID
  	TransactionId t_xmin ; 		
	// 数据删除时间的事务id
  	TransactionId t_xmax ; 		
 
 	union 
 	{ 
		// 插入或删除命令id
  		CommandId t_cid ; 	
		// 表格最做fullvacuum的事务ID
  		TransactionId t_xvac ; 	
 	} 			t_field3 ; 
 } HeapTupleFields ; 
 
 typedef  struct  DatumTupleFields 
 { 
 	// 修改首部的长度
 	int32 datum_len_ ; 		
	// -1 或者记录的唯一标识
 	int32 datum_typmod ; 	/* -1,或记录类型的标识符 */ 
 	// 符合类型的oid或记录id
 	Oid datum_typeid ; 	/* 复合类型 OID 或 RECORDOID */ 

 } DatumTupleFields ; 
 
 结构 HeapTupleHeaderData 
 { 
 	union 
 	{
  		HeapTupleFields t_heap ; 
 		DatumTupleFields t_datum ; 
 	} 			t_choice ; 
 	
	// 元组未删除,保存当前记录的位置;如果删除,保存新记录的位置
 	ItemPointerData t_ctid ; 		
 
 	// 属性和标记位
 # define  FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK2  2
  	uint16 t_infomask2 ; 	
 	// 标记位
 #define FIELDNO_HEAPTUPLEHEADERDATA_INFOMASK  3 
  	uint16 t_infomask ;		
 	// header的位图填充
 #define FIELDNO_HEAPTUPLEHEADERDATA_HOFF  4 
  	uint8 t_hoff ; 			
	// NULL的位图填充
 #define FIELDNO_HEAPTUPLEHEADERDATA_BITS  5 
  	bits8 t_bits [ FLEXIBLE_ARRAY_MEMBER ] ; 	/* NULL 位图 */ 
 
 } ;

  • 新数据被插入到表中,针对元组做一次查询会在记录的infomask字段设置HEAP_XMIN_COMMITTED这个标记可以快速判断元组可见性,每次对元组查询时候,如果发现事务已经提交并设置了HEAP_XMIN_COMMITTED,就不需要去clog中查询事务的状态了。
代码语言:javascript复制
$ /usr/local/postgres/bin/psql  -h 127.0.0.1 -d postgres
postgres=#  create extension pageinspect;
CREATE EXTENSION
postgres=# create table test1(a1 int,a2 int);
CREATE TABLE
postgres=# begin;
BEGIN
postgres=*# insert into test1 values(1,1);
INSERT 0 1
postgres=*# select txid_current();
 txid_current 
--------------
          740
(1 row)

postgres=*# d
          List of relations
 Schema | Name  | Type  |   Owner    
-------- ------- ------- ------------
 public | test1 | table | perrynzhou
(1 row)

postgres=*# select t_xmin, t_xmax,t_field3,t_infomask, t_infomask2,t_data  from heap_page_items(get_raw_page('public.test1', 0));
 t_xmin | t_xmax | t_field3 | t_infomask | t_infomask2 |       t_data       
-------- -------- ---------- ------------ ------------- --------------------
    740 |      0 |        0 |       2048 |           2 | x0100000001000000
(1 row)

postgres=*# commit;
COMMIT
postgres=# select t_xmin, t_xmax,t_field3,t_infomask, t_infomask2,t_data  from heap_page_items(get_raw_page('public.test1', 0));
 t_xmin | t_xmax | t_field3 | t_infomask | t_infomask2 |       t_data       
-------- -------- ---------- ------------ ------------- --------------------
    740 |      0 |        0 |       2048 |           2 | x0100000001000000
(1 row)

// 执行一次查询发现t_infomask 由2048更新为了2304
postgres=# select * from test1;
 a1 | a2 
---- ----
  1 |  1
(1 row)

postgres=# select t_xmin, t_xmax,t_field3,t_infomask, t_infomask2,t_data  from heap_page_items(get_raw_page('public.test1', 0));
 t_xmin | t_xmax | t_field3 | t_infomask | t_infomask2 |       t_data       
-------- -------- ---------- ------------ ------------- --------------------
    740 |      0 |        0 |       2304 |           2 | x0100000001000000
(1 row)

postgres=#
  • PostgreSQL中快照是记录数据库当前时刻的状态的重要数据结构,快照保存当前活跃事务的最小事务ID,最大事务ID,当前的活跃事务列表、当前事务的CommandID等,快照可以分为多种类型,具体定义在SnapshotType中每一种类型都对应一种判断记录可见性的方法。如下是快照数据结构的解释。快照中xmin记录当前所有活跃事务中最小事务ID;xmax则是记录当前已经提交的最大事务ID,xip记录xminxmax之间的事务活跃事务。这里隐含了2个意思,第一是小于xmin一定是结束的事务;第二是大于xmax是一定是活跃事务。PostgreSQL中基于MVCC多版本可见性需要结合快照来实现,一般会判断元组的xmin状态,如果xmin未提交,所在事务是当前事务,元组可见;如果不是,需要结合快照和clog来决定事务的状态。如果xmin已提交,需要判断元组的xmax状态才能知道元组是否可见,
代码语言:javascript复制
// 快照类型的定义
typedef enum SnapshotType
{
	
	// 本事务插入,元组可见;符合事务快照元组可见
	SNAPSHOT_MVCC = 0,

	// 事务提交或者当前事务运行中,则元组是可见
	SNAPSHOT_SELF,

	// 没有任何限制,元组都可见
	SNAPSHOT_ANY,

	// 用于检查toast表的可见性
	SNAPSHOT_TOAST,

	// 事务提交或者终止,可见性和SNAPSHOT_SELF保持一致;如果是进行中的写入事务,则它的可见性和SNAPSHOT_SELF不一致,此时会收集元组版本信息保存到快照中
	SNAPSHOT_DIRTY,
	
	// 用于逻辑复制中逻辑编解码的可见性判断
	SNAPSHOT_HISTORIC_MVCC,

	//  判断元组是否对某些事务可见
	SNAPSHOT_NON_VACUUMABLE
} SnapshotType;
代码语言:javascript复制
// PG中事务的快照的定义
typedef struct SnapshotData
{
	// 事务快照类型
	SnapshotType snapshot_type; 

	// 当前活跃事务中最小的事务ID
	TransactionId xmin;			
	// 当前活跃事务中已提交的最大事务ID
	TransactionId xmax;			

	// xmin和xmax之间的事务
	TransactionId *xip;
	uint32		xcnt;			/* # of xact ids in xip[] */

	// 子事务列表
	TransactionId *subxip;
	int32		subxcnt;		/* # of xact ids in subxip[] */
	bool		suboverflowed;	/* has the subxip array overflowed? */

	bool		takenDuringRecovery;	/* recovery-shaped snapshot? */
	bool		copied;			/* false if it's a static snapshot */

	CommandId	curcid;			/* in my xact, CID < curcid are visible */

	/*
	 * An extra return value for HeapTupleSatisfiesDirty, not used in MVCC
	 * snapshots.
	 */
	uint32		speculativeToken;

	/*
	 * For SNAPSHOT_NON_VACUUMABLE (and hopefully more in the future) this is
	 * used to determine whether row could be vacuumed.
	 */
	struct GlobalVisState *vistest;

	/*
	 * Book-keeping information, used by the snapshot manager
	 */
	uint32		active_count;	/* refcount on ActiveSnapshot stack */
	uint32		regd_count;		/* refcount on RegisteredSnapshots */
	pairingheap_node ph_node;	/* link in the RegisteredSnapshots heap */

	// 快照的时间戳
	TimestampTz whenTaken;		/* timestamp when snapshot was taken */
	// redo的LSN
	XLogRecPtr	lsn;			/* position in the WAL stream when taken */

	/*
	 * The transaction completion count at the time GetSnapshotData() built
	 * this snapshot. Allows to avoid re-computing static snapshots when no
	 * transactions completed since the last GetSnapshotData().
	 */
	uint64		snapXactCompletionCount;
} SnapshotData;

0 人点赞