Redis 读书笔记 实践篇 (上)

165 阅读13分钟

11 String,为什么不好用了?

String类型的内存空间消耗问题,选择节省内存开销的数据类型的解决方案;String类型并不是适用于所有场合的,它保存数据时所消耗的内存空间比较多;集合类型非常节省内存空间的底层实现结构。

11.1 String类型内存开销大的原因

一个图片 ID 和图片存储对象 ID 的记录平均用了 64 字节;但问题是,一组图片 ID 及其存储对象 ID 的记录,实际只需要 16 字节就可以了;因为 8 字节的 Long 类型最大可以表示 2 的 64 次方的数值,所以肯定可以表示 10 位数。 String类型还需要额外的内存空间记录数据长度、空间使用等信息,这些信息叫做元数据。

11.1.1 SDS简单动态字符串结构

当你保存的数据包含字符时,String类型就会简单动态字符串结构来保存。 image.png buf:字节数组,实际保存数据。len:占用4个字节,表示buf的已用长度。alloc:占用4个字节,表示buf的实际分配长度,一般大于len;所以len和alloc就是结构体的额外开销。

11.1.2 RedisObject

RedisObject:Redis不同的数据类型都有些相同的元数据要记录,RedisObject结构体统一记录这些元数据,同时指向实际数据。 image.png 为了节省内存空间,Redis还对Long类型整数和SDS的内存布局做了专门的设计。 image.png

11.1.3 计算String类型的内存使用量

10位数的图片ID和图片存储对象ID是Long型整数,可以直接用int编码的RedisObject保存;每个ID使用16个字节,key+Value就是32个字节;Redis使用一个全局哈希表保存所有键值对,哈希表每一项都是一个dictEntry结构体;8+8+8+16+16,少了8。 image.png 原因是Jemalloc在分配内存时,会根据我们申请的字节数N,找一个比N大但是最接近N的2的幂次数作为分配的空间,这样可以减少频繁分配的次数。

到这里,应该能理解String类型保存ID和图片存储对象ID需要用64个字节了。其实有效信息就只有16字节,却需要64字节。我们来换算下,如果要保存的图片有 1 亿张,那么 1 亿条的图片 ID 记录就需要 6.4GB 内存空间,其中有 4.8GB 的内存空间都用来保存元数据了,额外的内存空间开销很大。

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

压缩列表ziplist。压缩列表的构成由 三个字段 zlbytes(列表长度)、zltail(列表尾的偏移量)、zllen(列表结束)。 image.png

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

Redis基于压缩列表实现List、Hash、Sorted Set这样的集合类型。最大好处就是节省了dictEntry的开销。

之前使用String类型时,必须一个图片ID对应一个图片存储ID,这样有多少个图片,就有多少这样的键值对,而前面也提到了,Redis会把这些键值对保数据存在dictEntry中,而每个dictEntry就会消耗32字节。这其实是额外的消耗。如果使用hash或者别的集合类型的存储,由于把一些value集合在一起后和一个key对应,那么dictEntry的数量将会大大减少,从而减少了额外的开销。 这是本方案能降低内存消耗的一个重要原因。

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

Hash类型设置了用压缩列表保存数据时的两个阈值,当压缩列表保存时哈希集合中的最大个数和集合中单个元素的最大长度时Hash类型就会用哈希表来保存数据了。 如果说一旦Hash类型的实现结构由压缩列表转为哈希表,哈希表就没有那么高效了。

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

11.4 总结

String包括了 RedisObject 结构、SDS 结构、dictEntry 结构的内存开销。针对这种情况,我们可以使用压缩列表保存数据。使用 Hash 这种集合类型保存单值键值对的数据时,我们需要将单值数据拆分成两部分,分别作为 Hash 集合的键和值,就像刚才案例中用二级编码来表示图片 ID,希望你能把这个方法用到自己的场景中。

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

12.1 聚合统计:Set 和 Sorted Set

统计多个集合的共有元素(交集元素)、差集统计、并集统计; 计算累计用户set和user:id:20200803的并集结果保存在user:id这个累计用户set中

  • SUNIONSTORE user:id user:id user:id:20200803

计算累计用户set和04 set的差集保存在new中

  • SDIFFSTORE user:new user:id:20200804 user:id

计算03和04的交集

  • SINTERSTORE user:id:rem user:id:20200803 user:id:20200804

数据量大的情况下计算这些会导致Redis实例阻塞,可以从主从集群中选择一个从库来计算。 Set 和 Sorted Set 都支持多种聚合统计,不过,对于差集计算来说,只有 Set 支持。

12.2 排序统计:sorted set

使用sorted set根据实际权重来排序和获取数据

  • zrangebyscore comments N-9 N

12.3 二值状态统计:Bitmap

我们常说的打卡场景中签到1或未签到0就是典型的二值状态。Bitmap本身是用String类型作为底层数据结构实现的一种统计二值状态的数据类型。Bitmao提供getbit/setbit操作,使用一个偏移值offset对bit数组的某一个bit位进行读和写。从0开始算。

统计用户curry8.3已签到

  • SETBIT uid:sign:curry:202308 2 1

检查用户8.3是否签到

  • GETBIT uid:sign:curry:202308 2

统计该用户在8月份的签到次数

  • BITCOUNT uid:sign:curry:202308

如何统计 1亿个用户10天的签到情况,统计出着10天连续签到的用户总数? 把每天日期作为key,每个key对应1一个bitmap->一个用的签到情况。这样的话10个bitmap做与操作,得到结果也是一个bitmap。之后用bitcount统计下bitmap1个个数就是连续签到10天的用户总数。 image.png 一个亿的bitmap大约占 12MB 的内存(10^8/8/1024/1024)

12.4 基数统计:HyperLogLog

基数统计就是指统计一个集合中不重复的元素个数。比如统计网页的UV。一个用户一天内的多次访问只能算作一次。 HyperLogLog是一种用户统计技术的数据集合类型,计算技术所需的空间总是固定的,而且还很小,只需要花费12KB内存,就可以接近2^64 个元素的基数。

  • PFADD page1:uv user1 user2 user3 user4 user5
  • PFCOUNT page1:uv

12.5 总结

image.png

13 GEO是什么?

总结一下之前学习的Redis的5大基本数据类型:String、List、Hash、Sorted set、Set。 对于一些特殊的场景Redis还提供了3种扩展数据类型。Bitmap、HyperLogLog、GEO。

13.1 Hash类型

image.png 因为Hash类型的元素是无序的,无法涉及到范围查询。

13.2 Sorted Set

image.png 其中key是Sorted Set中的元素,而value则是元素的权重分数。 Sorted Set可以根据元素的权重分数排序,支持范围查询。

13.3 GeoHash编码

Redis采用GeoHash编码来保存经纬度两个值。 通过二分区间,区间编码把一组经纬度用一个值来表示,就可以保存为Sorted Set的权重分数了。 image.png

13.4 命令

geoadd命令 : 添加地理位置的坐标

  • GEOADD key longitude latitude member
  • GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
  • GEOADD cars:locations 116.034579 39.030452 33

geoRadius命令 :根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

  • GEORADIUS Sicily 15 37 100 km
  • GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10

14 如何在Redis中保存时间序列数据?

14.1 时间序列数据的写入特点:

  • 点查询,根据一个时间戳,查询相应时间的数据
  • 范围查询,查询起始和截止时间戳范围内的数据
  • 聚合计算,针对起始和截止时间戳范围内的所有数据进行计算,例如求最大、最小值,求均值等

14.2 第一种方案组合使用Redis内置的Hash和Sorted Set类型

把数据同时保存在Hash集合和Sorted Set集合中。既可以利用Hash类型实现对单键的快速查询,还能利用Sorted Set实现对范围查询的高级支持。在执行聚合计算需要把数据读取到客户端再进行聚合,有大量数据聚合数据传输开销大。数据保存两份开销不小。

查询某个时间点或者多个时间点上的温度数据时 image.png HGET device:temperature 202008030905

  • "25.1"

HMGET device:temperature 202008030905 202008030907 202008030908

    1. "25.1"
    1. "25.9"
    1. "24.9"

范围查询 image.png ZRANGEBYSCORE device:temperature 202008030907 202008030910

    1. "25.9"
    1. "24.9"
    1. "25.3"
    1. "25.2"

保存两个数据时使用multi、exec保证原子性 127.0.0.1:6379> MULTI

  • OK

127.0.0.1:6379> HSET device:temperature 202008030911 26.8

  • QUEUED

127.0.0.1:6379> ZADD device:temperature 202008030911 26.8

  • QUEUED

127.0.0.1:6379> EXEC

    1. (integer) 1
    1. (integer) 1

14.3 第二种方案使用RedisTimeSeries模块。

专门为存取时间序列数据而设计的扩展模块。RedisTimeSeries直接在Redis实例上进行多种数据聚合计算。但是他底层数据结构使用了链表范围查询是o(N)级别,也没有办法像Hash类型一样返回任一时间点的数据。

14.3.1 创建一个时间序列数据集合

TS.CREATE device:temperature RETENTION 600000 LABELS device_id 1

  • 数据有效期为 600s
  • 标签属性{device_id:1} 属于设备ID号为1的数据
14.3.2 插入&读取

TS.ADD device:temperature 1596416700 25.1

  • 1596416700

TS.GET device:temperature

  • 25.1
14.3.3 按标签过滤查询数据集合

TS.MGET FILTER device_id!=2

14.3.4 支持需要聚合计算的范围查询

TS.RANGE device:temperature 1596416700 1596417120 AGGREGATION avg 180000

  • 用 AGGREGATION 参数指定要执行的聚合计算类型;求均值(avg)、求最大 / 最小值(max/min),求和(sum)等
  • 我们就可以按照每 180s 的时间窗口,对 2020 年 8 月 3 日 9 时 5 分和 2020 年 8 月 3 日 9 时 12 分这段时间内的数据进行均值计算
14.4 建议

如果部署环境网络带宽高、Redis实力内存大,选择第一种; 如果部署环境中网络内存资源有限,而且数据量大,聚合计算频繁,需要按数据集合属性查询选第二种。

15 消息队列,Redis有哪些解决方案?

15.1 分布式系统组件使用消息队列的三大需求

  • 消息数据有序存取
  • 消息数据具有全局唯一编号
  • 消息数据在消费完成后被删除

15.2 List和Streams实现消息队列的特点和区别

image.png

15.3 基于List的消息队列解决方案

LPUSH mq "101030001:stock:5"

  • (integer) 1
15.3.1 性能损失

为了解决消费者程序的cpu一直消耗再执行rpop命令上,带来不必要的性能损失。 Redis提供了brpop命令,也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。

15.3.2 消息可靠性

为了留存消息,List提供了brpoplpush命令留存消息。即使消费者程序处理消息宕机了,等他重启后,可以从mqback再次读取消息。 image.png

15.3.3 消息堆积

启动多个消费者组成一个消费者,分担处理消息,但是List类型不支持消费组的实现。

15.4 基于Streams的消息队列解决方案

Streams 是 Redis 5.0 专门针对消息队列场景设计的数据类型

15.4.1 插入

XADD mqstream * repo 5

  • "1599203861727-0"

*表示Redis为插入的数据自动生成一个全局唯一的ID 第一部分数据表示插入消息在当前毫秒内的消息序号,从0开始编号。

15.4.2 读取

XREAD block 100 streams mqstream 1599203861727-0

  • 从id号1599203861727-0开始读取后续的所有消息
  • block实现类似于brpop的阻塞读取操作

XREAD block 10000 streams mqstream $

  • $ 符号表示读取最新的消息
  • block 10000 单位是毫秒,读取最新消息,如果没有消息到来,xread将阻塞1000毫秒返回空值
15.4.3 按消费组形式读取

XGROUP create mqstream group1 0 XREADGROUP group group1 consumer1 streams mqstream > image.png 这里有一个概念,使用消费者的目的是让组内的多个消费者共同分担读取消息,所以我们通常会让每个消费者读取部分消息,从而实现消息读取负责在多个消费者间是均衡分布的。 image.png

15.4.4 查询每个消费组内所有消费者

Streams会自动使用内部队列pending list留存消费者读取的消息,直到消费者使用xack命令通知streams消息已处理完成。使用ACK机制来着可靠性。消费者可以重启后用xpending命里查看已读取,尚未确认处理完成的消息。

各个消费者已读取,尚未确认的消息个数。2、3表示 group2 中所有消费者读取的消息最小 ID 和最大 ID image.png

进一步查看某个消费者读取了哪些? image.png 使用xack命令通知streams image.png