postgreSQL源码分析——存储管理——外存管理(7)

2021SC@SDUSC

概述

上次分析了postgreSQL的TOAST机制,而TOAST机制属于postgreSQL对于变长变量(大数据)的自动处理机制,并不能处理blob、clob这种用户主动使用的大对象。处理这种大对象,就需要用到postgreSQL处理大数据的另一种机制——大对象存储机制。
根据官方文档,大对象存储机制支持以下类型:

  1. BLOB,二进制大对象,用于保存图片、视频、混合媒体等。
  2. CLOB,字符大对象,存储大的单字节字符集数据,如文档等。
  3. DBCLOB,双字节字符大对象,用于存储大的双字节字符集数据,如变长双字节字符图形字符串。

源码分析

大对象的存储

所有的大对象都存储在一个叫pg_largeobject的系统表中,系统表的结构如下:

属性名属性类型描述
loidoid包含此页的大对象的标识符
pagenoint5此页在它所属大对象中的页号(从0开始计)
databytea实际存储在大对象中的数据。它从不会超过LOBLKSIZE字节,也可能更少。

目录pg_largeobject保存构成“大对象”的数据。一个大对象在被创建时会被分配一个OID。每个大对象被分解成段或“页”,即每个元组就是一页,以便小到可以被方便地作为行存储在pg_largeobject中。每页中的数据量被定义为LOBLKSIZE(目前是BLCKSZ/4或是2 kB)。

元组大小设置为2KB的原因如下:

  1. 更新元组时不会占用过多的空间。
  2. TOAST机制对于元组的限制也是2KB,因此可以触发TOAST机制来缩小元组的大小。

大对象的数据结构

源码位于src/include/storage/large_object.h,该数据结构用于存储当前打开的大对象,同时也是下面大对象管理所操作的对象。首先它的名称为LargeObjectDesc,全名就是large object descriptor,即大对象描述符,下面来看一下它的具体结构:

typedef struct LargeObjectDesc
{
	Oid			id;				//大对象的OID
	Snapshot	snapshot;		//大对象的快照,用于大对象读写时检查数据可见性。
	SubTransactionId subid;		//打开该大对象的子事务的ID
	uint64		offset;			//类似于c语言打开文件时的seek,通过偏移量记录当前读写指针位置
	int			flags;			//标志位,细节见下

//标志位的定义
#define IFS_RDLOCK		(1 << 0)	//读锁,说明打开这个大对象是只读的
#define IFS_WRLOCK		(1 << 1)	//写锁,说明打开这个大对象需要进行读写

} LargeObjectDesc;

快照技术

PostgreSQL为开发者提供了一组丰富的工具来管理对数据的并发访问。在内部,数据一致性通过使用一种多版本模型(多版本并发控制,MVCC)来维护。这就意味着每个 SQL 语句看到的都只是一小段时间之前的数据快照(一个数据库版本),而不管底层数据的当前状态。这样可以保护语句不会看到可能由其他在相同数据行上执行更新的并发事务造成的不一致数据,为每一个数据库会话提供事务隔离。MVCC避免了传统的数据库系统的锁定方法,将锁争夺最小化来允许多用户环境中的合理性能。

大对象的管理

源码文件位于src/backend/storage/large_object/inv_api.c,包含了用户级别的大对象管理的相关方法。
这里的inv是inversion的缩写,目的应该是为了实现面向对象中的控制反转技术

创建大对象

创建大对象通过inv_create函数实现,传入参数为:

  • lobjId 表示用于创建的新的大对象的Oid,或者为InvalidOid去自动生成一个Oid

创建大对象的整个流程为:
调用LargeObjectCreate函数,创建一个空的大对象(通过在pg_largeobject表中插入一个元组),这个函数会对输入参数进行判断,如果为InvalidOid(未指定Oid),会自动分配一个OID;如果已经指定了Oid,则会在pg_largeobject系统表中检查该Oid是否存在,如果已经存在则报错返回。

Oid
inv_create(Oid lobjId){
	Oid			lobjId_new;

	lobjId_new = LargeObjectCreate(lobjId);//借助LargeObjectCreate函数来创建一个数据为空的大对象
	//这个函数的详细解析在下面,会对入参OID进行判断,来选择自动生成OID还是根据已有的OID来建大对象

	recordDependencyOnOwner(LargeObjectRelationId,
							lobjId_new, GetUserId());//将指定用户注册为当前大对象的所有者
	//这个函数不是分析的重点,因此源码就不贴上来了

	InvokeObjectPostCreateHook(LargeObjectRelationId, lobjId_new, 0);
	
	CommandCounterIncrement();//使得元组对以后的操作可见

	return lobjId_new;//返回新创建的大对象的OID
}

LargeObjectCreate函数
位于src/backend/catalog/pg_largeobject.c文件中。
通过入参给的OID来创建一个大对象,实际上就是向pg_largeobject_metadata中插入一个entry,而不包含数据,因此就是创建了一个空的(大小为0)的大对象。

Oid
LargeObjectCreate(Oid loid)
{
	Relation	pg_lo_meta;//pg_largeobject_metadata表
	HeapTuple	ntup;//暂时存放元组的临时变量
	Oid			loid_new;//用于创建大对象的OID
	Datum		values[Natts_pg_largeobject_metadata];//系统表存储大对象元数据值
	bool		nulls[Natts_pg_largeobject_metadata];//系统表存储大对象是否为null

	pg_lo_meta = table_open(LargeObjectMetadataRelationId,
							RowExclusiveLock);//以排他锁形式打开系统表获取元数据

	 //通过memset来修改内存,即把大对象的元数据插入
	memset(values, 0, sizeof(values));
	memset(nulls, false, sizeof(nulls));

	if (OidIsValid(loid))//如果给了要新建大对象的OID
		loid_new = loid;//直接赋值
	else//没有给新建大对象的OID
		loid_new = GetNewOidWithIndex(pg_lo_meta,
									  LargeObjectMetadataOidIndexId,
									  Anum_pg_largeobject_metadata_oid);//自动生成一个OID

	values[Anum_pg_largeobject_metadata_oid - 1] = ObjectIdGetDatum(loid_new);//设置大对象的相关信息
	values[Anum_pg_largeobject_metadata_lomowner - 1]
		= ObjectIdGetDatum(GetUserId());//设置大对象的属主
	nulls[Anum_pg_largeobject_metadata_lomacl - 1] = true;//没有存储数据,设置为null

	ntup = heap_form_tuple(RelationGetDescr(pg_lo_meta),
						   values, nulls);//获取元组

	CatalogTupleInsert(pg_lo_meta, ntup);//在新的大对象创建以后更新索引表
	//为了实现大对象的快速查找,postgreSQL为pg_largeobject创建了一个索引表,可以根据索引快速查找。

	heap_freetuple(ntup);//释放元组所占内存

	table_close(pg_lo_meta, RowExclusiveLock);//关闭打开的系统表

	return loid_new;//返回创建的大对象的OID
}

关于Datum

Datum是保存可以存储在 PostgreSQL 表中的数据的内部表示的泛型类型。
可以使用 DatumGet*宏将其转换为特定数据类型之一。

相关的细节位于src\include\postgres.h这个文件。
Datum数据类型的定义:

typedef uintptr_t Datum;
typedef unsigned long long uintptr;

打开大对象

打开大对象通过inv_open函数实现,即访问一个已经存在的大对象。
传入的参数为:

  • lobjId 大对象的OID
  • flags 标志位,表示是以只读还是读写形式打开大对象
  • mcxt 内存上下文

返回一个大对象描述符(上面已经详细分析)
基本流程:

  1. 初始化一些基本属性(大对象描述符指针、标志位、快照),利用传参初始化标志位,并判断是否异常。
  2. 根据状态位来获取快照。
  3. 根据快照以及相关信息来判断是否具有读写的权限,如果和标志位的权限不符则报错。
  4. 检查无误后,为大对象描述符分配内存并赋值。
  5. 返回大对象描述符。

细节分析如下:

LargeObjectDesc *
inv_open(Oid lobjId, int flags, MemoryContext mcxt)
{
	LargeObjectDesc *retval;//用于存储生成的大对象描述符
	Snapshot	snapshot = NULL;//快照
	int			descflags = 0;//大对象描述符标志位,初始化为0。

关于INV的标志位,如下:

#define INV_WRITE		0x00020000 //二进制为0000 0000 0000 0010 0000 0000 0000 0000
#define INV_READ		0x00040000 //二进制为0000 0000 0000 0100 0000 0000 0000 0000
//因此同时读写的二进制就是 0000 0000 0000 0110 0000 0000 0000 0000
//此时利用&运算来判断更优

下面的判断将会使用

	//利用与运算来判断标志位,之所以不用==应该是因为这里使用&运算更好(利用位判断同时允许读和写的情况)
	if (flags & INV_WRITE)//如果是写标志位
		descflags |= IFS_WRLOCK | IFS_RDLOCK; //INV_WRITE | INV_READ 和 INV_WRITE 都表示写,没有区别
		//由于descflags初始值为0,利用或运算来进行赋值为写。
		
	if (flags & INV_READ)//如果是读标志位
		descflags |= IFS_RDLOCK;
		//由于descflags初始值为0,利用或运算来进行赋值为读。
		
	if (descflags == 0)//经过上面的赋值后,如果descflags仍为0,说明传入的flags出现异常
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("invalid flags for opening a large object: %d",
						flags)));//报错终止

	if (descflags & IFS_WRLOCK)//如果状态位为写
		snapshot = NULL;//将快照置空用于获取瞬时快照
	else//状态位为读
		snapshot = GetActiveSnapshot();//获取Active快照

	if (!myLargeObjectExists(lobjId, snapshot))//如果大对象指定的快照并不存在
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_OBJECT),
				 errmsg("large object %u does not exist", lobjId)));//报错终止

	//权限检查
	if ((descflags & IFS_RDLOCK) != 0)//如果标志位为读
	{
		if (!lo_compat_privileges &&
			pg_largeobject_aclcheck_snapshot(lobjId,
											 GetUserId(),
											 ACL_SELECT,
											 snapshot) != ACLCHECK_OK)//检查SELECT权限以及指定的快照
			ereport(ERROR,
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
					 errmsg("permission denied for large object %u",
							lobjId)));//如果没有权限,则报错不允许访问
	}
	if ((descflags & IFS_WRLOCK) != 0)//如果标志位为写
	{
		if (!lo_compat_privileges &&
			pg_largeobject_aclcheck_snapshot(lobjId,
											 GetUserId(),
											 ACL_UPDATE,
											 snapshot) != ACLCHECK_OK)//检查UPDATE权限以及指定的快照
			ereport(ERROR,
					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
					 errmsg("permission denied for large object %u",
							lobjId)));//如果没有权限,则报错不允许访问
	}

	//经过前面检查没有问题后,可以创建一个大对象描述符了
	retval = (LargeObjectDesc *) MemoryContextAlloc(mcxt,
													sizeof(LargeObjectDesc));//利用内存上下文创建一个LargeObjectDesc对象
	retval->id = lobjId;//赋值OID
	retval->subid = GetCurrentSubTransactionId();//赋值子事务ID
	retval->offset = 0;//赋值读写指针偏移量
	retval->flags = descflags;//赋值标志位

	if (snapshot)//如果快照不为null
		snapshot = RegisterSnapshotOnOwner(snapshot,
										   TopTransactionResourceOwner);//根据事务资源的属主注册快照,因为这个快照在大对象关闭前必须保持活跃。
	retval->snapshot = snapshot;//赋值快照

	return retval;//返回构建的大对象描述符
}

读取大对象

通过调用inv_read函数来读取大对象的文件内容。
输入参数:

  • obj_desc 文件描述符,用于确定要读取的大对象,以及根据flag判断是否有读取权限
  • buf 缓冲区
  • nbytes 要读取的字节数

输出结果:
一个整数,为成功读取的字节数。

主要流程:
在while循环中,如果读取到元组,就根据读写指针offset,以及当前页面的位置和大小计算出n,即可以读出的字节数,然后和需要的字节数比较(nbytes-nread),即想读取的字节数减去已经读取得字节数,从而判断这次需要读出多少字节,然后读出相应得字节数,放入缓冲区中。如果页面丢失,也进行上述操作,不同的是把读入数据设置为0。直到读取得字节数满足了期望读取得字节数,则终止循环。
细节分析如下:

int
inv_read(LargeObjectDesc *obj_desc, char *buf, int nbytes)
{
	int			nread = 0;//存储读取到的字节数
	int64		n;//一次读取的字节长度
	int64		off;//页内偏移量
	int			len;//元组(页)内数据域的长度
	int32		pageno = (int32) (obj_desc->offset / LOBLKSIZE);//页(块)号,通过读写指针偏移量/块大小计算得到
	uint64		pageoff;//页偏移量,就是该页在整个文件的字节位置。
	ScanKeyData skey[2];
	SysScanDesc sd;
	HeapTuple	tuple;

	Assert(PointerIsValid(obj_desc));//判断是否为有效的大对象描述符
	Assert(buf != NULL);//如果缓冲区指针为NULL则报错

	if ((obj_desc->flags & IFS_RDLOCK) == 0)//与运算,上面有详细的讲过,这里就是判断如果没有读标志
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("permission denied for large object %u",
						obj_desc->id)))//报错,拒绝读;

	if (nbytes <= 0)//如果要读取的字节数为<=0,则直接返回未读取
		return 0;

	open_lo_relation();//调用open_lo_relation函数打开一个大对象表

	//下面三个语句应该和快照相关,不是分析重点。
	ScanKeyInit(&skey[0],
				Anum_pg_largeobject_loid,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(obj_desc->id));
	ScanKeyInit(&skey[1],
				Anum_pg_largeobject_pageno,
				BTGreaterEqualStrategyNumber, F_INT4GE,
				Int32GetDatum(pageno));
	sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
									obj_desc->snapshot, 2, skey);

	while ((tuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)//如果读取到元组
	{
		Form_pg_largeobject data;//存储大对象表数据
		bytea	   *datafield;//存储数据域的指针
		bool		pfreeit;//判断是否需要释放内存,会由getdatafield函数给出。

		if (HeapTupleHasNulls(tuple))//如果该元组为空
			elog(ERROR, "null field found in pg_largeobject");
		data = (Form_pg_largeobject) GETSTRUCT(tuple);//获取大对象
		 
		pageoff = ((uint64) data->pageno) * LOBLKSIZE;//计算页偏移量,即页号*页大小
		if (pageoff > obj_desc->offset)//如果当前页偏移量大于读写指针的偏移量,意味着页丢失。
		{
			n = pageoff - obj_desc->offset;//计算出读写指针到当前页的字节距离
			n = (n <= (nbytes - nread)) ? n : (nbytes - nread);//通过三目运算符减少代码量,这里是判断还需要读取的数据量如果小于字节距离,则将n设置为还需要读取的数据量
			MemSet(buf + nread, 0, n);//调用memset函数初始化内存,buf+nread形成起始地址,即从起始地址开始的n个字节的内存都赋值为0,因为页面丢失。
			nread += n;//已读取的字节多了n
			obj_desc->offset += n;//读写了n个字节,读写指针前进n,从而使得读写指针和页偏移量同步
		}

		if (nread < nbytes)//如果读取的字节数小于期望读取的字节数
		{
			Assert(obj_desc->offset >= pageoff);//如果读写指针仍小于页偏移量,则前面出错,报错终止。
			off = (int) (obj_desc->offset - pageoff);//这里表明off为页内偏移量,即读写指针减去页偏移量就可以表示已经读取了页内多少字节
			Assert(off >= 0 && off < LOBLKSIZE);//如果off<0或者>=大对象块的大小,则报错

			getdatafield(data, &datafield, &len, &pfreeit);//获取对应元组(页)的数据域
			//len为数据域的长度
			if (len > off)//如果数据域的长度大于页内偏移量
			{
				n = len - off;//读完当前数据还需要的字节数
				n = (n <= (nbytes - nread)) ? n : (nbytes - nread);//和上面的相同,这里就不解释了
				memcpy(buf + nread, VARDATA(datafield) + off, n);//调用memcpy函数将数据复制进buf
				nread += n;//已经读的字节数多了n
				obj_desc->offset += n;//读写指针前进n
			}
			if (pfreeit)//如果需要释放内存
				pfree(datafield);//释放datafield的内存
		}

		if (nread >= nbytes)//如果读取的字节数大于等于需要的字节数
			break;//终止循环
	}

	systable_endscan_ordered(sd);

	return nread;//返回已经读取的字节数
}

写入大对象

通过调用inv_write函数来向大对象写入数据。
传入参数为:

  • obj_desc 大对象描述符
  • buf 要写入的数据缓冲区
  • nbytes 要写入的数据长度

返回结果为成功写入的字节数

主要流程为:
和上面的读取大对象类似,也是在while循环中。只要写入的字节数比要写入的字节数少,则持续循环。首先判断是否需要获取下一页,如果需要则获取,否则继续写入当前页。如果当前位于想要写入的页,首先判断有无缺失,有的话就填补0,然后再将数据插入到正确的位置;如果当前不位于想要写入的页,则创建一个新页,插入过程和上面相同。最终返回写入的字节数。
细节分析如下:

int
inv_write(LargeObjectDesc *obj_desc, const char *buf, int nbytes)
{
	int			nwritten = 0;//已经写入的字节数
	int			n;//一次写入的长度
	int			off;//页内偏移量
	int			len;//数据域的长度
	int32		pageno = (int32) (obj_desc->offset / LOBLKSIZE);//当前要写入的页号,和读入大对象计算页号的方式相同
	ScanKeyData skey[2];
	SysScanDesc sd;
	HeapTuple	oldtuple;//存放旧元组
	Form_pg_largeobject olddata;//存放旧数据
	bool		neednextpage;//是否需要下一页
	bytea	   *datafield;//数据域
	bool		pfreeit;//是否需要释放内存
	union//联合体
	{
		bytea		hdr;
		char		data[LOBLKSIZE + VARHDRSZ];//分配足够大的空间,可以存放一页加页头的数据量
		int32		align_it;
	}			workbuf;//利用联合体构成缓存区
	char	   *workb = VARDATA(&workbuf.hdr);
	HeapTuple	newtup;
	Datum		values[Natts_pg_largeobject];
	bool		nulls[Natts_pg_largeobject];
	bool		replace[Natts_pg_largeobject];
	CatalogIndexState indstate;
	
	//检查描述符以及缓冲区,和上面的读入数据相同
	Assert(PointerIsValid(obj_desc));
	Assert(buf != NULL);

	if ((obj_desc->flags & IFS_WRLOCK) == 0)//确保具有写的权限
		ereport(ERROR,
				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
				 errmsg("permission denied for large object %u",
						obj_desc->id)));//没有写入权限则报错

	if (nbytes <= 0)//期望写入字节<=0,直接返回0(未写入)
		return 0;

	//判断从读写指针的位置加上要写入的字节数是否超过最大的大对象的大小
	//这里虽然是加法,但不会溢出,因为nbytes是int32
	if ((nbytes + obj_desc->offset) > MAX_LARGE_OBJECT_SIZE)
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
				 errmsg("invalid large object write request size: %d",
						nbytes)));//超过则报错

	open_lo_relation();

	indstate = CatalogOpenIndexes(lo_heap_r);

	//和读文件类似的,也是一系列相关操作,不是分析重点。
	ScanKeyInit(&skey[0],
				Anum_pg_largeobject_loid,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(obj_desc->id));
	ScanKeyInit(&skey[1],
				Anum_pg_largeobject_pageno,
				BTGreaterEqualStrategyNumber, F_INT4GE,
				Int32GetDatum(pageno));
	sd = systable_beginscan_ordered(lo_heap_r, lo_index_r,
									obj_desc->snapshot, 2, skey);
	//初始化下面需要用到的参数
	oldtuple = NULL;
	olddata = NULL;
	neednextpage = true;

	while (nwritten < nbytes)//写入的字节数小于期望写入的字节数,持续循环
	{
		if (neednextpage)//如果需要,就首先获取一个已经存在的页,用于存储数据(并不一定上来就会用到)
		{
			if ((oldtuple = systable_getnext_ordered(sd, ForwardScanDirection)) != NULL)//获取元组(页)
			{
				if (HeapTupleHasNulls(oldtuple))//如果拿到的元组为空
					elog(ERROR, "null field found in pg_largeobject");//报错
				olddata = (Form_pg_largeobject) GETSTRUCT(oldtuple);
				Assert(olddata->pageno >= pageno);//拿到的(元组)页号如果小于当前的页号,说明并没有拿到下一页,因而出错。
			}
			neednextpage = false;//获取完下一页,将该变量置为false
		}

		if (olddata != NULL && olddata->pageno == pageno)//判断是否拿到下一页,并且是否为想要写入的页号
		{
			getdatafield(olddata, &datafield, &len, &pfreeit);//获取数据
			memcpy(workb, VARDATA(datafield), len);//将数据存放在workb缓冲区中
			if (pfreeit)//如果需要释放
				pfree(datafield);//释放数据域

			//填补缺失页
			off = (int) (obj_desc->offset % LOBLKSIZE);//计算读写指针的页内偏移量
			if (off > len)//如果页内偏移量大于该页数据长度,说明该页出线了缺失
				MemSet(workb + len, 0, off - len);//缺失字节全部置0

			n = LOBLKSIZE - off;//获取该页还可插入的数据大小
			n = (n <= (nbytes - nwritten)) ? n : (nbytes - nwritten);//判断n是否小于还需要写入的数据大小,如果小于则全写入,否则写入还需要写入的大小
			memcpy(workb + off, buf + nwritten, n);//将缓冲区对应位置的数据写入workb缓冲区
			nwritten += n;//已写入的字节数增n
			obj_desc->offset += n;//读写指针位置增n
			off += n;//页内偏移量增n
			len = (len >= off) ? len : off;//计算写入后页的新长度。
			SET_VARSIZE(&workbuf.hdr, len + VARHDRSZ);

			 //下面的操作目的是生成并且插入更新后的元组
			memset(values, 0, sizeof(values));
			memset(nulls, false, sizeof(nulls));
			memset(replace, false, sizeof(replace));
			values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
			replace[Anum_pg_largeobject_data - 1] = true;
			newtup = heap_modify_tuple(oldtuple, RelationGetDescr(lo_heap_r),
									   values, nulls, replace);//利用旧元组的数据生成新元组
			CatalogTupleUpdateWithInfo(lo_heap_r, &newtup->t_self, newtup,
									   indstate);//更新生成的元组
			heap_freetuple(newtup);//释放新元组占据的内存

			//旧元组被新元组替代了,因此旧元组不再被需要。
			oldtuple = NULL;
			olddata = NULL;
			neednextpage = true;
		}
		else//如果不为想要写入的页
		{
			//会写入一个全新的页
			off = (int) (obj_desc->offset % LOBLKSIZE);//计算页内偏移量
			if (off > 0)//如果页内偏移量不为0
				MemSet(workb, 0, off);//将缓冲区的前off字节都置为0,填充缺失。

			n = LOBLKSIZE - off;//获取该页还可插入的数据大小
			n = (n <= (nbytes - nwritten)) ? n : (nbytes - nwritten);//判断n是否小于还需要写入的数据大小,如果小于则全写入,否则写入还需要写入的大小
			memcpy(workb + off, buf + nwritten, n);//将缓冲区对应位置的数据写入workb缓冲区
			nwritten += n;//已写入的字节数增n
			obj_desc->offset += n;//读写指针位置增n
			len = off + n;//计算插入数据后新页的长度
			SET_VARSIZE(&workbuf.hdr, len + VARHDRSZ);

			//生成并插入新的元组,与上面的区别就是没有旧元组,不是依靠旧元组生成新元组,同时也无需释放旧元组
			memset(values, 0, sizeof(values));
			memset(nulls, false, sizeof(nulls));
			values[Anum_pg_largeobject_loid - 1] = ObjectIdGetDatum(obj_desc->id);
			values[Anum_pg_largeobject_pageno - 1] = Int32GetDatum(pageno);
			values[Anum_pg_largeobject_data - 1] = PointerGetDatum(&workbuf);
			newtup = heap_form_tuple(lo_heap_r->rd_att, values, nulls);
			CatalogTupleInsertWithInfo(lo_heap_r, newtup, indstate);
			heap_freetuple(newtup);
		}
		pageno++;//页号自增
	}

	systable_endscan_ordered(sd);
	CatalogCloseIndexes(indstate);
	CommandCounterIncrement();

	return nwritten;//返回成功写入的字节数
}

删除大对象

调用inv_drop函数来删除一个已经存在的大对象。
传入参数:

  • lobjId 即要删除的大对象的OID

细节分析如下:

int
inv_drop(Oid lobjId)
{
	ObjectAddress object;//用于定位大对象的位置

	 //删除大对象的相关依赖
	object.classId = LargeObjectRelationId;//大对象所在的表的OID
	object.objectId = lobjId;//大对象的OID
	object.objectSubId = 0;
	performDeletion(&object, DROP_CASCADE, 0);//调用performDeletion进行级联删除
	//关于传入的0,它是标志位,在其函数中有相关介绍,我会在下面。
	
	return 1;//由于历史原因,返回1代表删除成功
}

关于上面提到的标志位的介绍:

  • PERFORM_DELETION_INTERNAL: 表示系统内部调用的删除,而非用户调用的删除。
  • PERFORM_DELETION_CONCURRENTLY: 同时删除,只对删除索引及时生效。
  • PERFORM_DELETION_QUIETLY: 降低报告的等级。
  • PERFORM_DELETION_SKIP_ORIGINAL: 不删除指定的对象。
  • PERFORM_DELETION_SKIP_EXTENSIONS:不删除扩展,即使删除的对象是扩展的一部分,删除临时对象时会用到。
  • PERFORM_DELETION_CONCURRENT_LOCK: 如果不能同时删除则会上锁。

源码中的定义:

#define PERFORM_DELETION_INTERNAL			0x0001	
#define PERFORM_DELETION_CONCURRENTLY		0x0002	
#define PERFORM_DELETION_QUIETLY			0x0004	
#define PERFORM_DELETION_SKIP_ORIGINAL		0x0008	
#define PERFORM_DELETION_SKIP_EXTENSIONS	0x0010	
#define PERFORM_DELETION_CONCURRENT_LOCK	0x0020	

总结

通过对源码分析,了解了postgreSQL的另一种大对象的存储机制,这种机制与上一篇分析的机制完全不同,是用户可以主动使用的,这其中也涉及到了postgreSQL关于MVCC机制的核心-快照技术。
总的来讲,了解了大对象在系统表中如何存储,大对象的数据结构,以及一些核心的操作。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值