自我系统学习Redis小记-05

11 | “万金油”的String,为什么不好用了?

1、String 类型的内存空间消耗问题,以及选择节省内存开销的数据类型的解决方案。

String 类型并不是适用于所有场合的,它有一个明显的短板,就是它保存数据时所消耗的内存空间较多。

内存大-->生成rdb响应满-->redis响应慢

2、为什么 String 类型内存开销大?

除了记录实际数据,String 类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息也叫作元数据。当实际保存的数据较小时,元数据的空间开销就显得比较大了,有点“喧宾夺主”的意思。

简单动态字符串

buf:字节数组,保存实际数据。为了表示字节数组的结束,Redis 会自动在数组最后加一个“\0”,这就会额外占用 1 个字节的开销。

len:占 4 个字节,表示 buf 的已用长度。

alloc:也占个 4 字节,表示 buf 的实际分配长度,一般大于 len。

在 SDS 中,buf 保存实际数据,而 len 和 alloc 本身其实是 SDS 结构体的额外开销。

除此之外,还有一些其他的,比如RedisObject 记录一些元数据(最后访问时间、被引用次数)、以及 dictEntry(全局hash表的指针三个等,占用24字节,分别是指向 key、value 以及下一个 dictEntry)等等、这里只需要知道String除了实际数据还存了其他东西即可,认为不是重点,不做记录。

redisObject
sds
dictEntry结构体

3、用什么数据结构可以节省内存?

压缩列表(ziplist),一种非常节省内存的结构

表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量,以及列表中的 entry 个数。压缩列表尾还有一个 zlend,表示列表结束。

压缩列表结构

压缩列表之所以能节省内存,就在于它是用一系列连续的 entry 保存数据。

这些 entry 会挨个儿放置在内存中,不需要再用额外的指针进行连接,这样就可以节省指针所占用的空间。

每个 entry 的元数据包括下面几部分:

1)、prev_len,表示前一个 entry 的长度。prev_len 有两种取值情况:1 字节或 5 字节。取值 1 字节时,表示上一个 entry 的长度小于 254 字节。虽然 1 字节的值能表示的数值范围是 0 到 255,但是压缩列表中 zlend 的取值默认是 255,因此,就默认用 255表示整个压缩列表的结束,其他表示长度的地方就不能再用 255 这个值了。所以,当上一个 entry 长度小于 254 字节时,prev_len 取值为 1 字节,否则,就取值为 5 字节。

2)、len:表示自身长度,4 字节;

3)、encoding:表示编码方式,1 字节;

4)、content:保存实际数据。

Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的集合类型,最大好处是节省了 dictEntry 的开销。String是一个健值对对应一个dictEntry,集合是一个key对应多个dictEntry 这样就节省了内存。

4、如何用集合类型保存单值的键值对?

在保存单值的键值对时,可以采用基于 Hash 类型的二级编码方法。这里说的二级编码,就是把一个单值的数据拆分成两部分,前一部分作为 Hash 集合的 key,后一部分作为Hash 集合的 value,这样一来,我们就可以把单值数据保存到 Hash 集合中了。

以图片 ID 1101000060 和图片存储对象 ID 3302000080 为例,我们可以把图片 ID 的前7 位(1101000)作为 Hash 类型的键,把图片 ID 的最后 3 位(060)和图片存储对象ID 分别作为 Hash 类型值中的 key 和 value。

使用String占用64字节,但是使用二级编码只占用16字节。

注意,这里用前7位做Hash类型的key,二级编码方法中采用的 ID 长度是有讲究的。

Hash类型底层使用 压缩列表哈希表。注意什么时候用什么类型!

Hash类型设置了用压缩列表保存数据时的两个阈值,一旦超过阈值Hash 类型就会用哈希表来保存数据了。

两个阈值分别对应两个配置项:

1)、hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。

2)、hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

如果我们往 Hash 集合中写入的元素个数超过了 hash-max-ziplist-entries,或者写入的单个元素大小超过了 hash-max-ziplist-value,Redis 就会自动把 Hash 类型的实现结构由压缩列表转为哈希表

一旦从压缩列表转为了哈希表,Hash 类型就会一直用哈希表进行保存,而不会再转回压缩列表了。在节省内存空间方面,哈希表就没有压缩列表那么高效了

为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数。所以,在刚才的二级编码中,我们只用图片 ID 最后 3 位作为 Hash 集合的 key,也就保证了 Hash 集合的元素个数不超过 1000,同时,我们把 hash-max-ziplist-entries 设置为 1000,这样一来,Hash 集合就可以一直使用压缩列表来节省内存空间了。

5、小结

打破了对 String 的认知误区,以前,我们认为 String 是“万金油”,、

什么场合都适用,但是,在保存的键值对本身占用的内存空间不大时,

String 类型的元数据开销就占据主导了,这里面包括了RedisObject 结构、SDS 结构、dictEntry 结构的内存开销。

针对这种情况,我们可以使用压缩列表保存数据。当然,使用 Hash 这种集合类型保存单值键值对的数据时,我们需要将单值数据拆分成两部分,分别作为 Hash 集合的键和值,就像刚才案例中用二级编码来表示图片 ID。


12 | 有一亿个keys要统计,应该用哪种集合?

1、前言

对以下集合进行统计引出问题(访问量巨大,百万、千万):

1)、在移动应用中,需要统计每天的新增用户数和第二天的留存用户数;

2)、在电商网站的商品评论中,需要统计评论列表中的最新评论;

3)、在签到打卡中,需要统计一个月内连续打卡的用户数;

4)、在网页访问记录中,需要统计独立访客(Unique Visitor,UV)量。

要想选择合适的集合,我们就得了解常用的集合统计模式

集合类型常见的四种统计模式,包括聚合统计、排序统计、二值状态统计和基数统计。

2、聚合统计

是指统计多个集合元素的聚合结果。包括:交集统计、差集统计、并集统计。

统计手机 App 每天的新增用户数和第二天的留存用户数,正好对应了聚合统计。

完成这个统计任务需要记录两个集合,第一个集合:记录所有登陆过app的用户ID;第二个集合:记录每一天登陆过app的用户ID。

记录所有登陆过app的用户ID
记录每一天登陆过app的用户ID

在统计每天新增用户时,只用计算这两个集合的差集即可。

Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。所以,建议:可以从主从集群中选择一个从库,让它专门负责聚合计算,或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。

3、排序统计

要求集合类型能对元素保序,集合中的元素可以按序排列,这种对元素保序的集合类型叫作有序集合

List 是按照元素进入 List 的顺序进行排序的,而 Sorted Set 可以根据元素的权重来排序

场景:最新评论列表包含了所有评论中的最新留言

List会导致分页时候出现问题,如果恰好新插入一个数据,会导致读到旧元素。

Sorted Set不存在这种情况,因为它是根据元素的实际权重来排序和获取数据的。

//权重N,最新的权重越大,我们执行下面的命令时,就可以获得最新的 10 条评论

ZRANGEBYSCORE comments N-9 N

所以,面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页,可以使用Sorted Set。

4、二值状态统计

二值状态就是指集合元素的取值就只有 0 和 1 两种。

场景:签到,1-已经签到,0-未签到

在签到统计时,每个用户一天的签到用 1 个 bit 位就能表示,一个月31bit,可以选不复杂的结构。

这时,我们就可以选择 Bitmap

Bitmap是 Redis 提供的扩展数据类型

Bitmap介绍:

Bitmap 本身是用 String 类型作为底层数据结构,实现的一种统计二值状态的数据类型。

String 类型是会保存为二进制的字节数组,所以 Redis 把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。Bitmap就相当于一个 bit 数组。

Bitmap 提供了 GETBIT/SETBIT 操作,使用了一个偏移量 offset 对 bit 数组的某一个 bit 位进行读和写。

具体命令:SETBIT KEY_NAME OFFSET 1

Bitmap 的偏移量是从 0 开始算的,也就是说 offset的最小值是 0。

当使用 SETBIT 对一个 bit 位进行写操作时,这个bit位会被设置为1。

Bitmap 还提供了 BITCOUNT 操作,用来统计这个 bit 数组中所有 “1” 的个数。

Bitmap 支持用 BITOP 命令对多个 Bitmap 按位做“与”“或”“异或”的操作,操作的结果会保存到一个新的 Bitmap 中。

BITCOUNT 统计操作
BITOP 命令按位与操作

回到刚刚的问题,在统计 1 亿个用户连续 10 天的签到情况时,你可以把每天的日期作为key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。接下来,我们对 10 个 Bitmap 做“与”操作,得到的结果也是一个 Bitmap。在这个Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。

可以计算一下记录了 10 天签到情况后的内存开销。每天使用 1 个 1 亿位的 Bitmap,大约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力不算太大。不过,在实际应用时,最好对 Bitmap 设置过期时间,让Redis 自动删除不再需要的签到记录,以节省内存开销。

所以,如果只需要统计数据的二值状态,例如商品有没有、用户在不在等,就可以使用Bitmap,因为它只用一个 bit 位就能表示 0 或 1在记录海量数据时,Bitmap 能够有效地节省内存空间。

5、基数统计

统计场景:基数统计。基数统计就是指统计一个集合中不重复的元素个数

对应到我们刚才介绍的场景中,就是统计网页的 UV。

网页 UV 的统计有个独特的地方,就是需要去重,一个用户一天内的多次访问只能算作一次。

解决:

1)、首先想到 Redis 里面的 Set 自动去重,后续使用 SCARD 命令返回个数。

缺点是数据量大的时候非常耗内存,(某一page页非常火爆)。

SADD page1:uv user1

2)、采用 Hash 类型记录UV,后续使用 HLEN 命令统计 Hash 集合中的所有元素个数

即使一个用户多次访问,这个值还是1,缺点和 Set 类似,页面很多,数据量大时耗内存。

HSET page1:uv user1 1

3)、Redis 提供的 HyperLogLog

是一种用于统计基数的数据集合类型,最大优势在于,当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。

在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数

HyperLogLog 和 hash、set 比较就很节省内存空间。

在 UV 统计这个场景,可以用 PFADD 命令把访问页面的每个用户都添加到 HyperLogLog 中

#用于向 HyperLogLog 中添加新元素

PFADD page1:uv user1 user2 user3 user4 user5

#返回 HyperLogLog 的统计结果

PFCOUNT page1:uv

注意一下:HyperLogLog 是基数概率统计,有误差,标准误算率是0.81%,需要精确统计就要用 hash 和 set。

6、小结

各个数据结构支持情况和优缺点

1)、Set 和 Sorted Set 都支持多种聚合统计,不过,对于差集计算来说,只有 Set支持。Bitmap 也能做多个 Bitmap 间的聚合计算,包括与、或和异或操作。

2)、当需要进行排序统计时,List 中的元素虽然有序,但是一旦有新元素插入,原来的元素在List 中的位置就会移动,那么,按位置读取的排序结果可能就不准确了。而 Sorted Set 本身是按照集合元素的权重排序,可以准确地按序获取结果,所以建议你优先使用它。

3)、如果我们记录的数据只有 0 和 1 两个值的状态,Bitmap 会是一个很好的选择,这主要归功于 Bitmap 对于一个数据只用 1 个 bit 记录,可以节省内存。

4)、对于基数统计来说,如果集合元素量达到亿级别而且不需要精确统计时,我建议你使用HyperLogLog。


13 | GEO是什么?还可以定义新的数据类型吗?

1、前言

Redis 五大基本类型:String、List、Hash、Set 和Sorted Set。

在海量数据统计方面,基本类型内存开销很大,而且一些场景还不满足。

Redis提供三种拓展数据类型:Bitmap、HyperLogLog 和 GEO

2、面向 LBS 应用的 GEO 数据类型

基于位置信息服务(Location-Based Service,LBS)的应用。

LBS 应用访问的数据是人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围。

LBS 应用中经纬度的存取特点:

1、   每一辆网约车都有一个编号(例如 33),网约车需要将自己的经度信息(例如116.034579)和纬度信息(例如 39.000452 )发给叫车应用。 

2、用户在叫车的时候,叫车应用会根据用户的经纬度位置(例如经度 116.054579,纬度39.030452),查找用户的附近车辆,并进行匹配。

3、等把位置相近的用户和车辆匹配上以后,叫车应用就会根据车辆的编号,获取车辆的信息,并返回给用户。

一辆车(或一个用户)对应一组经纬度,并且随着车(或用户)的位置移动,相应的经纬度也会变化。

3、GEO 的底层结构

1)、当有很多车或者用户,需要用一个集合来保存,首先考虑Hash

hash存储位置信息

但问题是,对于一个 LBS 应用来说,除了记录经纬度信息,还需要根据用户的经纬度信息在车辆的 Hash 集合中进行范围查询。一旦涉及到范围查询,就意味着集合中的元素需要有序,但 Hash 类型的元素是无序的,显然不能满足我们的要求

2)、Sorted Set 类型,正是 GEO 底层数据结构

key 就是 Sorted Set 中的元素,而 value 则是元素的权重分数

更重要的是,Sorted Set 可以根据元素的权重分数排序,支持范围查询。这就能满足 LBS 服务中查找相邻位置的需求了

用 Sorted Set 来保存车辆的经纬度信息时,Sorted Set 的元素是车辆 ID,元素的权重分数是经纬度信息

sort set存储经纬度信息

问题:Sorted Set 元素的权重分数是一个浮点数(float 类型),而一组经纬度包含的是经度和纬度两个值,是没法直接保存为一个浮点数的,那具体该怎么保存?

解决:GeoHash编码

4、GeoHash编码方法----“二分区间,区间编码”

为了高效经纬度比较,业界最广泛使用的就是GeoHash编码。

经纬度单独编码过程:

经度范围是[-180,180]纬度的范围是[-90,90])。GeoHash 编码会把一个经度值编码成一个 N 位的二进制值,我们来对经度范围[-180,180]做 N 次的二分区操作,其中 N可以自定义。二分后,值落在左区间取0,右区间取1。

对精度116.37,纬度39.86,进行GeoHash编码,N取5,得到如下结果:

经度5次二分
纬度5次二分

最后组合两次编码的 GeoHash 编码,得到1110011101表示该经纬度的权重分值,规则如下:

经、纬度组合GeoHash编码

5、GeoHash 分区

使用 GeoHash 编码后,我们相当于把整个地理空间划分成了一个个方格,每个方格对应了 GeoHash 中的一个分区。

[180,180]、[90,90]的经纬度做两次分区

分区一:[-180,0) 和[-90,0),编码 00;

分区二:[-180,0) 和[0,90],编码 01;

分区三:[0,180]和[-90,0),编码 10;

分区四:[0,180]和[0,90],编码 11。

每个方格覆盖了一定范围内的经纬度值,分区越多,每个方格能覆盖到的地理空间就越小,也就越精准。我们把所有方格的编码值映射到一维空间时,相邻方格的 GeoHash 编码值基本也是接近的。

经纬度分别 二次分区 得到四块

所以,使用 Sorted Set 范围查询得到的相近编码值,在实际的地理空间上,也是相邻的方格,这就可以实现 LBS 应用“搜索附近的人或物”的功能了。

当然也有反例:相同条件分别四分区,得到16方格:

经纬度分别 四次分区 得到十六块

所以,为了避免查询不准确问题,我们可以同时查询给定经纬度所在的方格周围的 4 个或8 个方格。

6、如何操作 GEO 类型?

两个命令:分别是 GEOADD 和 GEORADIUS。

GEOADD 命令:用于把一组经纬度信息和相对应的一个 ID 记录到 GEO 类型集合中;

GEORADIUS 命令:会根据输入的经纬度位置,查找以这个经纬度为中心的一定范围内的其他元素。当然,我们可以自己定义这个范围。

# 车辆id为33,经纬度为116.034579 39.030452,加入到GEO集合中,前面为key

GEOADD cars:locations 116.034579 39.030452 33

#根据输入的用户的经纬度信息(116.054579,39.030452 ),查找以这个经纬度为中心的 5 公里内的车辆信息,5 这个参数可以自己控制

#asc 表示距离中心点最近的车辆从近到远排序

#count 表示返回十辆车,5km内可能有很多车,如果都返回占用数据带宽

GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10

7、小结

1)、Redis 的扩展数据类型 GEO。GEO 属于 Redis 提供的扩展数据类型。GEO 可以记录经纬度形式的地理位置信息,被广泛地应用在 LBS 服务中。GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

2)、GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是对二维地图做区间划分,以及对区间进行编码。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

也就是说 GEO = Sorted Set + GeoHash

3)、把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。

*4)、(了解一下)扩展数据类型有两种实现途径:一种是基于现有的数据类型,通过数据编码或是实现新的操作的方式,来实现扩展数据类型,例如基于Sorted Set 和 GeoHash 编码实现 GEO,以及基于 String 和位操作实现 Bitmap;另一种就是开发自定义的数据类型,具体的操作是增加新数据类型的定义,实现创建和释放函数,实现新数据类型支持的命令操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容