一.问题出发点
需要储存15亿级dives_id+500位标签数据 的标签系统数据,并实现200毫秒内的高并发查询 200毫秒不是还需要给其他处理留出时间,所以需要将查询时间压缩到30毫秒甚至更少,并且要承受住高并发处理。
二.数据库选择
已redis为基础进行优化分析
redis基础,redis属于内存储存,所以使用成本很大,但是查询效率十分高,单机就可以支持10万qps的并发量处理,当然需要增加redis service 节点
所以大数据redis可以支持特定场景下的高并发大数据处理查询 达到高速查询的效果
那么就要想办法优化redis存储结构,从以下几个点出发
1.从key的存储格式入手
2.从redis的key value储存结构入手
3.从统一格式减少数据碎片入手
4.从减少查询次数入手
三.优化
基础优化key储存结构
疑问:redis是怎么快速从大数据量中查询到key的
原文资料:https://blog.csdn.net/agangdi/article/details/21567199
key的储存结构
1、redis 中的每一个数据库,都由一个 redisDb 的结构存储。其中,redisDb.id 存储着 redis 数据库以整数表示的号码。redisDb.dict 存储着该库所有的键值对数据。redisDb.expires 保存着每一个键的过期时间。
2、当redis 服务器初始化时,会预先分配 16 个数据库(该数量可以通过配置文件配置),所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中。当我们选择数据库 select number 时,程序直接通过 redisServer.db[number] 来切换数据库。有时候当程序需要知道自己是在哪个数据库时,直接读取 redisDb.id 即可。
3、既然我们知道一个数据库的所有键值都存储在redisDb.dict中,那么我们要知道如果找到key的位置,就有必要了解一下dict 的结构了:
typedef struct dict {//特定于类型的处理函数dictType *type;
// 类型处理函数的私有数据void *privdata;
// 哈希表(2个)dictht ht[2];
// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行int rehashidx;
// 当前正在运作的安全迭代器数量int iterators;
} dict;
由上述的结构可以看出,redis的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而1号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以redis 中查找一个key,其实就是对进行该dict 结构中的 ht[0] 进行查找操作。
4、既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据key的哈希值找到该列表后,如果列表的长度大于1,那么我们需要遍历该链表来找到我们所查找的key。当然,一般情况下链表长度都为是1,所以时间复杂度可看作o(1)。
当redis拿到一个key 时,如果找到该key的位置。
了解了上述知识之后,我们就可以来分析redis如果在内存找到一个key了。
1、当拿到一个key后, redis 先判断当前库的0号哈希表是否为空,即:if (dict->ht[0].size == 0)。如果为true直接返回NULL。
2、判断该0号哈希表是否需要rehash,因为如果在进行rehash,那么两个表中者有可能存储该key。如果正在进行rehash,将调用一次_dictRehashStep方法,_dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动 rehash,这里不作赘述。
3、计算哈希表,根据当前字典与key进行哈希值的计算。
4、根据哈希值与当前字典计算哈希表的索引值。
5、根据索引值在哈希表中取出链表,遍历该链表找到key的位置。一般情况,该链表长度为1。
6、当 ht[0] 查找完了之后,再进行了次rehash判断,如果未在rehashing,则直接结束,否则对ht[1]重复345步骤。
我们的重点是了解到了redis key的储存形式,那么根据上述所说,以此来推论
1.当redis每保存一个key那么会储存一个完整的dict,当key值越多的时候,hash表也会膨胀,储存空间也会增大,
2.如果当key值过大会造成hash碰撞链表增长,那么需要去遍历链表增加时间复杂度变为o(n) <当然也不是完全的o(n)但是会有复杂度膨胀>所以当key值数量十分多的时候越多查询效率会相对降低虽然降低的不大
综上所述我们要想办法优化key值的数量
但是因为标签系统key值是32位MD5的设备号,所以都是一一对应的,单纯的key数量是无法减少的,如果减少会造成投放不准确。所以我们只能通过储存格式入手
redis储存格式key值没有变化,那么我们想办法从value值入手想办法减少key值
redis value值储存格式String、Hash 、List 、 Set 、 Ordered Set
hash结构的k-k-v格式有可能能符合减少k值的需求,那么来看一下hash结构得储存结构
资料连接:https://www.cnblogs.com/weknow619/p/10464139.html
typedef struct dict { //类型特定函数 dictType *type;
// 私有数据 void *privdata;
// 哈希表 dictht ht[2];
// rehash 索引 // 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
// 哈希表数组 dictEntry **table;
// 哈希表大小 unsigned long size;
// 哈希表大小掩码,用于计算索引值 // 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量 unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {void *val;uint64_t u64;int64_t s64;} v;
// 指向下个哈希表节点,形成链表 struct dictEntry *next;
} dictEntry;
typedef struct dictType {
// 计算哈希值的函数 unsigned int (*hashFunction)(const void *key);
// 复制键的函数 void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数 void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数 int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数 void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数 void (*valDestructor)(void *privdata, void *obj);
} dictType;
上面源码可以简化成如下结构:
虽然看到hash结构里面的底层代码的dict 和key值里面的dict结构是一样的,但其实redis底层会将hashtable(哈希表)压缩为ziplist(压缩列表)的结构,可以大大节省存储空间,经过实验,ziplist储存和hash表储存储存效率超过7倍 当储存数量越多储存节省空间也会相应增加
简单理解下ziplist:
资料链接:https://blog.csdn.net/yellowriver007/article/details/79021049
Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):
1.哈希中元素数量小于512个;
2.哈希中所有键值对的键和值字符串长度都小于64字节。
所以我们要在之后的处理注意Hash值的要求否则所有的工作就都打水漂了
当然我们可以修改list-max-ziplist-value与hash-max-ziplist-entries来使用不同的阈值。
具体源码:https://blog.csdn.net/m0_37343985/article/details/83715138
至此大概确定了优化思路,以key-hashmap 结构保存数据,降低key值储存空间,空值value值长度,控制key下hashmap数量
那么第一个问题,怎么减少key值数量
最直接的办法是切割,但是安卓和ios的设备id分别是idfa,imei长度不一致所以,切割后第一 切割长度不好统一,第二长度不一会更容易造成redis产生内存碎片(内存碎片会单独写)
所以需要通过MD5哈希化设备号为32位
然后讲前22位切割作为key-map的key值,后10位作为field
当我们的数据量十分庞大的时候前22位数据会出现重复项,这个时候map值会增加,由于切割的长度做了控制,当前20亿数据的量级不用担心元素数量大于512位,经过实验其实平均保存数量只有20到30之间,最大数量也只有80多一点所以其实还可以继续降低key值长度以此来达到优化存储空间的问题
当然根据上文所述查询效率其实并没有降低,由于实验区别十分小在这也就不对比了
至此key值优化搞定,下面优化value值
value值优化:
优化存储空间,第一反应是字节储存,并且因为map中value默认值位64位所以最好在这个区间内
那么将500个标签压缩在64个字节中并且因为500个标签属于离散数据,那么第一反应是one-host编码,但是one-host编码会导致数据长短不一500个标签全部都需要控制在64位中不太好实现
所以选择bitmap数据格式,将每一个标签标记为是否属实,属实为1,不属实为0
简单计算下1个字节可以存储8位 64位就是512个标签 大于500个标签也就是意味着用63位就可以存储下所有的标签数据
简单描述bitmap数据结构
资料:http://www.luyixian.cn/news_show_23323.aspx
32位计算机下存储一个int a=1 在内存中占32bit位,但是我们的数据全部都是0,1结构这样储存空间会十分浪费,其实开辟一个byte空间就可以存储8bit的数据,那么将所以的byte作为数组储存,比如需要存储50个数据那么只需要保存一个7个byte的数组就可以保存下这些数据总耗费空间位56bit这个空间,即使多出来的6位置为0也可以大大节省存储空间,毕竟两个int就已经占了64bit位的空间了。
优点: 运算效率高,不需要进行比较和移位; 占用内存少,比如N=10000000;只需占用内存为N/8=1250000Byte=1.25M。
而且这种数据在进行单条数据筛选的时候可以根据位置进行位运算处理大大提高查询效率,
当需要判断某一用户是否和某个标签匹配的时候只需要根据设备id取出value值然后进行指定位的比较就可以在接近O(1)的时间复杂度下实现,达到告诉处理反馈
至此全部的优化逻辑
key的存储格式:key-hashmap储存
value的储存格式:bitmap数据结构
统一数据格式格式:md5哈希化为32位,切割为两半22位和10位组合作为key和field
降低时间复杂度:O(1)复杂度,redis一次操作查询次数只为1次 标签对比次数也为1
平均查询处理时间30毫秒左右。
单节点7200mb 50个redis节点,总内存175gb使用内存130gb
内存碎片
资料://www.greatytc.com/p/cf803e9c38e9?from=timeline&isappinstalled=0
redis的内存状态 info memory
内存碎片率1.24还算比较健康
ratio指数>1表明有内存碎片,越大表明越多,<1表明正在使用虚拟内存,虚拟内存其实就是硬盘,性能比内存低得多,这是应该增强机器的内存以提高性能。一般来说,mem_fragmentation_ratio的数值在1 ~ 1.5之间是比较健康的
-----------------------------------------------------分割线 ---------------------------------------
产生原因
可以这样认为,redis产生内存碎片有两个原因,A:redis自身的内存分配器。B:修改cache的值,且修改后的value与原来value的大小差异较大。
进程需要用内存的话,会先通过OS向device申请,然后才能够使用。一般进程在不需要使用的时候,会释放掉这部分内存并返回给device。但是redis作者可能为了更高的性能,所以在redis中实现了自己的内存分配器来管理内存,不会马上返还内存,不用每次都向OS申请了,从而实现高性能。
但是,在内存分配器的那张图片我们知道,redis的每个k-v对初始化的内存大小是最适合的,当这个value改变的并且原来内存大小不适用的时候,就需要重新分配内存了。(但是value存比原来小不知道会不会产生碎片)。重新分配之后,就会有一部分内存redis无法正常回收,一直占用着。
1、重启redis服务,简单粗暴。2、redis4.0以上可以使用新增指令来手动回收内存碎片,配置监控使用性能更佳。
资料链接:https://my.oschina.net/watliu/blog/1620666
3.修改内存分配器。Redis支持glibc’s malloc、jemalloc11、tcmalloc几种不同的内存分配器,每个分配器在内存分配和碎片上都有不同的实现。不建议普通管理员修改Redis默认内存分配器,因为这需要完全理解这几种内存分配器的差异,也要重新编译Redis