业务背景:MySQL数据库中有一份十万左右的域名白名单数据。一般不会变动。
业务需求:查询一个URL的域名是否在白名单中。
业务要求:占用内存小,高效,达到1s几百万。
以下性能测试环境均基于:
内存:16G
CPU:8 Intel(R) Xeon(R) CPU E5-14100 @ 2.80GHz
一、直接查询MySQL
没有做性能调查,但是肯定达不到业务的要求。
二、C++ set容器
将白名单数据全部读入set容器中,占用11M左右内存,内存达到性能要求,但是性能呢?
当时我循环遍历MySQL中的白名单数据进行匹配set容器中的数据,没有做记录模糊记得十万左右的数据大约1s中左右,这还没有考虑待查URL中包含二级、三级域名的情况,所以这种直接哈希也达不到业务的性能要求。
三、trie树
以上两种方法不行我就想到了trie树。trie树也称字典树,其效率很高,利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高,但是其高效率是以空间为代价的(起初不觉得会占用多大空间,但是......)。我将白名单数据从后往前存储到trie树中,正好符合域名匹配不要考虑二级域名、三级域名的问题,后面我会做一个优化处理。
借用百度的图片来说明一下trie树:
trie的基本性质:根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。都满足业务要求。
先看一下我初始定义的节点结构体:
typedef struct _whitelist_tree_node_ {
uint8_t white_type; //是否是白名单,代表下一步执行的动作
struct _whitelist_tree_node_ *childs[128]; //子节点的指针
} whitelist_tree_node;
我这里在节点中没有定义保存节点值char变量,因为本业务用不到,因root节点不存内容,从root节点开始判断是否含有每个字符‘c’,只需判断该节点的childs[c] 是否等于NULL。white_type是我的一个属性,根据他我就可以不用判断二级、三级域名,在初始化树的时候赋值。他的值我用枚举定义的:
enum {
WHITELIST_UNDONE = 0, //该白名单URL未结束
WHITELIST_CONTINUE, //白名单URL第一个字符,需要继续向前查找
WHITELIST_DONE //匹配白名单,不需继续查
};
写好算法之后,开始测试。初始化之后,我一看占用内存,我擦,我惊呆了了,800M左右,这还了得,赶紧优化。
肯定是childs[128]占用的内存。所以首先考虑域名的合法值,域名的合法字符为 '0-9'、'.'、'-'、'A-z',不区分大小写,这个好,因'-'和'z'区间跨度太大,所以将小写转为大写,降低区间跨度,又因最小值为'-'=45,所以这里操作的下标统一减去45操作。OK,整理后的结构体是:
typedef struct _whitelist_tree_node_ {
uint8_t white_type; //是否是白名单,代表下一步执行的动作
struct _whitelist_tree_node_ *childs[46]; //子节点的指针
} whitelist_tree_node;
抱着希望再测试内存,我擦,还将进200M,不行,继续优化。
考虑这里用的静态数组,而且域名还算比较规则的,肯定浪费了不少的空间,并且我这白名单只需程序运行的时候初始化一次,中途不会修改树的结构,所以我考虑用动态申请数组来存储子节点,但是这样我怎么利用“只需判断该节点的childs[c] 是否等于NULL”这个快速的优点,难道循环?肯定不行,那么就得记录子节点中字母的插入的顺序,这个好记因子节点最多46个,也就是说子节点数组的下标最大45,用一个字节就可以表示了,OK,整理之后的节点结构体:
typedef struct _whitelist_tree_node_ {
uint8_t white_type; //匹配白名单是否结束,代表下一步执行的动作
uint8_t child_count; //该节点的子节点个数
uint8_t child_order[MAX_CHILDS_NUM];//子节点字母在childs的位置(起始位置认为1)
struct _whitelist_tree_node_ *childs;//子节点数组
} whitelist_tree_node;
这里多了一个child_count,就是为了好为child_order,并且记录动态数组的大小。
好了,再测试内存,43M,哈哈,总算可以用了,不容易啊。
那么性能呢,测试一下:
whitelist count: 99496
success num: 99496, failure num: 0
start: 1398762622.868052
stop: 1398762622.920456
time: 0.052404
待查10万URL全部是白名单的速率大约为0.05s,也是说1s约200万纯白名单的查询速度。
正常环境中肯定不全是白名单,测试1/10是白名单的100万数据,结果如下:
whitelist count: 99496
success num: 131922, failure num: 846934
start: 1398764581.688160
stop: 1398764581.956054
time: 0.267894
百万级别大约0.26s,也就是1s约400万,OK,达到性能要求。
综上,学任何一种算法都应该举一反三,结合自己的业务要求考虑算法会更好。下面是我完整的算法代码。或者下载附件查看,代码环境Linux。不对的地方请指正,谢谢。
头文件:
#define MIN_ASCII_VALUE 45
#define MAX_CHILDS_NUM 46
enum {
WHITELIST_UNDONE = 0, //该白名单URL未结束
WHITELIST_CONTINUE, //白名单URL第一个字符,需要继续向前查找
WHITELIST_DONE //匹配白名单,不需继续查
};
typedef struct _whitelist_tree_node_ {
uint8_t white_type; //匹配白名单是否结束,代表下一步执行的动作
uint8_t child_count; //该节点的子节点个数
uint8_t child_order[MAX_CHILDS_NUM]; //子节点字母在childs的位置(起始位置认为1)
struct _whitelist_tree_node_ *childs; //子节点数组
} whitelist_tree_node;
typedef struct _whitelist_tree_ {
whitelist_tree_node *root; //根节点
unsigned int whitelist_count; //URL白名单个数
} whitelist_tree, *PUrlWhiteListControl;
/**
* [InitWhiteListTree 初始化一颗空树]
* @param p_tree [白名单树结构,用完释放]
* @return [0: success, other: failure code]
*/
int InitWhiteListEmptyTree(whitelist_tree **p_tree);
/**
* [AddUrlToWhiteListTree 将URL添加到树中]
* @param url [URL]
* @param p_tree [白名单树指针]
* @return [description]
*/
int AddUrlToWhiteListTree(const char *url, whitelist_tree *p_tree);
/**
* [MatchWhiteLIst 查询是否匹配上白名单]
* @param beg_pos [待查URL起始位置,调用者保证有效]
* @param end_pos [待查URL结束位置,调用者保证有效]
* @param p_tree [白名单树结构]
* @return [0: match success, < 0: run failure code, > 0: not match]
*/
int MatchWhiteLIst(const char *beg_pos, const char *end_pos, whitelist_tree *p_tree);
/**
* [FreeWhiteListNode 释放节点内存]
* @param p_node [待释放节点]
*/
void FreeWhiteListNode(whitelist_tree_node *p_node);
/**
* [FreeWhiteListTree 释放整棵白名单树]
* @param p_tree [待释放树指针]
*/
void FreeWhiteListTree(whitelist_tree *p_tree);
c文件:
int InitWhiteListEmptyTree(whitelist_tree **p_tree)
{
if(p_tree == NULL) {
return ERROR_PARAMETER_IS_NULL;
}
*p_tree = (whitelist_tree *)malloc(sizeof(whitelist_tree));
if(*p_tree == NULL) {
return ERROR_MALLOC;
}
(*p_tree)->whitelist_count = 0;
(*p_tree)->root = (whitelist_tree_node *)malloc(sizeof(whitelist_tree_node));
if((*p_tree)->root == NULL) {
free(*p_tree);
*p_tree = NULL;
return ERROR_MALLOC;
}
memset((*p_tree)->root, 0, sizeof(whitelist_tree_node));
return 0;
}
int AddUrlToWhiteListTree(const char *url, whitelist_tree *p_tree)
{
if(p_tree == NULL || url == NULL) {
return ERROR_MALLOC;
}
whitelist_tree_node *p_node = p_tree->root;
whitelist_tree_node *p_new_childs = NULL;
//指向URL末尾,从后向前插
const char *end_pos = url + strlen(url);
//为空
if(end_pos <= url) {
return ERROR_URL_ILLEGAL;
}
while(--end_pos && end_pos >= url) {
int offset_pos = toupper(*end_pos)-MIN_ASCII_VALUE;
if(offset_pos < 0 || offset_pos >= MAX_CHILDS_NUM) {
return ERROR_URL_ILLEGAL;
}
//该子节点已经存在
if(p_node->child_order[offset_pos] != 0) {
//此处添加的是二级域名,以及域名已经是白名单
if(p_node->childs[p_node->child_order[offset_pos]-1].white_type == WHITELIST_DONE) {
return 0;
}
p_node = p_node->childs + p_node->child_order[offset_pos]-1;
continue;
}
++(p_node->child_count);
p_node->child_order[offset_pos] = p_node->child_count;
p_new_childs = (whitelist_tree_node *)malloc(p_node->child_count*sizeof(whitelist_tree_node));
if(p_new_childs == NULL) {
return ERROR_MALLOC;
}
memcpy(p_new_childs, p_node->childs, (p_node->child_count-1)*sizeof(whitelist_tree_node));
memset(p_new_childs + (p_node->child_count-1), 0, sizeof(whitelist_tree_node));
free(p_node->childs);
p_node->childs = p_new_childs;
p_node->childs[p_node->child_count-1].white_type = WHITELIST_UNDONE;
p_node = p_node->childs + p_node->child_count-1;
}
p_node->white_type = WHITELIST_CONTINUE;
//每个完整URL白名单追加一个结束点
int offset_pos = toupper('.')-MIN_ASCII_VALUE;
++(p_node->child_count);
p_node->child_order[offset_pos] = p_node->child_count;
p_new_childs = (whitelist_tree_node *)malloc(p_node->child_count*sizeof(whitelist_tree_node));
if(p_new_childs == NULL) {
return ERROR_MALLOC;
}
memcpy(p_new_childs, p_node->childs, (p_node->child_count-1)*sizeof(whitelist_tree_node));
memset(p_new_childs + (p_node->child_count-1), 0, sizeof(whitelist_tree_node));
free(p_node->childs);
p_node->childs = p_new_childs;
p_node->childs[p_node->child_count-1].white_type = WHITELIST_DONE;
return 0;
}
int MatchWhiteLIst(const char *beg_pos, const char *end_pos, whitelist_tree *p_tree)
{
if(beg_pos == NULL || end_pos == NULL || end_pos <= beg_pos) {
return ERROR_PARAMETER_IS_NULL;
}
whitelist_tree_node *p_node = p_tree->root;
while(--end_pos && end_pos >= beg_pos) {
int offset_pos = toupper(*end_pos)-45;
if(offset_pos < 0 || offset_pos >= 46) {
return ERROR_URL_ILLEGAL;
}
if(p_node->child_order[offset_pos] == 0) {
return NO_MATCH_URL_WHITE_LIST;
}
p_node = p_node->childs + p_node->child_order[offset_pos] - 1;
if(p_node->white_type == WHITELIST_DONE) {
return 0;
}
}
if(p_node->white_type == WHITELIST_UNDONE) {
return NO_MATCH_URL_WHITE_LIST;
}
return 0;
}
void FreeWhiteListNode(whitelist_tree_node *p_node)
{
//首先递归释放子节点
uint8_t i = 0;
for(; i < p_node->child_count; ++i) {
FreeWhiteListNode(p_node->childs + i);
}
free(p_node->childs);
}
void FreeWhiteListTree(whitelist_tree *p_tree)
{
FreeWhiteListNode(p_tree->root);
free(p_tree);
}