乐趣区

关于redis:Redis核心技术笔记1115

11 String

为什么 String 类型内存开销大

RedisObject 构造体 + SDS:

  • 当保留 64 位有符号整数时,String 会存为一个 8 字节的 Long 类型整数,称为 int 编码方式
  • 当数据蕴含字符串时,String 类型会用简略动静字符串 (SDS) 构造体来保留

    • buf:字节数组,保留理论数据。为了示意字节数组的完结,Redis 会主动在数组最初加一个“\0”,这就会额定占用 1 个字节的开销。
    • len:占 4 个字节,示意 buf 的已用长度。
    • alloc:也占个 4 字节,示意 buf 的理论调配长度,个别大于 len。

RedisObject

Redis 的数据类型有很多,而且,不同数据类型都有些雷同的元数据要记录(比方最初一次拜访的工夫、被援用的次数等),所以,Redis 会用一个 RedisObject 构造体来对立记录这些元数据,同时指向理论数据。

一个 RedisObject 蕴含了 8 字节的元数据和一个 8 字节指针,这个指针再进一步指向具体数据类型的理论数据所在。

编码方式

  • 保留 Long 类型整数时,RedisObject 中的指针就间接赋值为整数数据了,只占用 8 字节。
  • 保留小于等于 44 字节的字符串时,RedisObject 中的元数据、指针和 SDS 是一块间断的内存区域,能够防止内存碎片。这种布局形式也被称为 embstr 编码方式。
  • 保留大于 44 字节的字符串时,Redis 会给 SDS 独立的空间,用指针指向 SDS 构造,称为 raw 编码方式。

内存调配库 jemalloc

Redis 会应用一个全局哈希表保留所有键值对,哈希表的每一项是一个 dictEntry 的构造体,用来指向一个键值对。dictEntry 构造中有三个 8 字节的指针,别离指向 key、value 以及下一个 dictEntry,三个指针共 24 字节。

jemalloc 在分配内存时,会依据咱们申请的字节数 N,找一个比 N 大,然而最靠近 N 的 2 的幂次数作为调配的空间,这样能够缩小频繁调配的次数。

申请 24 字节空间,jemalloc 则会调配 32 字节。所以,在咱们刚刚说的场景里,dictEntry 构造就占用了 32 字节。

示例:10 位数图片 ID 和对象 ID 存为 String 类型时,内存构造:
RedisObject:8(元数据)+ 8(INT)= 16 字节
哈希表:8(key) + 8(value) + 8(next) = 24 字节 =>jemalloc 调配 32 字节
总计:16 * 2(键值对)+ 32 = 64 字节

压缩列表 ziplist

压缩列表之所以能节俭内存,就在于它是用一系列间断的 entry 保留数据。

  • 表头:zlbytes(列表长度)、zltail(列表尾偏移量)、zllen(列表 entry 个数)
  • 元数据:间断的 entry
  • 表尾:zlend(列表完结)

每个 entry 形成:

  • prev_len:前一个 entry 的长度,小于 254 字节时占 1 字节,否则占 5 字节。
  • len:示意本身长度,4 字节;
  • encoding:记录该节点 content 属性保留数据的类型及长度。小于等于 63 字节占 1 字节,小于等于 16383 字节占 2 字节,否则占 5 字节。
  • content:理论数据

Redis 基于压缩列表实现了 List、Hash 和 Sorted Set 这样的汇合类型,这样做的最大益处就是节俭了 dictEntry 的开销。

  • 当你用 String 类型时,一个键值对就有一个 dictEntry,要用 32 字节空间。
  • 采纳汇合类型时,一个 key 就对应一个汇合的数据,能保留的数据多了很多,但也只用了一个 dictEntry,这样就节俭了内存。

汇合类型如何保留单值的键值对

能够采纳基于 Hash 类型的二级编码方法:
把一个单值的数据拆分成两局部,前一部分作为 Hash 汇合的 key,后一部分作为 Hash 汇合的 value,这样一来,咱们就能够把单值数据保留到 Hash 汇合中了。

每个 entry 保留一个图片存储对象 ID(8 字节),此时,每个 entry 的 prev_len 只须要 1 个字节就行,因为每个 entry 的前一个 entry 长度都只有 8 字节,小于 254 字节。这样一来,一个图片的存储对象 ID 所占用的内存大小是 14 字节(1+ 4 + 1 + 8),理论调配 16 字节。新增一个图片:一个 entry(16 字节)

Hash 类型的底层实现

Redis Hash 类型的两种底层实现构造,压缩列表和哈希表,都在什么时候应用呢?
Hash 类型设置了用压缩列表保留数据时的两个阈值,一旦超过了阈值,Hash 类型就会用哈希表来保留数据了。

  • hash-max-ziplist-entries:示意用压缩列表保留时哈希汇合中的最大元素个数。
  • hash-max-ziplist-value:示意用压缩列表保留时哈希汇合中单个元素的最大长度。

一旦从压缩列表转为了哈希表,Hash 类型就会始终用哈希表进行保留,而不会再转回压缩列表了。

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

为了能充沛应用压缩列表的精简内存布局,咱们个别要管制保留在 Hash 汇合中的元素个数。所以,在方才的二级编码中,咱们只用图片 ID 最初 3 位作为 Hash 汇合的 key,也就保障了 Hash 汇合的元素个数不超过 1000,同时,咱们把 hash-max-ziplist-entries 设置为 1000,这样一来,Hash 汇合就能够始终应用压缩列表来节俭内存空间了。

小结

在保留的键值对自身占用的内存空间不大时,String 类型的元数据开销就占据主导了,这外面包含了 RedisObject 构造、SDS 构造、dictEntry 构造的内存开销。

针对这种状况,咱们能够应用压缩列表保留数据。

应用 Hash 这种汇合类型保留单值键值对的数据时,咱们须要将单值数据拆分成两局部,别离作为 Hash 汇合的键和值。

Redis 容量预估网址:http://www.redis.cn/redis_mem…

12 汇合统计模式

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

聚合统计(Set 汇合)

聚合统计,就是指统计多个汇合元素的聚合后果,包含:

  • 统计多个汇合的共有元素(交加统计);
  • 把两个汇合相比,统计其中一个汇合独有的元素(差集统计);
  • 统计多个汇合的所有元素(并集统计)。

示例:记录每天登录用户 ID,统计累计用户,新增用户,留存用户。
key 是 user:id 以及当天日期,例如 user20200803;
value 是 Set 汇合,记录当天登录的用户 ID。

累计用户:统计 user:id 和 user20200803 的并集,保留到 user:id 中

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

新增用户:统计 user20200804 和 user:id 的差集,保留到 user:new 中

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

留存用户:统计 0803 和 0804 的交加,保留到 userrem

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

Set 汇合聚合统计危险:计算复杂度高,数据量大时间接执行计算会导致 Redis 实例阻塞。
解决方案:

  1. 选一个从库负责聚合运算
  2. 把数据读取到客户端,在客户端实现聚合统计

排序统计

汇合中的元素能够按序排列,这种对元素保序的汇合类型叫作有序汇合。

Redis 汇合类型:List,Hash,Set,Sorted Set
有序汇合类型:

  • List:依照元素进入 List 的程序进行排序
  • Sorted Set:依据元素权重排序

List 问题:List 是通过元素地位来排序的,新元素插入后元素地位会扭转,分页操作时,Lrange 可能读取到旧数据。

所以,在面对须要展现最新列表、排行榜等场景时,如果数据更新频繁或者须要分页显示,倡议你优先思考应用 Sorted Set。

二值状态统计

在签到统计时,每个用户一天的签到用 1 个 bit 位就能示意,一个月(假如是 31 天)的签到状况用 31 个 bit 位就能够,而一年的签到也只须要用 365 个 bit 位,基本不必太简单的汇合类型。这个时候,咱们就能够抉择 Bitmap。

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

Bitmap 能够看做一个 bit 数组,提供 GETBIT/SETBIT 操作,应用偏移值 offset 对 bitmap 数组进行读写,offset 最小值为 0。

  • BITSET 后该 bit 位设置为 1;
  • BITCOUNT 统计所有为 1 的个数
  • BITOP 按位与

示例:统计 8 月 3 日签到数(3 日:0-1-2)

SETBIT uid:sign:3000:202008 2 1 
GETBIT uid:sign:3000:202008 2
BITCOUNT uid:sign:3000:202008

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

基数统计

基数统计就是指统计一个汇合中不反复的元素个数。

在 Redis 的汇合类型中,Set 类型默认反对去重。

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

示例:统计拜访页面 UV

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

HyperLogLog 的统计规定是基于概率实现的,所以它给出的统计后果是有肯定误差的,规范误算率是 0.81%。

小结

汇合类型优缺点:

其余统计场景:

  1. 应用 Sorted Set 统计在线用户数:

    • 用户上线时应用 zadd online_users $timestamp $user_id 把用户增加到 Sorted Set 中
    • 应用 zcount online_users $starttime $endtime 就能够得出指定时间段内的在线用户数
  2. 应用 Set 记录用户喜爱水果,如 sadd user1 apple banana,再应用zunionstore fruits_union 2 user1 user2 把后果存储到 fruits_union 这个 key 中,zrange fruits_union 0 -1 withscores能够得出每种水果被喜爱的次数。

13 GEO

地位信息服务(Location-Based Service,LBS)利用拜访的数据是和人或物关联的一组经纬度信息,而且要能查问相邻的经纬度范畴,GEO 就非常适合利用在 LBS 服务的场景中。

对于一个 LBS 利用来说,除了记录经纬度信息,还须要依据用户的经纬度信息在车辆的 Hash 汇合中进行范畴查问。

实际上,GEO 类型的底层数据结构就是用 Sorted Set 来实现的,元素是车辆 ID,元素的权重分数是 GeoHash 编码过的经纬度信息。

GeoHash 编码

二分区间,区间编码。
即先对经度和纬度别离编码,而后再把经纬度各自的编码组合成一个最终编码。

GeoHash 编码会把值编码成一个 N 位的二进制值,经度范畴 [-180,180],纬度范畴[-90,90] 做 N 次的二分区操作,其中 N 能够自定义。编码值落在左分区,咱们就用 0 示意;如果落在右分区,就用 1 示意。每做完一次二分区,咱们就能够失去 1 位编码值。

示例:经度值 116.37,纬度值 39.86 的编码过程

当一组经纬度值都编完码后,咱们再把它们的各自编码值组合在一起,组合的规定是:最终编码值的偶数位上顺次是经度的编码值,奇数位上顺次是纬度的编码值,其中,偶数位从 0 开始,奇数位从 1 开始。

应用 GeoHash 编码后,咱们相当于把整个天文空间划分成了一个个方格,每个方格对应了 GeoHash 中的一个分区。所以应用 Sorted Set 范畴查问失去的相近编码值,在理论的天文空间上,也是相邻的方格,这就能够实现 LBS 利用“搜寻左近的人或物”的性能了。

GEO 其实是把经纬度编码合并作为 sorted set 的 key,但有时编码相近并不一定是相邻方格,个别的做法是同时查问四周的 4 或 8 个方格

操作方法

  • GEOADD 命令:用于把一组经纬度信息和绝对应的一个 ID 记录到 GEO 类型汇合中
  • GEORADIUS 命令:会依据输出的经纬度地位,查找以这个经纬度为核心的肯定范畴内的其余元素。当然,咱们能够本人定义这个范畴。
GEOADD cars:locations 116.034579 39.030452 33
GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10

自定义数据类型

Redis 键值对中的每一个值都是用 RedisObject 保留的。

根本构造

RedisObject 的外部组成包含了 type、encoding、lru 和 refcount 4 个元数据,以及 1 个 *ptr 指针。

  • type:示意值的类型,涵盖了咱们后面学习的五大根本类型;
  • encoding:是值的编码方式,用来示意 Redis 中实现各个根本类型的底层数据结构,例如 SDS、压缩列表、哈希表、跳表等;
  • lru:记录了这个对象最初一次被拜访的工夫,用于淘汰过期的键值对;
  • refcount:记录了对象的援用计数;
  • *ptr:是指向数据的指针。

咱们在定义了新的数据类型后,也只有在 RedisObject 中设置好新类型的 type 和 encoding,再用 *ptr 指向新类型的实现,就行了。

Redis 其余数据类型和利用

List 队列:
rpush 入栈 + lpop 出栈。
毛病:不反对 ack,不反对多消费者。

PubSub:
反对多消费者。
毛病:PubSub 只能发给在线消费者,消费者下线会失落数据。

Stream 数据结构:
能够长久化、反对 ack 机制、反对多个消费者、反对回溯生产。

布隆过滤器:
解决业务层内存穿透。

14 工夫序列数据

与产生工夫相干的一组数据,就是工夫序列数据。
这些数据的特点是没有严格的关系模型,记录的信息能够示意成键和值的关系。

读写特点

写入特点:写入要快,数据类型在进行数据插入时,复杂度要低,尽量不要阻塞。

  1. 工夫序列数据通常是继续高并发写入的
  2. 一个工夫序列数据被记录后通常就不会变了

读取特点:查问模式多,单条记录查问,范畴查问,聚合计算等。
解决方案:基于 Hash 和 Sorted Set 实现,以及基于 RedisTimeSeries 模块实现。

Hash 和 Sorted Set 组合

保留工夫序列数据,同时存储 Hash 和 Sorted Set 两种类型。

Hash 类型

长处:能够实现对单键的疾速查问,满足了工夫序列数据的单键查问需要。
毛病:无奈范畴查找。

HGET device:temperature 202008030905
"25.1"

HMGET device:temperature 202008030905 202008030907 202008030908
1) "25.1"
2) "25.9"
3) "24.9"

Sorted Set

把工夫戳作为 Sorted Set 汇合的元素分数,把工夫点上记录的数据作为元素自身。

ZRANGEBYSCORE device:temperature 202008030907 202008030910
1) "25.9"
2) "24.9"
3) "25.3"
4) "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
2) (integer) 1

聚合计算

RedisTimeSeries 反对间接在 Redis 实例上进行聚合计算。

RedisTimeSeries 是 Redis 的一个扩大模块。它专门面向工夫序列数据提供了数据类型和拜访接口,并且反对在 Redis 实例上间接对数据进行按工夫范畴的聚合计算。

问题:如果你是 Redis 的开发维护者,你会把聚合计算也设计为 Sorted Set 的外在性能吗?
解答:
不会。因为聚合计算是 CPU 密集型工作,Redis 在解决申请时是单线程的,也就是它在做聚合计算时无奈利用到多核 CPU 来晋升计算速度,如果计算量太大,这也会导致 Redis 的响应提早变长,影响 Redis 的性能。

Redis 的定位就是高性能的内存数据库,要求访问速度极快。所以对于时序数据的存储和聚合计算,我感觉更好的形式是交给时序数据库去做,时序数据库会针对这些存储和计算的场景做针对性优化。

15 音讯队列

音讯队列在存取音讯时,必须要满足三个需要,别离是音讯保序、解决反复的音讯和保障音讯可靠性。

Redis 的 List 和 Streams 两种数据类型,就能够满足音讯队列的这三个需要。

List 解决方案

音讯保序

生产者能够应用 LPUSH 命令把要发送的音讯顺次写入 List,而消费者则能够应用 RPOP 命令,从 List 的另一端依照音讯的写入程序,顺次读取音讯并进行解决。

性能危险:消费者须要不停执行 RPOP,造成性能损失。
解决方案:阻塞式读取 BRPOP,客户端在没有读到队列数据时,主动阻塞,直到有新的数据写入队列,再开始读取新数据。

反复生产

要求:消费者程序自身能对反复音讯进行判断。
计划:音讯队列给每一个音讯提供全局惟一的 ID 号;消费者程序要把曾经解决过的音讯的 ID 号记录下来。

幂等性:对于同一条音讯,消费者收到一次的处理结果和收到屡次的处理结果是统一的。

可靠性

List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取音讯,同时,Redis 会把这个音讯再插入到另一个 List(能够叫作备份 List)留存。

这样一来,如果消费者程序读了音讯但没能失常解决,等它重启后,就能够从备份 List 中从新读取音讯并进行解决了。

处理过程:生产者先用 LPUSH 把音讯插入到音讯队列 mq 中。消费者程序应用 BRPOPLPUSH 命令读取音讯,同时音讯还会被 Redis 插入到 mqback 队列中。如果消费者程序处理音讯时宕机了,等它重启后,能够从 mqback 中再次读取音讯,持续解决。

问题:生产者音讯发送很快,而消费者解决音讯的速度比较慢,这就导致 List 中的音讯越积越多,给 Redis 的内存带来很大压力。
解决:应用 streams 计划,启动多个消费者程序组成一个生产组,一起分担解决 List 中的音讯。

Streams 解决方案

Streams 是 Redis 专门为音讯队列设计的数据类型,它提供了丰盛的音讯队列操作命令。

  • XADD:插入音讯,保障有序,能够主动生成全局惟一 ID;
  • XREAD:用于读取音讯,能够按 ID 读取数据;
  • XREADGROUP:按生产组模式读取音讯;
  • XPENDING:用来查问每个生产组内所有消费者已读取但尚未确认的音讯;
  • XACK:用于向音讯队列确认音讯解决已实现。

XADD

XADD 命令能够往音讯队列中插入新音讯,音讯的格局是键 – 值对模式。对于插入的每一条音讯,Streams 能够主动为其生成一个全局惟一的 ID。

XADD mqstream * repo 5
"1599203861727-0"

* 示意插入数据主动生成全局惟一 ID,也能够自行设定。

XREAD

XREAD 在读取音讯时,能够指定一个音讯 ID,并从这个音讯 ID 的下一条音讯开始进行读取。

XREAD BLOCK 100 STREAMS mqstream 1599203861727-0

调用 XRAED 时设定 block 配置项,实现相似于 BRPOP 的阻塞读取操作。

XREAD block 10000 streams mqstream $
(nil)
(10.00s)

“$”符号示意读取最新的音讯,XREAD 没有新音讯时阻塞 10000 毫秒(即 10 秒),而后返回 nil。

XGROUP 创立生产组

Streams 自身能够应用 XGROUP 创立生产组,创立生产组之后,Streams 能够应用 XREADGROUP 命令让生产组内的消费者读取音讯。

XGROUP create mqstream group1 0

XREADGROUP group group1 consumer1 streams mqstream >

让 group1 生产组里的消费者 consumer1 从 mqstream 中读取所有音讯,其中,命令最初的参数“>”,示意从第一条尚未被生产的音讯开始读取。

留神:音讯队列中的音讯一旦被生产组里的一个消费者读取了,就不能再被该生产组内的其余消费者读取了。

XPENDING

为了保障消费者在产生故障或宕机再次重启后,依然能够读取未解决完的音讯,Streams 会主动应用外部队列(也称为 PENDING List)留存生产组里每个消费者读取的音讯,直到消费者应用 XACK 命令告诉 Streams“音讯曾经解决实现”。

如果消费者没有胜利解决音讯,它就不会给 Streams 发送 XACK 命令,音讯依然会留存。此时,消费者能够在重启后,用 XPENDING 命令查看已读取、但尚未确认解决实现的音讯。

查看 group2 中消费者组已读取、但未确认的音讯

XPENDING mqstream group2

查问指定消费者

XPENDING mqstream group2 - + 10 consumer2

XACK

解决音讯后,消费者能够应用 XACK 命令告诉 Streams,而后这条音讯就会被删除。

退出移动版