redis-不同业务场景下的统计数据
1、为什么 String 类型内存开销大?
String 类型具体是怎么保存数据的呢?当你保存 64 位有符号整数时,String 类型会把它保存为一个 8 字节的 Long 类型整数,这种保存方式通常也叫作 int 编码方式。但是,当你保存的数据中包含字符时,String 类型就会用简单动态字符串(Simple Dynamic String,SDS)结构体来保存,如下图所示:
RedisObject 的内部组成包括了 type、encoding、lru 和 refcount 4 个元数据,以及 1 个*ptr指针。
type:表示值的类型,涵盖了五大基本类型;encoding:是值的编码方式,用来表示 Redis 中实现各个基本类型的底层数据结构,例如 SDS、压缩列表、哈希表、跳表等;lru:记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对;refcount:记录了对象的引用计数;*ptr:是指向数据的指针。
为了节省内存空间,Redis 还对 Long 类型整数和 SDS 的内存布局做了专门的设计。一方面,当保存的是 Long 类型整数时,RedisObject 中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数了,节省了指针的空间开销。另一方面,当保存的是字符串数据,并且字符串小于等于 44 字节时,RedisObject 中的元数据、指针和 SDS 是一块连续的内存区域,这样就可以避免内存碎片。这种布局方式也被称为 embstr 编码方式。当然,当字符串大于 44 字节时,SDS 的数据量就开始变多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是会给 SDS 分配独立的空间,并用指针指向 SDS 结构。这种布局方式被称为 raw 编码模式。为了帮助你理解 int、embstr 和 raw 这三种编码模式,如下所示:
Redis 会使用一个全局哈希表保存所有键值对,哈希表的每一项是一个 dictEntry 的结构体,用来指向一个键值对。dictEntry 结构中有三个 8 字节的指针,分别指向 key、value 以及下一个 dictEntry,三个指针共 24 字节,如下图所示
但是,这三个指针只有 24 字节,为什么会占用了 32 字节呢?这就要提到 Redis 使用的内存分配库 jemalloc 了。jemalloc 在分配内存时,会根据我们申请的字节数 N,找一个比 N 大,但是最接近 N 的 2 的幂次数作为分配的空间,这样可以减少频繁分配的次数。
而一个 RedisObject 包含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的实际数据所在
二、用什么数据结构可以节省内存?
压缩列表:表头有三个字段 zlbytes(总字节长度)、zltail (最后一个元素的偏移量)和 zllen(元素的个数),分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。
三、不同的业务场景采用不同的结构
五种基本类型:string hash list set sorted set
三种扩展类型: bitmap hyperLogLog GEO
聚合统计(统计每天的新增用户数和第二天的留存用户数等),利用set集合的差集、并集
排序统计 (排行榜以及统计评论列表中的最新评论等) list(但是不支持分页数据) sorted set
二值状态 (每日签到,签到统计等) bitmap (基于string)
原理:bitmap就是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。
用法:给一个指定key的值得第offset位 赋值为value。
基数统计(uv统计 需要去重) hash set(数据量太大消耗内存空间)HyperLogLog (基于string)但是有误差
面向 LBS 应用(附近,地理位置)的 GEO 数据类型(基于 sorted set)
原理:GeoHash ,基本原理就是“二分区间,区间编码”。
假设要编码的经度值是 116.37,用 5 位编码值(也就是 N=5,做 5 次分区)。编码过程如下
纬度的编码方式,和对经度的一样,只是纬度的范围是[-90,90],下面这张表显示了对纬度值 39.86 的编码过程。
当一组经纬度值都编完码后,再把它们的各自编码值组合在一起,组合的规则是:最终编码值的偶数位上依次是经度的编码值,奇数位上依次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始,这样就能得到最终编码值 是1110011101
用法:
添加一个车辆信息:GEOADD cars:locations 116.034579 39.030452 33
查找附近5km的排序最近的十辆车:GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
时间序列数据(某一系列时间的数据)同时采用hash 和sorted set两种结构存储(hash解决单点,sorted set解决范围查询),但是hash 和sorted set是两个命令,怎么保证原子性呢
redis有两个命令
MULTI 命令:表示一系列原子性操作的开始。收到这个命令后,Redis 就知道,接下来再收到的命令需要放到一个内部队列中,后续一起执行,保证原子性。
EXEC 命令:表示一系列原子性操作的结束。一旦 Redis 收到了这个命令,就表示所有要保证原子性的命令操作都已经发送完成了。此时,Redis 开始执行刚才放到内部队列中的所有命令操作。 如下图所示
如果需要对时间序列数据进行聚合计算,那么就要基于 RedisTimeSeries 模块保存时间序列数据
Set、Sorted Set、Hash、List、Bitmap、HyperLogLog 汇总表
需要注意的是:
1、如果是在集群模式使用多个key聚合计算的命令,一定要注意,因为这些key可能分布在不同的实例上,多个实例之间是无法做聚合运算的,这样操作可能会直接报错或者得到的结果是错误的!
2、当数据量非常大时,使用这些统计命令,因为复杂度较高,可能会有阻塞Redis的风险,建议把这些统计数据与在线业务数据拆分开,实例单独部署,防止在做统计操作时影响到在线业务。