trie树 省内存trie树 URL匹配

   业务背景:MySQL数据库中有一份十万左右的域名白名单数据。一般不会变动。

   业务需求:查询一个URL的域名是否在白名单中。

   业务要求:占用内存小,高效,达到1s几百万。

   以下性能测试环境均基于:

       内存:16G

       CPU8  Intel(R) Xeon(R) CPU E5-14100 @ 2.80GHz

一、直接查询MySQL

   没有做性能调查,但是肯定达不到业务的要求。

二、C++ set容器

   将白名单数据全部读入set容器中,占用11M左右内存,内存达到性能要求,但是性能呢?

   当时我循环遍历MySQL中的白名单数据进行匹配set容器中的数据,没有做记录模糊记得十万左右的数据大约1s中左右,这还没有考虑待查URL中包含二级、三级域名的情况,所以这种直接哈希也达不到业务的性能要求。

三、trie树

   以上两种方法不行我就想到了trie树。trie树也称字典树,其效率很高,利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高,但是其高效率是以空间为代价的(起初不觉得会占用多大空间,但是……)。我将白名单数据从后往前存储到trie树中,正好符合域名匹配不要考虑二级域名、三级域名的问题,后面我会做一个优化处理。

   借用百度的图片来说明一下trie树:

《trie树 省内存trie树 URL匹配》

   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);
}




点赞