9.1-kv存储持久化的设计

include/persist.h

定义文件格式、结构体、对外接口。

真正的持久化实现,包含 mmapio_uring、文件序列化、恢复逻辑。

src\\kvstore.c

把持久化接到命令处理流程里,负责启动加载、SAVE、写命令追加 AOF。

src/ntyco.c

网络层,只负责收包、回调kvs protocol。

array.c rbtree.c hash.c skiptable.c

各个引擎的内存实现

在持久化落盘的时候使用io_ruing的方式。在加载日志的时候使用mmap的方式

整体架构:

客户端

cpp 复制代码
客户端
  |
  v
ntyco / reactor / proactor
  |
  v
kvs_protocol()
  |
  v
kvs_filter_protocol()
  |
  +--> SET / MOD / DEL 先改内存引擎
  |        |
  |        +--> persist_append_increment() 追加 AOF
  |
  +--> SAVE
           |
           +--> persist_save_all() 写全量快照
           +--> persist_clear_increment() 清空旧 AOF

kvstore.h源代码:

复制代码
#ifndef __PERSIST_H__
#define __PERSIST_H__

/**
 * 持久化模块头文件
 * 
 * 这一层只负责定义规则, 不负责真正把数据写进文件
 * 
 * 文件应该长什么样
 * 对外提供哪些函数
 * 每条记录有哪些字段
 */

#include <stdint.h>

/*
 * 这里不能 include "kvstore.h"。
 *
 * 原因:
 *   kvstore.h 会 include persist.h;
 *   如果 persist.h 又 include kvstore.h,就会形成循环包含。
 *
 * 这里的函数参数只用到了这些结构体的"指针",
 * 所以我们不需要知道结构体里面有哪些成员,
 * 只要提前告诉编译器"这些类型以后会存在"就够了。
 */
typedef struct kvs_array_s kvs_array_t;
typedef struct _rbtree kvs_rbtree_t;
typedef struct hashtable_s kvs_hash_t;
typedef struct kvs_skiplist_s kvs_skiplist_t;


/**
 * 这个枚举用来标记"当前这条数据来自哪个容器"。
 * 既可以用用到文件头
 * 也可以用到单条命令的头
 * 用掩码,因为后面可以同时保存多个容器
 */
typedef enum persist_engine_e{
	PERSIST_ENGINE_ARRAY	= 1u << 0,
	PERSIST_ENGINE_RBTREE	= 1u << 1,
	PERSIST_ENGINE_HASH		= 1u << 2,
	PERSIST_ENGINE_SKIPLIST	= 1u << 3,
	PERSIST_ENGINE_ALL		= PERSIST_ENGINE_ARRAY | PERSIST_ENGINE_RBTREE | PERSIST_ENGINE_HASH | PERSIST_ENGINE_SKIPLIST,
}persist_engine_t;

/*
 * 增量持久化保存的不是"最终数据",而是"发生过什么写操作"。
 *
 * SET:新增一条 key/value
 * MOD:修改一条 key/value
 * DEL:删除一条 key
 */
typedef enum persist_op_e {
	PERSIST_OP_SET = 1,
	PERSIST_OP_MOD = 2,
	PERSIST_OP_DEL = 3,
} persist_op_t;

/*文件头里的"魔数", 用来识别这是不是持久化文件*/
#define PERSIST_MAGIC "KVS1"
#define PERSIST_MAGIC_LEN	4

/*版本号,方便以后升级文件的格式*/
#define PERSIST_VERSION	1

/**
 * 文件头
 * 一份持久化文件,最前面先写这个结构
 */
typedef struct persist_file_header_s{
	char magic[PERSIST_MAGIC_LEN];
	uint32_t version;
	uint32_t engine_mask;
	uint32_t record_count;
}persist_file_header_t;

/**
 * 每一条键值对都先写这个"记录头"
 */
typedef struct persist_record_header_s{
	uint32_t engine;
	uint32_t key_len;
	uint32_t value_len;
}persist_record_header_t;

/*
 * 增量日志里每一条操作的头。
 *
 * 增量日志是一条一条追加的:
 *   [increment_header][key bytes][value bytes]
 *
 * DEL 操作没有 value,所以 value_len 可以是 0。
 */
#define PERSIST_INCR_MAGIC "AOF1"
#define PERSIST_INCR_MAGIC_LEN 4
#define PERSIST_INCR_VERSION 1

typedef struct persist_increment_header_s {
	char magic[PERSIST_INCR_MAGIC_LEN];
	uint32_t version;
	uint32_t op;
	uint32_t engine;
	uint32_t key_len;
	uint32_t value_len;
} persist_increment_header_t;

/*对外的接口:保存所有数据、加载所有数据*/
int persist_save_all(const char *path);
int persist_load_all(const char *path);

/* 增量持久化接口 */
int persist_append_increment(const char *path, persist_op_t op, persist_engine_t engine, const char *key, const char *value);
int persist_load_increment(const char *path);
int persist_clear_increment(const char *path);

/*各引擎保存*/
int persist_save_array(const char *path, const kvs_array_t *inst);
int persist_save_rbtree(const char *path, const kvs_rbtree_t *inst);
int persist_save_hash(const char *path, const kvs_hash_t *inst);
int persist_save_skiplist(const char *path, const kvs_skiplist_t *inst);

/*引擎加载*/
int persist_load_array(const char *path, kvs_array_t *inst);
int persist_load_rbtree(const char *path, kvs_rbtree_t *inst);
int persist_load_hash(const char *path, kvs_hash_t *inst);
int persist_load_skiplist(const char *path, kvs_skiplist_t *inst);

#endif

启动回复流程:

cpp 复制代码
main()
  |
  +--> init_kvengine()
  |
  +--> persist_load_all("kvstore.data")
  |        |
  |        +--> mmap 读快照
  |        +--> 清空旧引擎
  |        +--> 一条条恢复到内存
  |
  +--> persist_load_increment("kvstore.aof")
           |
           +--> mmap 读 AOF
           +--> 一条条回放写操作

文件格式:

kvstore.data 不是简单文本,是二进制结构:

cpp 复制代码
[文件头]
  magic
  version
  engine_mask
  record_count

[记录1]
  engine
  key_len
  value_len
  key bytes
  value bytes

[记录2]
  ...

kvstore.aof 也是二进制,但记录的是操作:

复制代码
[增量头]
  magic
  version
  op
  engine
  key_len
  value_len

[key bytes]
[value bytes]

关键函数:

cpp 复制代码
网络入口
- ntyco_start(port, kvs_protocol)
- kvs_protocol() 把一行命令交给协议解析

命令分发
- kvs_process_one_cmd()
- kvs_filter_protocol()

写内存
- kvs_array_set / mod / del
- kvs_rbtree_set / mod / del
- kvs_hash_set / mod / del
- kvs_skiplist_set / mod / del

追加增量日志
- kvs_append_increment_by_cmd()
- persist_append_increment()

保存全量快照
- persist_save_all()
- persist_count_*()
- persist_dump_*_to_buf()
- persist_write_fd_by_uring()

恢复
- persist_load_all()
- persist_load_snapshot_from_mem()
- persist_reinit_all_globals()
- persist_apply_snapshot_record()
- persist_load_increment()
- persist_load_increment_from_mem()
- persist_replay_increment_op()

mmap 是用法:

mmap 只用在"读文件"阶段,不是单独一个大模块。

cpp 复制代码
map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
ret = persist_load_snapshot_from_mem((const char *)map, st.st_size);
munmap(map, st.st_size);

它的意义是:

  • 不用 fread 一段一段拷贝
  • 直接把文件映射成内存
  • 解析函数用指针 p 一直往后走
  • 读快照和读 AOF 都是这个套路

可以理解成:

"把磁盘文件变成一块只读内存,在按字节指针解析"

io_uring是怎么用的:

io_uring 只用在 persist_save_all() 的落盘阶段。

流程是:

  1. 先把快照内容拼进内存缓冲区 persist_buf_t
  2. 打开 kvstore.data.tmp
  3. persist_write_fd_by_uring()
  4. 分块写入,单块最多 1MB
  5. 最后 fsync
  6. rename(tmp, path) 原子替换正式快照

也就是说:

内存缓冲区 -> io_uring 写 tmp 文件 -> fsync -> rename 成正式 data 文件。

这样设计的好处是:

  • 写快照时更稳
  • 避免半写坏文件直接变正式文件
  • rename 之后要么是旧文件,要么是新文件,不会中间态暴露给加载器

注意:

AOF 追加没有用 io_uring ,它现在是 fopen("ab") + fwrite + fflush + fclose

这是刻意简化的设计:快照是大块写,AOF 是一条条小写,处理方式不一样。

全量持久化涉及到的代码:

当输入 SAVE的时候,会触发全量持久化:

cpp 复制代码
//触发全量持久化
int kvs_filter_protocol(kvs_command_t *req, kvs_response_t *out, int from_replica){
    if (cmd == KVS_CMD_SAVE) {
		ret = persist_save_all(persist_file);
		if (ret == 0) {
			ret = persist_clear_increment(persist_incr_file);
		}
		if (ret == 0) {
			return kvs_reply_status(out, resp_mode, "OK");
		}
		return kvs_reply_error(out, resp_mode, "save failed");
	}
}


int persist_save_all(const char *path)
{
	if(!path) return -1;
	
	persist_buf_t buf;
	uint32_t array_count = 0;
	uint32_t rbtree_count = 0;
	uint32_t hash_count = 0;
	uint32_t skiplist_count = 0;
	uint32_t record_count = 0;
	uint32_t engine_mask = 0;
	char *tmp_path = NULL;
	int fd = -1;
	int ret = -1;

	memset(&buf, 0, sizeof(buf));

	/*
	 * 先统计所有引擎里一共有多少条数据。
	 * 这样文件头里的 record_count 就是准确的。
	 */
	array_count = persist_count_array(&global_array);
	rbtree_count = persist_count_rbtree_node(&global_rbtree, global_rbtree.root);
	hash_count = persist_count_hash(&global_hash);
	skiplist_count = persist_count_skiplist(&global_skiplist);

	record_count = array_count + rbtree_count + hash_count + skiplist_count;

	if(array_count > 0) engine_mask |= PERSIST_ENGINE_ARRAY;
	if(rbtree_count > 0) engine_mask |= PERSIST_ENGINE_RBTREE;
	if(hash_count > 0)engine_mask |= PERSIST_ENGINE_HASH;
	if(skiplist_count > 0)engine_mask |= PERSIST_ENGINE_SKIPLIST;

	if(persist_buf_append_header(&buf, engine_mask, record_count) < 0){
		goto done;
	}

	if(persist_dump_array_to_buf(&buf, &global_array) < 0) goto done;
	if(persist_dump_rbtree_to_buf(&buf, &global_rbtree, global_rbtree.root) < 0) goto done;
	if(persist_dump_hash_to_buf(&buf, &global_hash) < 0) goto done;
	if(persist_dump_skiplist_to_buf(&buf, &global_skiplist) < 0) goto done;

	tmp_path = (char *)kvs_malloc(strlen(path) + 5);
	if(!tmp_path){
		goto done;
	}

	snprintf(tmp_path, strlen(path) + 5, "%s.tmp", path);

	fd = open(tmp_path, O_CREAT | O_TRUNC | O_WRONLY, 0644);
	if(fd < 0) goto done;

	if(persist_write_fd_by_uring(fd, buf.data, buf.len) < 0) goto done;

	close(fd);
	fd = -1;

	if(rename(tmp_path, path) < 0){
		goto done;
	}

	ret = 0;
	
	done:
		if(fd >= 0) close(fd);
		if(tmp_path){
			if(ret != 0) unlink(tmp_path);
			kvs_free(tmp_path);
		}
		persist_buf_free(&buf);
		return ret;
}


static int persist_buf_append_header(persist_buf_t *buf, uint32_t engine_mask, uint32_t record_count){
	persist_file_header_t header;

	memset(&header, 0, sizeof(header));
	memcpy(header.magic, PERSIST_MAGIC, PERSIST_MAGIC_LEN);
	header.version = PERSIST_VERSION;
	header.engine_mask = engine_mask;
	header.record_count = record_count;

	return persist_buf_append(buf, &header, sizeof(header));
}

static int persist_buf_append(persist_buf_t *buf, const void *src, size_t len){
	if(!buf || !src) return -1;
	if(len == 0) return 0;

	if(persist_buf_reserve(buf,buf->len + len) < 0){
		return -1;
	}

	memcpy(buf->data + buf->len, src, len);
	buf->len += len;
	return 0;
}

static int persist_buf_reserve(persist_buf_t *buf, size_t need){
	size_t new_cap = 0;
	char *new_data = NULL;

	if(!buf || need == 0) return -1;
	if(buf->cap >= need) return 0;

	new_cap = (buf->cap > 0) ? buf->cap : 4096;
	while(new_cap < need){
		new_cap *= 2;
	}

	new_data = (char *)kvs_malloc(new_cap);
	if(!new_data) return -1;

	if(buf->data && buf->len > 0){
		memcpy(new_data, buf->data, buf->len);
	}

	if(buf->data){
		kvs_free(buf->data);
	}

	buf->data = new_data;
	buf->cap = new_cap;
	return 0;

}


static int persist_write_fd_by_uring(int fd, const char *buf, size_t len){
	struct io_uring ring;
	size_t offset = 0;

	if(io_uring_queue_init(8, &ring, 0) != 0){
		return -1;
	}

	while(offset < len){
		size_t chunk = len - offset;
		struct io_uring_sqe *sqe = NULL;
		struct io_uring_cqe *cqe = NULL;

		if(chunk > (1u << 20)){
			chunk = (1u << 20);
		}

		sqe = io_uring_get_sqe(&ring);
		if(!sqe){
			if(io_uring_submit(&ring) < 0){
				io_uring_queue_exit(&ring);
				return -1;
			}

			continue;
		}

		io_uring_prep_write(sqe, fd, buf + offset, chunk, (off_t)offset);

		if(io_uring_submit(&ring) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		if(io_uring_wait_cqe(&ring, &cqe) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		if(cqe->res <= 0){
			io_uring_cqe_seen(&ring, cqe);
			io_uring_queue_exit(&ring);
			return -1;
		}

		offset += (size_t)cqe->res;
		io_uring_cqe_seen(&ring, cqe);
	}

	{
		struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
		struct io_uring_cqe *cqe = NULL;

		if(!sqe){
			io_uring_queue_exit(&ring);
			return -1;
		}

		io_uring_prep_fsync(sqe, fd, 0);

		if(io_uring_submit(&ring) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		if(io_uring_wait_cqe(&ring, &cqe) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		if(cqe->res < 0){
			io_uring_cqe_seen(&ring, cqe);
			io_uring_queue_exit(&ring);
			return -1;
		}

		io_uring_cqe_seen(&ring, cqe);
	}
	
	io_uring_queue_exit(&ring);
	return 0;
}

全量持久化加载:

cpp 复制代码
/*
 * 读取全部数据。
 * 
 */
int persist_load_all(const char *path)
{
	if(!path) return -1;

	int fd = -1;
	struct stat st;
	void *map = NULL;
	int ret = -1;

	fd = open(path, O_RDONLY);
	if(fd < 0) return -1;

	if(fstat(fd, &st) < 0){
		close(fd);
		return -1;
	}

	if(st.st_size == 0){
		close(fd);
		return 0;
	}

	map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	if(map == MAP_FAILED){
		close(fd);
		return -1;
	}

	ret = persist_load_snapshot_from_mem((const char *)map, (size_t)st.st_size);

	munmap(map, (size_t)st.st_size);
	close(fd);
	return ret;
	
}

static int persist_load_snapshot_from_mem(const char *base, size_t size){

	const char *p = base;
	const char *end = base + size;
	persist_file_header_t file_header;

	if(size < sizeof(file_header)){
		return -1;
	}

	memcpy(&file_header, p, sizeof(file_header));
	p += sizeof(file_header);

	if(memcmp(file_header.magic, PERSIST_MAGIC, PERSIST_MAGIC_LEN) != 0){
		return -1;
	}

	if(file_header.version != PERSIST_VERSION){
		return -1;
	}

	if(file_header.engine_mask & ~PERSIST_ENGINE_ALL){
		return -1;
	}

	/**
	 * 先清空,再回复
	 */
	if(persist_reinit_all_globals() < 0){
		return -1;
	}

	for(uint32_t i = 0; i < file_header.record_count; i++){
		persist_record_header_t record_header;
		char *key_buf = NULL;
		char *value_buf = NULL;

		if((size_t)(end - p) < sizeof(record_header)){
			return -1;
		}

		memcpy(&record_header, p, sizeof(record_header));
		p += sizeof(record_header);

		if((size_t)(end - p) < (size_t)record_header.key_len + (size_t)record_header.value_len){
			return -1;
		}

		key_buf = (char *) kvs_malloc((size_t)record_header.key_len + 1);
		value_buf = (char *) kvs_malloc((size_t)record_header.value_len + 1);
		if(!key_buf || !value_buf){
			if(key_buf) kvs_free(key_buf);
			if(value_buf) kvs_free(value_buf);
			return -1;
		}

		memset(key_buf, 0, (size_t)record_header.key_len + 1);
		memset(value_buf, 0, (size_t)record_header.value_len + 1);

		memcpy(key_buf, p, record_header.key_len);
		p += record_header.key_len;
		memcpy(value_buf, p, record_header.value_len);
		p += record_header.value_len;

		key_buf[record_header.key_len] = '\0';
		value_buf[record_header.value_len] = '\0';

		if (persist_apply_snapshot_record((persist_engine_t)record_header.engine,
										  key_buf, record_header.key_len,
										  value_buf, record_header.value_len) < 0) {
			kvs_free(key_buf);
			kvs_free(value_buf);
			return -1;
		}

		kvs_free(key_buf);
		kvs_free(value_buf);
	}

	return (p == end) ? 0 : -1;
}

增量持久化:

每次执行写操作(set mod delte)操作后,往日志里追加本次的改动。

cpp 复制代码
int kvs_filter_protocol(kvs_command_t *req, kvs_response_t *out, int from_replica)
{
    if (ret == 0 && kvs_is_write_cmd(cmd)) {
		if (kvs_append_increment_by_cmd(cmd, key, value) < 0) {
			out->len = 0;
			return kvs_reply_error(out, resp_mode, "persist failed");
		}

		if (from_replica) {
			kvs_replication_incr_offset();
		} else {
			kvs_replication_sync_write_to_slave(cmd, key, value);
		}
	}

}

void kvs_replication_incr_offset(void){
	g_repl_offset++;
	kvs_save_repl_offset();
	printf("incr repl offset:%lld\n", g_repl_offset);
}

static void kvs_save_repl_offset(void){
	FILE *fp = fopen(repl_offset_file, "w");
	if(!fp) return;

	fprintf(fp, "%lld\n", g_repl_offset);
	fclose(fp);
}


/*
 * 对外的一层同步函数。
 *
 * kvs_filter_protocol 执行完本地写入以后调用它。
 */
void kvs_replication_sync_write_to_slave(int cmd, char *key, char *value)
{
	if (!kvs_replica_enabled()) return;

	char plain_cmd[KVS_REPL_CMD_SIZE] = {0};
	char replica_cmd[KVS_REPL_CMD_SIZE] = {0};

	if(kvs_build_plain_write_cmd(cmd, key, value, plain_cmd, sizeof(plain_cmd)) < 0){
		return;
	}

	/**
	 * 不管从库现在又没有连上,主库都先把写命令放进backlog
	 * 这样从库以后可以用PSYNC追数据
	 */
	kvs_repl_backlog_append(plain_cmd);

	if (kvs_build_replica_cmd(cmd, key, value,
							  replica_cmd, sizeof(replica_cmd)) < 0) {
		return;
	}

	if (kvs_send_cmd_to_slave(replica_cmd) < 0) {
		printf("replica sync failed: %s\n", replica_cmd);
	}
}


static int kvs_build_plain_write_cmd(int cmd, char *key, char *value, char *out, int out_size){
	if(!key || !out || out_size <= 0) return -1;

	switch (cmd)
	{
	case KVS_CMD_DEL:
	case KVS_CMD_RDEL:
	case KVS_CMD_HDEL:
	case KVS_CMD_SDEL:
		/**
		 * 删除命令没有value
		 */
		return snprintf(out, out_size, "%s %s\r\n", command[cmd], key);
	
	default:
		if(value == NULL) return -1;
		return snprintf(out, out_size, "%s %s %s\r\n", command[cmd], key, value);
	}
}


static void kvs_repl_backlog_append(char *plain_cmd){
	if(!plain_cmd) return;

	/**
	 * 新写命令产生一个新的offset。
	 */
	g_repl_offset++;

	int idx = (int)((g_repl_offset - 1) % KVS_REPL_BACKLOG_SIZE);

	g_repl_backlog[idx].offset = g_repl_offset;

	memset(g_repl_backlog[idx].cmd, 0, KVS_REPL_CMD_SIZE);
	strncpy(g_repl_backlog[idx].cmd, plain_cmd, KVS_REPL_CMD_SIZE - 1);

	printf("bakclog append offset=%lld cmd=%s", g_repl_offset, plain_cmd);

}

/*
 * 把一条写命令拼成文本协议。
 *
 * 例如:
 *   SET name Tom
 *   DEL name
 *
 * 注意:
 *   当前项目的协议本来就不支持 key/value 中带空格,
 *   所以这里也先保持一致。
 */
static int kvs_build_replica_cmd(int cmd, char *key, char *value,
								 char *out, int out_size)
{
	char plain_cmd[KVS_REPL_CMD_SIZE] = {0};
	if(kvs_build_plain_write_cmd(cmd, key, value, plain_cmd, sizeof(plain_cmd)) < 0){
		return -1;
	}

	/**
	 * 真正发给从库时,加 REPL前缀
	 * 从库看到REPL,才会走 kvs_protocol_replica
	 */
	return snprintf(out, out_size, "REPL %s", plain_cmd);
}

static int kvs_send_cmd_to_slave(char *cmd)
{
	if(!cmd) return -1;

	/**
	 * 尝试两次:
	 * 	第一次:复用旧链接,如果是第一次连,就新建连接。
	 * 	如果旧链接已经断了,就关闭它
	 * 第二次:重新connect 从库,再发送
	 */

	for(int retry = 0; retry < 2; retry++){
		int fd = kvs_connect_slave();
		if(fd < 0) return -1;

		if(kvs_send_all(fd, cmd, strlen(cmd)) < 0){
			kvs_replication_close_slave_conn();
			continue;
		}

		/**
		 * 等从库返回执行结果
		 * 这里先不严格判断OK / ERROR,只要能收到响应,
		 * 救认为这条同步命令已经送到 并且 被从库处理
		 */
		char buf[128] = {0};
		int n = recv(fd, buf, sizeof(buf) - 1, 0);
		if(n <= 0){
			printf("recv slave ack failed, reconnect next time\n");
			kvs_replication_close_slave_conn();
			continue;
		}

		printf("replica sync ok:%s -> %s", cmd, buf);
		return 0;
	}

	return -1;
}

增量持久化加载:

cpp 复制代码
persist_load_increment(persist_incr_file);

/*
 * 加载增量日志。
 *
 * 启动恢复时的顺序应该是:
 *   1. persist_load_all("kvstore.data")
 *      先恢复上一次全量快照
 *
 *   2. persist_load_increment("kvstore.aof")
 *      再把快照之后发生过的写操作重新执行一遍
 */
int persist_load_increment(const char *path)
{
	if(!path) return -1;

	int fd = -1;
	struct stat st;
	void *map = NULL;
	int ret = -1;

	fd = open(path, O_RDONLY);
	if(fd < 0) return 0;

	if(fstat(fd, &st) < 0){
		close(fd);
		return -1;
	}

	if(st.st_size == 0){
		close(fd);
		return 0;
	}

	map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	if(map == MAP_FAILED){
		close(fd);
		return -1;
	}

	ret = persist_load_increment_from_mem((const char *)map, (size_t)st.st_size);

	munmap(map, (size_t)st.st_size);
	close(fd);

	return ret;
}

static int persist_load_increment_from_mem(const char *base, size_t size){
	const char *p = base;
	const char *end = base + size;

	while(p < end){
		persist_increment_header_t header;
		char *key_buf = NULL;
		char *value_buf = NULL;

		if((size_t)(end - p) < sizeof(header)) return-1;

		memcpy(&header, p, sizeof(header));
		p += sizeof(header);

		if(memcmp(header.magic, PERSIST_INCR_MAGIC, PERSIST_INCR_MAGIC_LEN) != 0) return -1;
		if(header.version != PERSIST_INCR_VERSION) return -1;
		if(header.key_len == 0) return -1;
		if((header.op == PERSIST_OP_SET || header.op == PERSIST_OP_MOD) && header.value_len == 0) return -1;

		if((size_t)(end - p) < (size_t)header.key_len + (size_t)header.value_len) return -1;

		key_buf = (char *)kvs_malloc((size_t)header.key_len + 1);
		value_buf = (char *)kvs_malloc((size_t)header.value_len + 1);

		if(!key_buf || !value_buf){
			if(key_buf) kvs_free(key_buf);
			if(value_buf) kvs_free(value_buf);
			return -1;
		}

		memset(key_buf, 0, (size_t)header.key_len + 1);
		memset(value_buf, 0, (size_t)header.value_len + 1);

		memcpy(key_buf, p, header.key_len);
		p += header.key_len;
		memcpy(value_buf, p, header.value_len);
		p += header.value_len;

		key_buf[header.key_len] = '\0';
		value_buf[header.value_len] = '\0';
		
		if (persist_replay_increment_op((persist_op_t)header.op,
										(persist_engine_t)header.engine,
										key_buf, value_buf,
										header.key_len, header.value_len) < 0)
		{
			kvs_free(key_buf);
			kvs_free(value_buf);
			return -1;
		}

		kvs_free(key_buf);
		kvs_free(value_buf);
		
	}

	return 0;
}

/*
 * 回放一条增量操作。
 *
 * 加载增量日志时,我们不是直接改文件,
 * 而是把日志里记录的 SET/MOD/DEL 重新在内存里执行一遍。
 * 增量回放:SET 允许覆盖,MOD 也允许补救式覆盖,像 Redis 一样恢复最终态
 */
static int persist_replay_increment_op(persist_op_t op, persist_engine_t engine,
									   char *key, char *value,
									   uint32_t key_len, uint32_t value_len)
{
	int ret = -1;

	switch (engine) {
	case PERSIST_ENGINE_ARRAY:
		if (op == PERSIST_OP_SET) {
			ret = kvs_array_set(&global_array, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_array_mod(&global_array, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_MOD) {
			ret = kvs_array_mod(&global_array, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_array_set(&global_array, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_DEL) {
			ret = kvs_array_del(&global_array, key, (int)key_len);
			if (ret > 0) ret = 0;
		}
		break;

	case PERSIST_ENGINE_RBTREE:
		if (op == PERSIST_OP_SET) {
			ret = kvs_rbtree_set(&global_rbtree, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_rbtree_mod(&global_rbtree, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_MOD) {
			ret = kvs_rbtree_mod(&global_rbtree, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_rbtree_set(&global_rbtree, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_DEL) {
			ret = kvs_rbtree_del(&global_rbtree, key, (int)key_len);
			if (ret > 0) ret = 0;
		}
		break;

	case PERSIST_ENGINE_HASH:
		if (op == PERSIST_OP_SET) {
			ret = kvs_hash_set(&global_hash, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_hash_mod(&global_hash, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_MOD) {
			ret = kvs_hash_mod(&global_hash, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_hash_set(&global_hash, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_DEL) {
			ret = kvs_hash_del(&global_hash, key, (int)key_len);
			if (ret > 0) ret = 0;
		}
		break;

	case PERSIST_ENGINE_SKIPLIST:
		if (op == PERSIST_OP_SET) {
			ret = kvs_skiplist_set(&global_skiplist, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_skiplist_mod(&global_skiplist, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_MOD) {
			ret = kvs_skiplist_mod(&global_skiplist, key, (int)key_len, value, (int)value_len);
			if (ret > 0) ret = kvs_skiplist_set(&global_skiplist, key, (int)key_len, value, (int)value_len);
		} else if (op == PERSIST_OP_DEL) {
			ret = kvs_skiplist_del(&global_skiplist, key, (int)key_len);
			if (ret > 0) ret = 0;
		}
		break;

	default:
		return -1;
	}

	return (ret < 0) ? -1 : 0;
}

什么是 io_uring?

io_uring 是 Linux 提供的一种高性能异步 I/O 接口。它允许你提交多个 I/O 操作(比如读文件、写文件、网络收发等),然后内核高效地完成这些操作并通知你结果。

传统方式(如 readwrite)是同步的:你调用 write 后程序会阻塞,直到数据写完才返回。而 io_uring 采用提交队列完成队列的模型:

  • 提交队列(SQ):你把要做的 I/O 操作(称为 SQE,Submission Queue Entry)放入这个队列。

  • 完成队列(CQ):内核完成操作后,会把结果(称为 CQE,Completion Queue Entry)放入这个队列,你可以从中取出结果。

它的好处是:可以一次性提交多个 I/O 请求,减少系统调用开销,并且支持异步操作,适合高性能服务器。

我们的函数 persist_write_fd_by_uring 就是利用 io_uring 将整个缓冲区 buf 的内容写入文件描述符 fd,并在最后调用 fsync 确保数据真正落盘。

SAVE指令:使用到io_uring。之前的保存是遍历每个引擎,然后挨个存储。使用iouring后,只需要把任务存储任务提交到sqe队列里,内核会自动完成。节省大量时间。

cpp 复制代码
/**
 * 使用io_uring实现全量持久化
 * static:函数仅在本文件内使用。

	int fd:已打开的文件描述符,要写入的目标文件。

	const char *buf:要写入的数据缓冲区。

	size_t len:要写入的字节数。

	返回值:成功返回 0,失败返回 -1。

	函数的目标是把 buf 中的 len 个字节完整地写入 fd,并且保证数据刷到磁盘上(通过最后 fsync)
 */
static int persist_write_fd_by_uring(int fd, const char *buf, size_t len){
	//声明一个 io_uring 结构体,用来管理整个 I/O 队列。
	struct io_uring ring;
	// 记录当前已经写入的字节数,后面会用到。
	size_t offset = 0;

	/**
	 * 第一个参数 8:提交队列的深度(entries),即最多可以同时有多少个未完成的 I/O 请求。这里设为 8。

		第二个参数 &ring:指向 ring 结构体的指针。

		第三个参数 0:标志位,这里不使用特殊标志。

		如果初始化失败(返回值不为 0),函数返回 -1。
	 */
	if(io_uring_queue_init(8, &ring, 0) != 0){
		return -1;
	}

	/**
	 * 由于一次 write 可能不能写完所有数据,所以需要循环,每次写入一部分
	 * offset < len:只要还没写完,就继续循环。
	 */
	while(offset < len){
		//chunk = len - offset:剩余还没写入的字节数。
		size_t chunk = len - offset;
		//sqe:指向提交队列
		//cqe:指向完成队列
		struct io_uring_sqe *sqe = NULL;
		struct io_uring_cqe *cqe = NULL;

		/**
		 * 如果剩余数据超过 1 MB(1u << 20 即 2^20 = 1048576 字节),则本次只写入 1 MB
		 * 这样做的目的是避免一次提交过大的写请求,可能超过内核限制,也方便管理
		 */
		if(chunk > (1u << 20)){
			chunk = (1u << 20);
		}

		//io_uring_get_sqe 从 ring 的提交队列中取一个空闲的 SQE。
		sqe = io_uring_get_sqe(&ring);
		//如果返回 NULL,说明提交队列已满,需要先提交已有的请求。
		if(!sqe){
			if(io_uring_submit(&ring) < 0){
				io_uring_queue_exit(&ring);
				return -1;
			}

			continue;
		}

		/**
		 * io_uring_prep_write 设置这个 SQE 为"写文件"操作。
		 * sqe:要设置的提交队列条目。
		 * fd:文件描述符。
		 * buf + offset:源数据地址(当前要写入的数据起始位置)
		 * chunk:要写入的字节数
		 * (off_t)offset:文件偏移量。意思是从文件的这个位置开始写。因为是顺序写,
		 * 所以偏移量就是已经写入的字节数 offset。
		 */
		io_uring_prep_write(sqe, fd, buf + offset, chunk, (off_t)offset);

		/**
		 * 将当前设置的 SQE 提交给内核执行写操作。
		 * 如果提交失败,清理并返回 -1。
		 */
		if(io_uring_submit(&ring) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		/**
		 * 等待写操作完成
		 * io_uring_wait_cqe 等待一个完成事件(CQE)。它会阻塞直到有一个 I/O 操作完成
		 * 完成后,cqe 指向该完成条目
		 * 如果等待出错,清理并返回 -1。
		 */
		if(io_uring_wait_cqe(&ring, &cqe) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		/**
		 * cqe->res 保存了 I/O 操作的结果。对于写操作,返回值是实际写入的字节数。
		 * 如果返回值 <= 0,表示写入失败(0 或负值),则进行清理并返回 -1
		 */
		if(cqe->res <= 0){
			io_uring_cqe_seen(&ring, cqe);
			io_uring_queue_exit(&ring);
			return -1;
		}

		/**
		 * 更新已写入字节数
		 * offset 加上本次实际写入的字节数
		 * io_uring_cqe_seen 标记这个 CQE 已被处理,内核可以回收它的资源。
		 * 循环回到 while(offset < len) 继续写入剩余数据。
		 */
		offset += (size_t)cqe->res;
		io_uring_cqe_seen(&ring, cqe);
	}

	/**
	 * 最后执行 fsync(确保数据落盘)
	 * 写完所有数据后,文件内容可能还在操作系统的缓存中,没有真正写入磁盘。
	 * 所以需要调用 fsync 强制刷盘。
	 */
	{
		/**
		 * 再次获取一个 SQE,如果获取失败则清理并返回 -1。
		 */
		struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
		struct io_uring_cqe *cqe = NULL;

		if(!sqe){
			io_uring_queue_exit(&ring);
			return -1;
		}

		/**
		 * io_uring_prep_fsync 设置这个 SQE 为 fsync 操作。
		 * 	fd:要同步的文件描述符
		 * 	最后一个参数 0 是 fsync 的 flags,通常传 0
		 */
		io_uring_prep_fsync(sqe, fd, 0);

		//提交 fsync 请求。
		if(io_uring_submit(&ring) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		//等待 fsync 完成。
		if(io_uring_wait_cqe(&ring, &cqe) < 0){
			io_uring_queue_exit(&ring);
			return -1;
		}

		/**
		 * 检查 fsync 结果,如果返回值 < 0 表示失败,清理并返回 -1。
		 * 成功则标记 CQE 已处理。
		 */
		if(cqe->res < 0){
			io_uring_cqe_seen(&ring, cqe);
			io_uring_queue_exit(&ring);
			return -1;
		}

		io_uring_cqe_seen(&ring, cqe);
	}
	/**
	 * o_uring_queue_exit 释放 io_uring 相关的资源。
	 * 返回 0 表示整个写入和同步过程成功。
	 */
	io_uring_queue_exit(&ring);
	return 0;
}

总结

这个函数展示了如何使用 io_uring 进行文件的异步写入和同步:

  1. 初始化 io_uring

  2. 循环分块(每次最多 1 MB)将数据写入文件,每块都提交写请求并等待完成。

  3. 写完后,发起 fsync 请求,确保数据落到磁盘。

  4. 释放资源并返回结果。

使用 io_uring 的好处是能更高效地提交多个 I/O 操作,但在这里我们实际上是同步地提交并等待每个请求,所以性能提升有限。不过它至少演示了 io_uring 的基本用法,也为后续优化(比如批量提交多个块)打下了基础。

加载数据:

无论是全量还是增量数据,加载数据的时候,都使用了mmap。

什么是 mmap?

mmap 是一种内存映射文件 的技术。它把磁盘上的文件直接"映射"到进程的虚拟内存地址空间中。映射后,你可以像操作普通内存数组一样读写文件内容,而不需要手动 malloc 一块缓冲区,再用 read 把文件内容读进去。

cpp 复制代码
/*
 * 加载增量日志。
 *
 * 启动恢复时的顺序应该是:
 *   1. persist_load_all("kvstore.data")
 *      先恢复上一次全量快照
 *
 *   2. persist_load_increment("kvstore.aof")
 *      再把快照之后发生过的写操作重新执行一遍
 * 		整体逻辑:打开文件 → 获取大小 → 映射到内存 → 调用解析函数处理内存块 → 解除映射并关闭文件。
 */
int persist_load_increment(const char *path)
{
	if(!path) return -1;

	/**
	 * st:用于存放文件的状态信息(如大小)。
	 * map:指向映射后的内存区域,初始为 NULL。
	 * ret:函数最终返回值,初始为 -1,后续根据情况修改。
	 */
	int fd = -1;
	struct stat st;
	void *map = NULL;
	int ret = -1;

	/**
	 * open(path, O_RDONLY):以只读方式打开文件。
	 * 这里返回 0 而不是 -1,是因为没有增量日志文件并不是错误,而是正常情况
	 * (比如系统第一次启动,还没有写 AOF 文件)。
	 * 所以返回 0 表示"没有数据可加载",让上层继续运行
	 */
	fd = open(path, O_RDONLY);
	if(fd < 0) return 0;

	/**
	 * fstat(fd, &st):获取文件状态信息,存入 st 结构体
	 * st.st_size 就是文件的大小(字节数)。
	 * 如果 fstat 失败,说明出现错误,关闭文件并返回 -1。
	 * 
	 */
	if(fstat(fd, &st) < 0){
		close(fd);
		return -1;
	}

	/**
	 * 如果文件大小为 0,说明没有内容,直接关闭文件并返回 0(成功,但没数据)
	 * 这是一种边界情况,避免后面 mmap 映射一个空文件导致问题。
	 */
	if(st.st_size == 0){
		close(fd);
		return 0;
	}

	/**
	 * mmap 是一种内存映射文件的技术。它把磁盘上的文件直接"映射"到进程的虚拟内存地址空间中。
	 * 映射后,可以像操作普通内存数组一样读写文件内容,而不需要手动 malloc 一块缓冲区,
	 * 再用 read 把文件内容读进去。
	 * 第一个参数 NULL:表示让内核自动选择映射的起始地址。我们通常传 NULL,让系统决定。

		第二个参数 (size_t)st.st_size:要映射的字节数,这里就是整个文件的大小。

		第三个参数 PROT_READ:映射区域的保护权限。PROT_READ 表示只读,我们只需要读取文件内容,不修改。

		第四个参数 MAP_PRIVATE:映射类型。MAP_PRIVATE 表示私有映射,对映射区域的修改不会写回文件,也不会影响其他进程。这里只读,所以无所谓。

		第五个参数 fd:要映射的文件描述符。

		第六个参数 0:文件偏移量,从文件的哪个位置开始映射。0 表示从文件开头开始。

		返回值:
			成功返回映射区域的起始地址,类型是 void *。

			失败返回 MAP_FAILED(通常定义为 (void *)-1)。

			所以 if(map == MAP_FAILED) 表示映射失败,此时关闭文件并返回 -1。
		好处:
			避免手动分配大块内存和读取文件,操作系统负责将文件内容按需加载到物理内存。

			访问映射区域就像访问数组一样,代码简洁高效。

			对于大文件,mmap 可以做到按需分页加载,节省内存
	 */
	map = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	if(map == MAP_FAILED){
		close(fd);
		return -1;
	}

	/**
	 * 将映射后的内存地址 map 强制转换为 const char *,并传入文件大小。

		这个函数负责在内存中解析 AOF 帧格式或旧版文本格式,逐条执行命令恢复数据。

		返回值保存在 ret 中,作为当前函数的最终返回值。
	 */
	ret = persist_load_increment_from_mem((const char *)map, (size_t)st.st_size);

	/**
	 * munmap(map, size):解除内存映射,释放虚拟地址空间。
	 * return ret:返回解析函数的执行结果。
	 */
	munmap(map, (size_t)st.st_size);
	close(fd);

	return ret;
}

整体流程总结

  1. 打开文件,如果不存在则返回 0(正常)。

  2. 获取文件大小,空文件返回 0。

  3. mmap 将文件内容映射到内存。

  4. 调用 persist_load_increment_from_mem 在内存中解析并恢复数据。

  5. 解除映射,关闭文件,返回结果。


为什么用 mmap?

  • 简化代码:不需要手动分配缓冲区和循环读取文件。

  • 性能好:对于大文件,mmap 可以避免多次系统调用,并且利用操作系统的页缓存。

  • 方便直接解析二进制帧或文本行。

相关推荐
其实防守也摸鱼40 分钟前
教育信息技术应用创新---基础软件信息赛
运维·服务器·数据库·github·copilot
Gauss松鼠会1 小时前
【GaussDB】GaussDB 组件、节点和AZ故障仲裁与切换流程
运维·服务器·数据库·gaussdb
myy-learn1 小时前
32 SQLITE数据库
jvm·数据库·sqlite
JavaPub-rodert1 小时前
Redis 和 MySQL 如何保证数据一致性?从业务方案到底层原理完整讲解
数据库·redis·mysql
大牧师2 小时前
TypeORM 学习教程
数据库·sql·学习·node.js·orm·nest.js·typeorm
IvorySQL2 小时前
PostgreSQL 日报| PGQ 功能发现严重缺陷(9 月 4 日)
数据库·postgresql
后台模板学习2 小时前
用 IM 即时聊天项目一次讲清消息去重算法的踩坑与解决方案
java·数据库·spring
imbackneverdie2 小时前
国内有哪些比较全面的生物医学相关数据库?
大数据·数据库·人工智能·ai·信息可视化·aigc·科研
数字智核2 小时前
2026昆山工厂空压机突然停机怎么办?找谁抢修
服务器·网络·数据库