【4】自己写数据库函数库 — 获取一条记录

函数db_fetch为用户接口,根据传入的键值寻找对应的数据,如果找到,则返回指向数据的指针,否则返回NULL,代码如下:

/* 根据给定的键读取一条记录 */
char *db_fetch(DBHANDLE h, const char *key)
{
	DB   *db = h;
	char *ptr;

	/* 参数三:0表示读,1表示写,根据此函数决定加什么锁 */
	if (_db_find_and_lock(db, key, 0) < 0)
	{
		/* 返回-1表示未找到记录 */
		ptr = NULL;
		db->cnt_fetcherr++;
	}
	else	/* 返回0表示查找成功 */
	{
		ptr = _db_readdat(db);	/* 返回找到的数据 */
		db->cnt_fetchok++;
	}

	if (un_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	return ptr;
}

此函数的最后用到了un_lock宏函数来解锁,因为在_db_find_and_lock函数中获得了一个锁。db_fetch函数的大致流程是:根据传入的key调用_db_find_and_lock查找对应的索引记录,如果找到,则继续调用_db_readdat读取对应的数据记录,未找到,则不会调用_db_readdat函数,而是直接返回NULL。 所以,用户在调用db_fetch时一定要判断返回值。我在刚开使用时由于未判断返回值而直接打印返回的指针,结果导致在未找到的情况下出现Segmentation fault。


函数_db_find_and_lock是关键所在,下面是它的代码:

/* writelock:0表示读,1表示写
 * 返回值:0查找成功,-1查找失败
 */
static int _db_find_and_lock(DB *db, const char *key, int writelock)
{
	off_t offset, nextoffset;

	/* 根据散列函数定位桶的起始字符,这里要跳过第一个空闲链表 */
	db->chainoff = (_db_hash(db, key) * PTR_SZ) + db->hashoff;
	db->ptroff   = db->chainoff;	/* 同样指向某个桶 */

	if (writelock)	/* 写锁 */
	{
		/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
		if (writew_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}
	else	/* 读锁 */
	{
		/* 这里只锁一个链的开头一个字节,其它链仍然可用 */
		if (readw_lock(db->idxfd, db->chainoff, SEEK_SET, 1) < 0)
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}

	/* 知道偏移量,知道指针宽度,atol将指针转换成数字类型 */
	offset = _db_readptr(db, db->ptroff);		/* 直接读取桶中的元素,即第一个节点的偏移量 */

	/* 沿着桶向下查找 */
	while (offset != 0)							/* 如果为0则该散列链为空 */
	{
		nextoffset = _db_readidx(db, offset);	/* 读取一条索引记录存入db->idxbuf */

		/* db->idxbuf = 键值 '\0' 数据记录偏移量 '\0' 数据记录长度 '\0' */

		if (strcmp(db->idxbuf, key) == 0)
			break;
		db->ptroff = offset;		/* ptroff记录前一索引记录的地址 */
		offset = nextoffset;
	}

	return (offset == 0 ? -1 : 0);	/* offset = 0则没有找到记录 */
}

_db_find_and_lock函数根据传入的key计算出散列值,然后定位到散列表中的某个链表,从链表头向后遍历每条记录,直到找出某条记录保存的key和传入的key相等或到链表结尾为止。散列函数如下:

/* 散列函数 */
static DBHASH _db_hash(DB *db, const char *key)
{
	DBHASH hval = 0;
	char c;
	int i;

	/* 以下为散列函数 */
	for (i = 1; (c = *key++) != 0; i++)
		hval += c * i;
	return (hval % db->nhash);	/* db->nhash = 137 */
}

下面介绍_db_find_and_lock调用到的相关函数。首先是_db_readptr,代码如下:

/* 读取一个指针并转换为数值型,凡是要获取指针值都要调用这个函数
 * 根据offset知道指针所在位置,指针所占字节宽度也已知,就能读取一个指针的值了
 * 注意:此函数未进行加锁操作,需要手动进行
 */
static off_t _db_readptr(DB *db, off_t offset)
{
	char asciiptr[PTR_SZ + 1];

	if (lseek(db->idxfd, offset, SEEK_SET) == -1)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (read(db->idxfd, asciiptr, PTR_SZ) != PTR_SZ)	/* PTR_SZ = 6,一次读6个字符 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	asciiptr[PTR_SZ] = 0;	/* 追加一个空字符,为了atol函数辨识结尾处 */

	/* atol会跳过前面的空格,遇到非数字或字符串结束时停止转换 */
	return (atol(asciiptr));
}

由于所有的记录都是以字符的形式保存在文件中的,所以如果想要获得指针值,则必须将字符串类型转换成数值类型,上述函数就是这个作用。根据偏移量,就能知道指针所在位置,然后读取这个字符串,把字符串转换成数值类型,然后返回。


接下来是_db_readidx函数:

/* 根据索引记录的偏移值offset读取一条索引记录,填充DB结构许多成员
 * 返回值:下一条记录的偏移量
 */
static off_t _db_readidx(DB *db, off_t offset)
{
	ssize_t i;
	char *ptr1, *ptr2;
	char asciiptr[PTR_SZ + 1], asciilen[IDXLEN_SZ + 1];
	struct iovec iov[2];

	/* 保存当前索引记录偏移量
	 * 如果offset为0,表示从当前偏移量处读
	 */
	if ((db->idxoff = lseek(db->idxfd, offset, offset == 0 ? SEEK_CUR : SEEK_SET)) == -1)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 散布读,第一部分放asciiptr中,第二部分放asciilen中 */
	iov[0].iov_base = asciiptr;
	iov[0].iov_len  = PTR_SZ;		/* 6 */
	iov[1].iov_base = asciilen;
	iov[1].iov_len  = IDXLEN_SZ;	/* 4 */
	if ((i = readv(db->idxfd, &iov[0], 2)) != PTR_SZ + IDXLEN_SZ)
	{
		/* 返回读取的字节数,结尾返回-1 */
		if (i == 0 && offset == 0)
			return -1;	/* 这个返回值是给函数db_nextrec使用的,_db_find_and_lock永远不可能返回-1 */
		else
		{
			printf("%d\n", __LINE__);
			exit(-1);
		}
	}

	/* 下一条索引记录偏移量 */
	asciiptr[PTR_SZ] = 0;
	db->ptrval = atol(asciiptr);

	/* 当前索引记录长度 */
	asciilen[IDXLEN_SZ] = 0;
	if ((db->idxlen = atoi(asciilen)) < IDXLEN_MIN || db->idxlen > IDXLEN_MAX)
	{
		/* 索引记录长度必须在6~1024字节之间 */
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 下面读取实际的索引记录,文件指针已经在调用readv后指向正确位置,即key开头 */
	if ((i = read(db->idxfd, db->idxbuf, db->idxlen)) != db->idxlen)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (db->idxbuf[db->idxlen -1] != NEWLINE)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	db->idxbuf[db->idxlen - 1] = 0;	/* 把换行符替换为空字符,为了atol函数做准备 */

	/* 找出分隔符 */
	if ((ptr1 = strchr(db->idxbuf, SEP)) == NULL)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	*ptr1++ = 0;	/* 分隔符替换为空字符,ptr1现在指向数据记录的偏移量 */

	if ((ptr2 = strchr(ptr1, SEP)) == NULL)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	*ptr2++ = 0;	/* 分隔符替换为空字符,ptr2现在指向数据记录的长度 */

	if (strchr(ptr2, SEP) != NULL)	/* 只有两个分隔符 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 保存实际数据的偏移量 */
	if ((db->datoff = atol(ptr1)) < 0)	/* atol遇到空字符结束 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	/* 保存实际数据的长度,范围必须在2~1024字节之间 */
	if ((db->datlen = atol(ptr2)) <= 0 || db->datlen > DATLEN_MAX)
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}

	return (db->ptrval);
}

由_db_readptr知道索引记录偏移量,在调用_db_readidx函数从文件中读取一条索引记录到一个buffer中,这个buffer实际上就是db->idxbuf。在调用完这个函数后,DB结构体中的许多成员都记录了某条记录的信息,例如数据偏移量,数据长度,索引记录长度等。知道了数据偏移量和数据长度,就可以读取数据文件中实际的数据了,此即函数_db_readdat的职责:

/* 读取实际数据到缓存db->datbuf中
 * 返回值:NULL结尾的实际数据
 */
static char* _db_readdat(DB *db)
{
	if (lseek(db->datfd, db->datoff, SEEK_SET) == -1)			/* 定位 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (read(db->datfd, db->datbuf, db->datlen) != db->datlen)	/* 读入缓存 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	if (db->datbuf[db->datlen - 1] != NEWLINE)	/* 数据文件中的一个条必以换行符结尾 */
	{
		printf("%d\n", __LINE__);
		exit(-1);
	}
	db->datbuf[db->datlen - 1] = 0;				/* 换行符替换成空字符 */
	return (db->datbuf);
}

由于存放在文件中时,每条数据记录都是以换行符结尾,此函数读取一条数据记录后,把换行符修改为结束符,这方便了用户代码直接打印返回的数据。

你可能感兴趣的:(【4】自己写数据库函数库 — 获取一条记录)