目录
- 前言
- 一、设计思路
- 二、文件存储格式
- 三、数据库操作
- 3.1. 数据库结构
- 3.2. 数据库初始化
- 3.3. 插入
- 3.4. 删除
- 3.5. 修改
- 3.6. 查询
- 3.7. 清空
- 四、示例代码
前言
之前介绍了C语言实现AVL树, 本文是对AVL树的一个简单应用,在资源偏紧张的硬件设备中可以使用,如资源足够还是建议使用sqlite。
先把仓库地址贴一下:https://gitee.com/sauryniu/file-database
一、设计思路
简单的实现文件数据库需要考虑两个方面,一个是数据的持久存储,另一个是数据的快速操作(增删改查)。关于持久存储,我们可以把数据存储到文件中;快速操作的话则需要借助算法实现,这里选择在内存中建立AVL树,相对二叉树来说性能比较均衡,又不像其他树那样复杂。
二、文件存储格式
- 文件头:文件数据库头部,用户自定义大小和数据结构,可用于保存版本信息之类的数据。
- 数据块个数:类型为int,最大4字节,取决于操作系统位数。
- 数据块:用户真正保存的数据内容,自定义大小和数据结构,但是需要统一。
三、数据库操作
3.1. 数据库结构
封装结构对象,方便操作。
struct _file_db
{
file_db_t* _this;
void* _private_;
/*
@func:
添加元素到文件数据库中
@para:
db : 文件数据库指针
ele : 要被添加的元素指针
@return:
int : < 0 : 失败, 0 : 成功
@note:
ele 传入的建议是非指针变量的地址,如果使用的是指向动态内存的指针,则用完后需自行释放资源
*/
int (*add)(file_db_t* db, void *ele);
/*
@func:
通过键值删除指定键值对应的元素
@para:
db : 文件数据库指针
key : 元素对应的键值
@return:
int : < 0 : 失败, 0 : 成功
@note:
none.
*/
int (*del)(file_db_t* db, int key);
/*
@func:
根据键值编辑指定的元素
@para:
db : 文件数据库指针
key : 元素对应的键值
ele : 目标元素,将替换给定键值所对应的元素的值
@return:
int : < 0 : 失败, 0 : 成功
@note:
ele 参数通过用户传入的获取键值的函数计算得到的键值需要和本函数传入键值 key 一致
*/
int (*edit)(file_db_t* db, int key, void *ele);
/*
@func:
根据键值查询文件数据库中的元素
@para:
db : 文件数据库指针
key : 元素的键值
@return:
void* : NULL 查询失败, other 查询到的元素的指针
@note:
none.
*/
void* (*query)(file_db_t* db, int key);
/*
@func:
写入文件数据库的文件头
@para:
db : 文件数据库指针
head : 写入的文件头指针
@return:
int : < 0 : 失败, 0 : 成功
@note:
none.
*/
int (*write_head)(file_db_t* db, void* head);
/*
@func:
读取文件数据库的文件头到指定内存中
@para:
db : 文件数据库指针
head : 读出的文件头指针
@return:
int : < 0 : 失败, 0 : 成功
@note:
none.
*/
int (*read_head)(file_db_t* db, void* head);
/*
@func:
返回文件数据库中的元素个数
@para:
db : 文件数据库指针
@return:
int : < 0 : 失败, other : 元素个数
@note:
none.
*/
int (*size)(file_db_t* db);
/*
@func:
遍历文件数据库中的所有内容,然后使用传入的函数指针对每个元素执行操作
@para:
db : 文件数据库指针
visit : 对元素操作的函数指针
@return:
int : < 0 : 失败, 0 : 成功
@note:
none.
*/
int (*traverse)(file_db_t* db, void (*visit)(void*));
/*
@func:
清除文件数据库内容,但是保存文件头
@para:
db : 文件数据库指针
@return:
int : -1 : 失败, 0 : 成功
@note:
【重要】此函数不能与 file_db_destory 同时使用
*/
int (*clear)(file_db_t* db);
/*
@func:
并释放文件数据库相关动态内存
@para:
db : 文件数据库指针
@return:
int : -1 : 失败, 0 : 成功
@note:
【重要】此函数不能与 file_db_destory 同时使用
*/
int (*free)(file_db_t* db);
/*
@func:
销毁数据库文件并释放相关动态内存
@para:
db : 文件数据库指针
@return:
int : -1 : 失败, 0 : 成功
@note:
【重要】此函数不能与 file_db_free 同时使用
*/
int (*destory)(file_db_t* db);
};
3.2. 数据库初始化
初始化数据库,传入数据库路径,以及其他相关信息,如果文件不存在会创建
/*
@func:
使用指定的文件初始化文件数据库
@para:
path : 存放数据的文件所在路径
head_size : 头大小
data_size : 数据块大小
pf_hash_func :从数据块中获取key的哈希函数
head : 头数据
@return:
file_db_t* : 文件数据库指针
@note:
如果不再使用该数据库需要调用销毁函数释放内存
【重要】保存的数据的数据类型大小必须是固定的
*/
file_db_t* file_db_init(const char* path, int head_size, int data_size, int (*pf_hash_func)(void *), void* head);
3.3. 插入
插入数据块时,会先查询数据库中key是否存在,不存在才插入。插入时,先把数据块写到文件尾部,然后更新数据块数量,最后把数据更新到AVL树中。
/*
@func:
添加元素到文件数据库中
@para:
db : 文件数据库指针
ele : 要被添加的元素指针
@return:
int : < 0 : 失败, 0 : 成功
-4 :内部错误
-5 :key已存在
-6 :内存不足
-7 :写入文件异常
@note:
ele 传入变量如果使用的是指向动态内存的指针,则用完后需自行释放资源
*/
static int file_db_add(file_db_t* db, void* ele)
{
file_db_private_t* _this = get_private_member(db);
if(NULL == _this)
return -4;
if(NULL != _this->m_tree->query_by_key(_this->m_tree->_this, _this->pf_get_ele_key(ele)))
return -5;
int res_code = 0;
file_db_record_t* record_data = (file_db_record_t*)malloc(sizeof(file_db_record_t));
if(NULL == record_data)
return -6;
void* ele_memory = malloc(_this->m_data_size);
if(NULL == ele_memory)
{
free(record_data);
return -6;
}
memcpy(ele_memory, ele, _this->m_data_size);
pthread_mutex_lock(&_this->m_file_db_mutex);
FILE *db_fp = fopen(_this->m_path, "rb+");
if(NULL == db_fp)
{
FILE_DB_LOG_DEBUG("open db file error");
res_code = -7;
goto RUNTIME_ERROR;
}
fseek(db_fp, 0, SEEK_END);
record_data->offset = ftell(db_fp);
record_data->db = db;
int write_len = fwrite(ele_memory, 1, _this->m_data_size, db_fp);
if(write_len != _this->m_data_size)
{
FILE_DB_LOG_DEBUG("write ele error, write[%d] but [%d]", _this->m_data_size, write_len);
fclose(db_fp);
res_code = -7;
goto RUNTIME_ERROR;
}
_this->m_data_cnt++;
FILE_DB_LOG_DEBUG("write cnt %d, key %d", _this->m_data_cnt, _this->pf_get_ele_key(ele));
fseek(db_fp, _this->m_head_size, SEEK_SET);
write_len = fwrite(&_this->m_data_cnt, 1, sizeof(int), db_fp);
if(write_len != sizeof(int))
{
FILE_DB_LOG_DEBUG("write cnt error");
_this->m_data_cnt--;
fclose(db_fp);
truncate(_this->m_path, record_data->offset);
res_code = -9;
goto RUNTIME_ERROR;
}
fflush(db_fp);
fclose(db_fp);
record_data->ele = ele_memory;
pthread_mutex_unlock(&_this->m_file_db_mutex);
return _this->m_tree->add(_this->m_tree->_this, (void *)record_data);
RUNTIME_ERROR:
free(ele_memory);
free(record_data);
pthread_mutex_unlock(&_this->m_file_db_mutex);
return res_code;
}
3.4. 删除
删除数据库块时,会先找到该数据块在文件中的位置,把文件尾部的第一个数据块复制到待删除的数据块所在位置,然后把数据块数量减1,最后在AVL树中把该数据块删除。
/*
@func:
通过键值删除指定键值对应的元素
@para:
db : 文件数据库指针
key : 元素对应的键值
@return:
int : < 0 : 失败, 0 : 成功
@note:
none.
*/
static int file_db_del(file_db_t* db, int key)
{
file_db_private_t* _this = get_private_member(db);
if(NULL == _this)
{
FILE_DB_LOG_DEBUG("Filedatabase error!");
return -2;
}
file_db_record_t* record_data = _this->m_tree->query_by_key(_this->m_tree->_this, key);
if(NULL == record_data)
{
FILE_DB_LOG_DEBUG("No such element. Del fail!");
return -3;
}
pthread_mutex_lock(&_this->m_file_db_mutex);
FILE* db_fp = fopen(_this->m_path, "rb+");
if(NULL == db_fp)
{
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -4;
}
if(record_data->offset < _this->m_head_size + sizeof(int) + _this->m_data_size * (_this->m_data_cnt - 1))
{
fseek(db_fp, 0, SEEK_END);
fseek(db_fp, -_this->m_data_size, SEEK_END);
void* buff = malloc(_this->m_data_size);
if(1 != fread(buff, _this->m_data_size, 1, db_fp))
{
FILE_DB_LOG_DEBUG("Read tail element error! file position %ld", ftell(db_fp));
free(buff);
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -5;
}
fseek(db_fp, record_data->offset, SEEK_SET);
if(1 != fwrite(buff, _this->m_data_size, 1, db_fp))
{
FILE_DB_LOG_DEBUG("Write tail element error!");
free(buff);
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -6;
}
free(buff);
}
_this->m_data_cnt--;
fseek(db_fp, _this->m_head_size, SEEK_SET);
int write_len = fwrite(&_this->m_data_cnt, sizeof(int), 1, db_fp);
if(write_len != 1)
{
FILE_DB_LOG_DEBUG("write cnt error");
_this->m_data_cnt++;
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -7;
}
fclose(db_fp);
truncate(_this->m_path, _this->m_head_size + sizeof(int) + _this->m_data_cnt * _this->m_data_size );
pthread_mutex_unlock(&_this->m_file_db_mutex);
return _this->m_tree->del_node_by_key(_this->m_tree->_this, key);
}
3.5. 修改
修改数据块,需要传入数据块对应key值和数据块内容,定位到文件中数据块位置后更新,最后把AVL树中的数据更新一下就可以。
/*
@func:
根据键值编辑指定的元素
@para:
db : 文件数据库指针
key : 元素对应的键值
ele : 目标元素,将替换给定键值所对应的元素的值
@return:
int : < 0 : 失败, 0 : 成功
@note:
ele 参数通过用户传入的获取键值的函数计算得到的键值需要和本函数传入键值 key 一致
*/
static int file_db_edit(file_db_t* db, int key, void *ele)
{
file_db_private_t* _this = get_private_member(db);
if(NULL == _this || NULL == ele) return -1;
if(key != _this->pf_get_ele_key(ele))
{
FILE_DB_LOG_DEBUG("Edit error, Key value and element do not match");
return -1;
}
pthread_mutex_lock(&_this->m_file_db_mutex);
file_db_record_t* record_data = (file_db_record_t*)(_this->m_tree->query_by_key(_this->m_tree->_this, key));
if(NULL == record_data)
{
FILE_DB_LOG_DEBUG("Edit error, Query data error");
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -2;
}
memset(record_data->ele, 0, _this->m_data_size);
memcpy(record_data->ele, ele, _this->m_data_size);
FILE_DB_LOG_DEBUG("query key[%d], ele key[%d], get key[%d]", key, _this->pf_get_ele_key(ele), _this->pf_get_ele_key(record_data->ele));
FILE* db_fp = fopen(_this->m_path, "rb+");
if(NULL == db_fp)
{
FILE_DB_LOG_DEBUG("Edit error, File error");
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -3;
}
fseek(db_fp, record_data->offset, SEEK_SET);
if(fwrite(record_data->ele, _this->m_data_size, 1, db_fp) != 1)
{
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
FILE_DB_LOG_DEBUG("Edit element, write new error!");
return -4;
}
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
return 0;
}
3.6. 查询
查询比较简单,直接传入key,在AVL树中查询即可。
/*
@func:
根据键值查询文件数据库中的元素
@para:
db : 文件数据库指针
key : 元素的键值
@return:
void* : NULL 查询失败, other 查询到的元素的指针
@note:
none.
*/
static void* file_db_query(file_db_t* db, int key)
{
file_db_private_t* _this = get_private_member(db);
if(NULL == _this)
{
FILE_DB_LOG_DEBUG("_this is NULL");
return NULL;
}
file_db_record_t* record_data = (file_db_record_t*)(_this->m_tree->query_by_key(_this->m_tree->_this, key));
if(NULL == record_data)
{
FILE_DB_LOG_DEBUG("record_data is NULL");
return NULL;
}
return record_data->ele;
}
3.7. 清空
清空时,直接把文件截断,然后把数据块数量写入0,最后清空所有AVL树节点即可。
/*
@func:
清除文件数据库内容,但是保存文件头
@para:
db : 文件数据库指针
@return:
int : -1 : 失败, 0 : 成功
@note:
【重要】此函数不能与 file_db_destory 同时使用
*/
static int file_db_clear(file_db_t* db)
{
file_db_private_t* _this = get_private_member(db);
if(NULL == _this) return -1;
int cnt = 0;
pthread_mutex_lock(&_this->m_file_db_mutex);
FILE* db_fp = fopen(_this->m_path, "rb+");
if(NULL == db_fp)
{
pthread_mutex_unlock(&_this->m_file_db_mutex);
return -1;
}
fseek(db_fp, _this->m_head_size, SEEK_SET);
if(1 != fwrite(&cnt, sizeof(int), 1, db_fp))
{
fclose(db_fp);
pthread_mutex_unlock(&_this->m_file_db_mutex);
FILE_DB_LOG_DEBUG("[file_db_clear] : write cnt error");
return -2;
}
fclose(db_fp);
truncate(_this->m_path, _this->m_head_size + sizeof(int));
pthread_mutex_unlock(&_this->m_file_db_mutex);
_this->m_data_cnt = 0;
_this->m_tree->clear_node(_this->m_tree->_this);
return 0;
}
四、示例代码
示例代码使用名称为 example.db 的文件作为数据库文件,采用随机数的方式最大写入20个元素,通常会小于20个,因为会有一部分重复的,然后进行删、改、查的操作。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "FileDatabase.h"
#define EXAMPLE_FILE_DB "example.db"
#define FILE_DB_SIZE 20
static file_db_t* file_db;
typedef struct _db_head
{
int version;
char reserve[252];
}db_head_t;
typedef struct _db_data
{
int key;
char value[60];
}db_data_t;
static int get_key(void *ele)
{
if(NULL == ele)
{
return -1;
}
db_data_t* record = (db_data_t*)ele;
return record->key;
}
static void visit(void* ele)
{
if(NULL == ele)
{
printf("ele null!!!");
}
db_data_t* data = (db_data_t*)ele;
printf("[visit] key[%d]->value[%s]\r\n", data->key, data->value);
}
int main(void)
{
db_head_t head;
head.version = 1;
file_db = file_db_init(EXAMPLE_FILE_DB, sizeof(db_head_t), sizeof(db_data_t), get_key, &head);
if(NULL == file_db)
{
printf("create db error\r\n");
return -1;
}
db_data_t element;
int cnt = 0;
srand(time(NULL));
int key[FILE_DB_SIZE];
int index = 0;
for(int i = 0; i < FILE_DB_SIZE; ++i)
{
int num = rand()%FILE_DB_SIZE;
key[index++] = num;
//printf("index :%d -> key %d\r\n", index, key[index]);
element.key = num;
memset(element.value, 0, 60);
sprintf(element.value, "element%d", num);
if(0 == file_db->add(file_db->_this, &element))
{
cnt++;
}
}
printf("Add cnt %d, total %d\r\n", cnt, file_db->size(file_db->_this));
int random_key = rand()%FILE_DB_SIZE;
printf("Del key[%d], res[%d]\r\n", key[random_key], file_db->del(file_db->_this, key[random_key]));
random_key = rand()%FILE_DB_SIZE;
memset(element.value, 0, 60);
sprintf(element.value, "edit%d", key[random_key]);
element.key = key[random_key];
printf("Edit key[%d], res[%d]\r\n", key[random_key], file_db->edit(file_db->_this, key[random_key], &element));
random_key = rand()%FILE_DB_SIZE;
db_data_t* ele_query = (db_data_t*) file_db->query(file_db->_this, key[random_key]);
if(NULL == ele_query)
{
printf("ele_query null\r\n");
}
else
{
printf("Query key[%d], value[%s]\r\n", ele_query->key, ele_query->value);
}
file_db->traverse(file_db->_this, visit);
file_db->clear(file_db->_this);
file_db->free(file_db->_this);
//file_db->destory(file_db->_this);
return 0;
}