Redis缓存数据库进阶——Redis高级特性与应用(4)

一、Redis的慢查询与配置

许多存储系统(例如 MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能。

Redis客户端执行一条命令分为如下4个部分:

需要注意,慢查询只统计步骤3的时间,所以没有慢查询并不代表客户端没有超时问题。因为有可能是命令的网络问题或者是命令在Redis在排队,所以不是说命令执行很慢就说是慢查询,而有可能是网络的问题或者是Redis服务非常繁忙(队列等待长)。

慢查询配置

对于任何慢查询功能,需要明确两件事:多慢算慢,也就是预设阀值怎么设置?慢查询记录存放在哪?

Redis提供了两种方式进行慢查询的配置

1、慢查询动态设置

慢查询的阈值默认值是10毫秒

参数:slowlog-log-slower-than就是时间预设阀值,它的单位是微秒(1秒=1000毫秒=1 000 000微秒),默认值是10 000,假如执行了一条“很慢”的命令(例如keys *),如果它的执行时间超过了10 000微秒,也就是10毫秒,那么它将被记录在慢查询日志中。

查看慢查询的阈值

cat /usr/local/redis/redis.conf

注意:

如果配置slowlog-log-slower-than=0表示会记录所有的命令,slowlog-log-slower-than<0对于任何命令都不会进行记录。

2、配置文件设置(修改后需重启服务才生效)

config set slowlog-log-slower-than 20000 

使用config set完后,若想将配置持久化保存到Redis.conf,要执行config rewrite

config rewrite

打开Redis的配置文件redis.conf,就可以看到以下配置:

slowlog-max-len用来设置慢查询日志最多存储多少条,解决存储空间的问题。

实际上Redis服务器将所有的慢查询日志保存在服务器状态的slowlog链表中(内存列表),slowlog-max-len就是列表的最大长度(默认128条)。当慢查询日志列表被填满后,新的慢查询命令则会继续入队,队列中的第一条数据机会出列。

虽然慢查询日志是存放在Redis内存列表中的,但是Redis并没有告诉我们这里列表是什么,而是通过一组命令来实现对慢查询日志的访问和管理。并没有说明存放在哪。这个怎么办呢?Redis提供了一系列的慢查询操作命令让我们可以方便的操作。

2、慢查询操作命令

虽然慢查询日志是存放在Redis内存列表中的,但是Redis并没有告诉我们这里列表是什么,而是通过一组命令来实现对慢查询日志的访问和管理。并没有说明存放在哪。这个怎么办呢?Redis提供了一些列的慢查询操作命令让我们可以方便的操作。

获取慢查询日志

slowlog get n

参数n可以指定查询条数。

可以看到每个慢查询日志有6个属性组成,分别是慢查询日志的标识id、发生时间戳、命令耗时(单位微秒)、执行命令和参数,客户端IP+端口和客户端名称。

获取慢查询日志列表当前的长度

slowlog len

慢查询日志重置,实际是对列表做清理操作

slowlog reset

3、慢查询建议

慢查询功能可以有效地帮助我们找到Redis可能存在的瓶颈,但在实际使用过程中要注意以下几点:

slowlog-max-len配置建议:

建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,线上生产建议设置为1000以上。

slowlog-log-slower-than配置建议: 配置建议:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。

由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒或者更低比如100微秒。

慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞。

二、慢查询原理及推荐方案

Redis慢查询是Redis提供的一项性能优化功能,它主要基于Redis的事务机制和日志记录功能实现。Redis的事务机制可以确保多个操作要么全部执行,要么全部不执行,这有助于提高数据的一致性。同时,事务机制会开启一个事务日志,用于记录每个操作的详细信息,包括执行时间、命令参数等。通过这些信息,Redis能够识别出执行时间超过预设阈值的慢查询,并将它们记录下来。

具体来说,Redis慢查询的实现依赖于以下几个关键点:

  1. 慢查询阈值:Redis允许用户设置一个慢查询的阈值(以微秒为单位),只有当查询的执行时间超过这个阈值时,该查询才会被视为慢查询并被记录下来。
  2. 慢查询日志:Redis使用一个循环队列来保存慢查询日志,当队列满了之后,最早的日志会被新的日志覆盖。这样,Redis就可以持续记录最近的慢查询情况,而不会消耗过多的内存资源。
  3. 慢查询命令:Redis提供了多个命令来操作慢查询日志,如SLOWLOG GETSLOWLOG LENSLOWLOG RESET等,这些命令允许用户查询、获取慢查询日志的长度以及清空慢查询日志。



    推荐方案

    针对Redis慢查询问题,以下是一些推荐的处理方案:

  • 优化查询语句
    • 分析慢查询日志中的查询语句,找出执行时间长的查询语句并进行优化。
    • 尽量避免在Redis中进行复杂的计算或逻辑处理,保持查询语句的简洁性。
    • 对于复杂的查询需求,考虑使用Redis的管道(pipeline)或Lua脚本来减少网络往返次数和提高处理效率。
  • 调整数据结构
    • 根据数据的实际使用情况,选择合适的Redis数据结构来存储数据。不同的数据结构在查询性能上存在差异,例如,哈希表(hash)和有序集合(sorted set)在查询特定字段时性能较好。
    • 避免使用过大的键值对,尤其是避免在单个键中存储大量数据,以减少内存使用和提高查询性能。
  • 增加Redis实例或分片
    • 当单个Redis实例无法满足应用的需求时,可以考虑增加Redis实例或使用Redis分片来分散负载和提高性能。
    • 对于热点数据或热点键,可以考虑使用分片技术将数据分散到多个Redis节点上,以降低单个节点的负载压力。
  • 优化Redis配置
    • 根据实际情况调整Redis的配置参数,如内存限制、持久化策略、连接数限制等,以优化Redis的性能表现。
    • 启用Redis的压缩算法来减少内存使用,同时保证数据的完整性。
  • 使用监控和告警工具
    • 使用Redis监控工具或第三方监控工具来实时监控Redis的性能指标和状态,及时发现并解决潜在的性能问题。
    • 设置告警规则,当Redis的CPU使用率、内存使用率等关键指标超过预设阈值时,及时通知运维人员进行处理。
  • 定期维护和优化
    • 定期对Redis进行维护和优化工作,如清理过期的键、执行碎片整理等,以保持Redis的性能稳定。
    • 关注Redis的版本更新和官方推荐的最佳实践,及时升级Redis版本并应用最佳实践来优化Redis的性能表现。

通过以上方案的综合应用,可以有效地解决Redis慢查询问题,提高Redis的性能表现。

 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中。

三、Pipeline命令

其中1和4花费的时间称为Round Trip Time (RTT,往返时间),也就是数据在网络上传输的时间。

Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。

但大部分命令是不支持批量操作的,例如要执行n次 hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。

举例:Redis的客户端和服务端可能部署在不同的机器上。例如客户端在本地,Redis服务器在阿里云的广州,两地直线距离约为800公里,那么1次RTT时间=800 x2/ ( 300000×2/3 ) =8毫秒,(光在真空中传输速度为每秒30万公里,这里假设光纤为光速的2/3 )。而Redis命令真正执行的时间通常在微秒(1000微妙=1毫秒)级别,所以才会有Redis 性能瓶颈是网络这样的说法。

Pipeline(流水线)机制能改善上面这类问题,它能将一组 Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,没有使用Pipeline执行了n条命令,整个过程需要n次RTT。

四、普通命令和pipeline性能对比实战

Pipeline并不是什么新的技术或机制,很多技术上都使用过。而且RTT在不同网络环境下会有不同,例如同机房和同机器会比较快,跨机房跨地区会比较慢。

redis-cli的--pipe选项实际上就是使用Pipeline机制,但绝对部分情况下,我们使用Java语言的Redis客户端中的Pipeline会更多一点。

差距有100多倍,可以得到如下两个结论:

1、Pipeline执行速度一般比逐条执行要快。

2、客户端和服务端的网络延时越大,Pipeline的效果越明显。

Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成,比如可以将Pipeline的总发送大小控制在内核输入输出缓冲区大小之内或者控制在单个TCP 报文最大值1460字节之内。

内核的输入输出缓冲区大小一般是4K-8K,不同操作系统会不同(当然也可以配置修改)

最大传输单元(Maximum Transmission Unit,MTU),这个在以太网中最大值是1500字节。那为什么单个TCP 报文最大值是1460,因为因为还要扣减20个字节的IP头和20个字节的TCP头,所以是1460。

同时Pipeline只能操作一个Redis实例,但是即使在分布式Redis场景中,也可以作为批量操作的重要优化手段。


五、pipeline与事务的区别

 Redis的事务

大家应该对事务比较了解,简单地说,事务表示一组动作,要么全部执行,要么全部不执行。

例如在社交网站上用户A关注了用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据不一致的情况。

Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi 命令代表事务开始,exec命令代表事务结束。另外discard命令是回滚。

一个客户端

另外一个客户端

在事务没有提交的时查询(查不到数据)

事务提交

可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中的一个缓存队列(所以discard也只是丢弃这个缓存队列中的未执行命令,并不会回滚已经操作过的数据,这一点要和关系型数据库的Rollback操作区分开)。

只有当exec执行后,用户A关注用户B的行为才算完成,如下所示exec返回的两个结果对应sadd命令。

Redis的watch命令

六、redis与Lua脚本

Redis中的Lua

eval 命令

EVAL script numkeys key [key ...] arg [arg ...]

命令说明

1、script 参数:

是一段 Lua 脚本程序,它会被运行在Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。

2、numkeys 参数:

用于指定键名参数的个数。

3、key [key...] 参数: 从EVAL 的第三个参数开始算起,使用了 numkeys 个键(key),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用1为基址的形式访问(KEYS[1],KEYS[2]···)。

4、arg [arg...]参数:

可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似(ARGV[1],ARGV[2]···)。

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

这个命令的含义如下:

  • 执行一个 Lua 脚本,该脚本返回一个包含四个元素的 Lua 表(在 Redis 中,Lua 表被用于表示数组或集合)。
  • numkeys 是 2,表示接下来的两个参数 key1 和 key2 是键(key)。
  • key1 和 key2 是传递给脚本的键(key),在 Lua 脚本中通过 KEYS[1] 和 KEYS[2] 访问。
  • first 和 second 是传递给脚本的附加参数,在 Lua 脚本中通过 ARGV[1] 和 ARGV[2] 访问。

因此,Lua 脚本 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 的执行结果是一个包含四个字符串的表:key1key2firstsecond。但是,需要注意的是,Redis 的 EVAL 命令本身并不直接返回 Lua 表;相反,它会将 Lua 表的元素作为 Redis 回复的多个部分返回。

所以,当你执行这个命令时,Redis 会返回四个字符串:key1key2firstsecond,每个字符串作为回复的一个独立部分。这可以用于在单个 Redis 命令中处理多个值,而无需多次往返于客户端和服务器之间。

Lua 脚本中调用 Redis 命令

这里我们主要记住 call() 命令即可:

eval "return redis.call('mset',KEYS[1],ARGV[1],KEYS[2],ARGV[2])" 2 key1 key2 first second

这个命令的含义和执行流程如下:

  1. 执行Lua脚本:Redis服务器会执行提供的Lua脚本字符串。

  2. 脚本参数

    • KEYS[1] 和 KEYS[2] 分别被替换为命令中指定的 key1 和 key2
    • ARGV[1] 和 ARGV[2] 分别被替换为命令中指定的 first 和 second
  3. Lua脚本内容:脚本内部调用了 redis.call 函数,这个函数允许Lua脚本直接执行Redis命令。在这个例子中,它执行了 mset 命令。

  4. mset命令mset 命令用于同时设置多个键值对。如果键已经存在,则它们会被覆盖。这个命令接受偶数个参数,其中第一个参数是键,第二个参数是与该键相关联的值,依此类推。

  5. 返回值:Lua脚本通过 return 语句返回 redis.call('mset', ...) 的结果。但是,需要注意的是,mset 命令本身不返回任何值(在Redis的协议中,它返回的是一个简单的状态回复,通常是 "OK"),因此在Lua脚本中返回的是 nil(或者更具体地说,是Lua的nil值,它在Redis的返回值中通常被转换为空回复)。

  6. Redis客户端接收到的响应:由于 mset 命令不返回具体的值,并且Lua脚本返回了 nil,因此Redis客户端将接收到一个空回复(或者在某些客户端库中,这可能被转换为 nullNone 或类似的空值)。

总结来说,这个命令的作用是同时设置两个键值对(key1 -> first 和 key2 -> second),但客户端不会接收到任何表示这些键值对已成功设置的直接反馈(除了知道命令已成功执行且没有错误发生)。如果需要检查这些键的值,你需要单独地执行 GET 命令来获取它们。

在这个范例中key [key ...] 参数的作用不明显,其实它最大的作用是方便我们在Lua 脚本中调用 Redis 命令

evalsha 命令

但是eval命令要求你在每次执行脚本的时候都发送一次脚本,所以Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗, Redis 提供了evalsha 命令,它的作用和 EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 摘要。

这里就需要借助script命令。

script flush :清除所有脚本缓存。

script exists :根据给定的脚本校验,检查指定的脚本是否存在于脚本缓存。

script load :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它。

script kill :杀死当前正在运行的脚本。

这里的 SCRIPT LOAD 命令就可以用来生成脚本的 SHA1 摘要

script load "return redis.call('set',KEYS[1],ARGV[1])"

在Redis中,SCRIPT LOAD 命令用于将一段Lua脚本加载到Redis服务器的脚本缓存中。这个命令不会执行脚本,而是将脚本存储在内存中,并返回一个唯一的SHA1哈希值,该哈希值对应于加载的脚本。之后,你可以使用 EVALSHA 命令和这个哈希值来执行之前加载的脚本,这通常比直接使用 EVAL 命令更高效,因为 EVALSHA 命令不需要再次发送脚本本身到服务器。

这个命令的含义是:

  1. 加载Lua脚本:将给定的Lua脚本字符串 "return redis.call('set',KEYS[1],ARGV[1])" 加载到Redis服务器的脚本缓存中。

  2. 脚本内容:脚本内容是一个简单的Redis命令调用,它使用 redis.call 函数来执行Redis的 set 命令。set 命令用于设置给定键的值。在这个脚本中,KEYS[1] 对应于要设置的键,而 ARGV[1] 对应于该键的新值。

  3. 返回值SCRIPT LOAD 命令返回一个SHA1哈希值,该哈希值是加载到脚本缓存中的脚本的唯一标识符。你可以使用这个哈希值在后续的 EVALSHA 命令中执行该脚本。

然后就可以执行这个脚本,返回哈希值 

evalsha "c686f316aaf1eb01d5a4de1b0b63cd233010e63d" 1 key1 testscript

1 是 KEYS 数组的长度(即你将要传递给脚本的键的数量),key1 是你想要设置的键,testscript是你想要设置的值。注意,由于 EVALSHA 命令的语法与 EVAL 命令类似,但它使用哈希值而不是脚本本身,因此你需要提供正确的 KEYS 和 ARGV 参数。

如果你发现 EVALSHA 命令因为某种原因(如脚本已被从缓存中逐出)而失败,Redis将返回一个错误,并允许你回退到使用 EVAL 命令和完整的脚本字符串。

七、Redis结合Lua脚本限流实战

 滑动窗口算法

在线演示滑动窗口:

Selective Repeat Protocol

滑动窗口通俗来讲就是一种流量控制技术。

它本质上是描述接收方的TCP数据报缓冲区大小的数据,发送方根据这个数据来计算自己最多能发送多长的数据,如果发送方收到接收方的窗口大小为0的TCP数据报,那么发送方将停止发送数据,等到接收方发送窗口大小不为0的数据报的到来。

首先是第一次发送数据这个时候的窗口大小是根据链路带宽的大小来决定的。我们假设这个时候窗口的大小是3。这个时候接受方收到数据以后会对数据进行确认告诉发送方我下次希望手到的是数据是多少。这里我们看到接收方发送的ACK=3(这是发送方发送序列2的回答确认,下一次接收方期望接收到的是3序列信号)。这个时候发送方收到这个数据以后就知道我第一次发送的3个数据对方只收到了2个。就知道第3个数据对方没有收到。下次在发送的时候就从第3个数据开始发。

此时窗口大小变成了2 。

于是发送方发送2个数据。看到接收方发送的ACK是5就表示他下一次希望收到的数据是5,发送方就知道我刚才发送的2个数据对方收了这个时候开始发送第5个数据。

这就是滑动窗口的工作机制,当链路变好了或者变差了这个窗口还会发生变话,并不是第一次协商好了以后就永远不变了。

所以滑动窗口协议,是TCP使用的一种流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。

只有在接收窗口向前滑动时(与此同时也发送了确认),发送窗口才有可能向前滑动。

收发两端的窗口按照以上规律不断地向前滑动,因此这种协议又称为滑动窗口协议。

TCP中的滑动窗口

发送方和接收方都会维护一个数据帧的序列,这个序列被称作窗口。发送方的窗口大小由接收方确认,目的是控制发送速度,以免接收方的缓存不够大导致溢出,同时控制流量也可以避免网络拥塞。

在TCP 的可靠性的图中,我们可以看到,发送方每发送一个数据接收方就要给发送方一个ACK对这个数据进行确认。只有接收了这个确认数据以后发送方才能传输下个数据。

存在的问题:如果窗口过小,当传输比较大的数据的时候需要不停的对数据进行确认,这个时候就会造成很大的延迟。

如果窗口过大,我们假设发送方一次发送100个数据,但接收方只能处理50个数据,这样每次都只对这50个数据进行确认。发送方下一次还是发送100个数据,但接受方还是只能处理50个数据。这样就避免了不必要的数据来拥塞我们的链路。

因此,我们引入了滑动窗口。

漏洞算法

定义

先有一个桶,桶的容量是固定的。

以任意速率向桶流入水滴,如果桶满了则溢出(被丢弃)。

桶底下有个洞,按照固定的速率从桶中流出水滴。

特点

漏桶核心是:请求来了以后,直接进桶,然后桶根据自己的漏洞大小慢慢往外面漏。

具体实现的时候要考虑性能(比如Redis实现的时候数据结构的操作是不是会导致性能问题)

令牌算法

定义

先有一个桶,容量是固定的,是用来放令牌的。

以固定速率向桶放令牌,如果桶满了就不放令牌了。

Ø处理请求是先从桶拿令牌,先拿到令牌再处理请求,拿不到令牌同样也被限流了。

特点

突发情况下可以一次拿多个令牌进行处理。

具体实现的时候要考虑性能(比如Redis实现的时候数据结构的操作是不是会导致性能问题)

发布订阅

八、Redis中的发布和订阅

 Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道( channel)发布消息,订阅该频道的每个客户端都可以收到该消息。

操作命令

Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令。

发布消息
publish channel message

返回值是接收到信息的订阅者数量,如果是0说明没有订阅者,这条消息就丢了(再启动订阅者也不会收到)。

订阅消息

订阅者可以订阅一个或多个频道,如果此时另一个客户端发布一条消息,当前订阅者客户端会收到消息。

如果有多个客户端同时订阅了同一个频道,都会收到消息。

客户端在执行订阅命令之后进入了订阅状态(类似于监听),只能接收subscribe、psubscribe,unsubscribe、 punsubscribe的四个命令。

查询订阅情况
查看活跃的频道

pubsub channels [pattern]

Pubsub 命令用于查看订阅与发布系统状态,包括活跃的频道(是指当前频道至少有一个订阅者),其中[pattern]是可以指定具体的模式,类似于通配符。

查看频道订阅数

pubsub numsub channel

最后也可以通过 help看具体的参数运用

使用场景和缺点

需要消息解耦又并不关注消息可靠性的地方都可以使用发布订阅模式。

PubSub 的生产者传递过来一个消息,Redis会直接找到相应的消费者传递过去。如果一个消费者都没有,那么消息直接丢弃。如果开始有三个消费者,一个消费者突然挂掉了,生产者会继续发送消息,另外两个消费者可以持续收到消息。但是挂掉的消费者重新连上的时候,这断连期间生产者发送的消息,对于这个消费者来说就是彻底丢失了。

所以和很多专业的消息队列系统(例如Kafka、RocketMQ)相比,Redis 的发布订阅很粗糙,例如无法实现消息堆积和回溯。但胜在足够简单,如果当前场景可以容忍的这些缺点,也不失为一个不错的选择。

正是因为 PubSub 有这些缺点,它的应用场景其实是非常狭窄的。从Redis5.0 新增了 Stream 数据结构,这个功能给 Redis 带来了持久化消息队列,我们马上将要学习到。

九、Redis中的Stream及玩法

Redis Stream 的结构如上图所示,每一个Stream都有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。消息是持久化的,Redis 重启后,内容还在。

 1、xadd 追加消息

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用xadd指令追加消息时自动创建。

创建了一个名为kiki的Stream,id1="1721530924109-0"

消息 ID 的形式是timestampInMillis-sequence,例如1527846880572-5(时间戳-序号 ),它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第 5 条消息。

消息 ID 可以由服务器自动生成(*代表默认自动),也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的 ID 要大于前面的消息 ID。

2、xrange 获取消息列表,会自动过滤已经删除的消息

xrange streamtest - +       -表示最小值 , + 表示最大值

3、xlen 消息长度

xlen streamname

4、del 删除 Stream

十、Stream的运用

十一、Stream与消息队列中的问题

十二、基于Stream在Redis中实现消息队列

生产端

消费端

单消费者

虽然Stream中有消费者组的概念,但是可以在不定义消费组的情况下进行 Stream 消息的独立消费,当 Stream 没有新消息时,甚至可以阻塞等待。Redis 设计了一个单独的消费指令xread,可以将 Stream 当成普通的消息队列 (list) 来使用。使用 xread 时,我们可以完全忽略消费组 (Consumer Group) 的存在,就好比 Stream 就是一个普通的列表 (list)。

xread count 1 streams stream5 0-0

 xread count 2 streams stream5 1721546388146-0

xread count 1 streams stream5 $

应该以阻塞的方式读取尾部最新的一条消息,直到新的消息的到来

xread block 0 count 1 streams stream5 $

一般来说客户端如果想要使用 xread 进行顺序消费,一定要记住当前消费到哪里了,也就是返回的消息 ID。下次继续调用 xread 时,将上次返回的最后一个消息 ID 作为参数传递进去,就可以继续消费后续的消息。不然很容易重复消息,基于这点单消费者基本上没啥运用场景。

消费组

创建消费组

Stream 通过xgroup create指令创建消费组 (Consumer Group),需要传递起始消息 ID 参数用来初始化last_delivered_id变量。

0-表示从头开始消费

①XADD streamTest * rongli good

   XADD streamTest * jbl nice

向名为 streamTest 的 Stream 中添加一条新的消息。

这个命令是 Redis Stream 数据类型的一部分,用于处理消息队列和日志等场景。

让我们分解这个命令:

  • XADD:这是 Redis 的一个命令,用于向 Stream 添加新的消息。
  • streamTest:这是 Stream 的名称,即你想要添加消息到的 Stream 的键名。
  • *:这个星号是一个特殊的 ID 生成器,它告诉 Redis 为这条新消息生成一个唯一的 ID。Redis 会使用当前时间戳和序列号来生成这个 ID,确保每条消息的 ID 都是唯一的,并且按照添加的顺序递增。
  • rongli good:这是消息的内容部分,但实际上它是被解析为字段和值的列表。在这个例子中,它看起来像是只有一个字段(rongli)和一个值(good),但实际上 Redis 将 rongli 视为字段名,将 good 视为该字段的值。如果你的意图是添加一个简单的消息而不是键值对,你可能想要使用不同的格式(但 Redis Stream 本质上是设计来存储键值对的)。不过,对于简单的用例,你可以将字段名视为消息标识符,值视为消息内容。

②xgroup create streamTest cg1 $

用于在 Redis Stream streamTest 中创建一个新的消费者组 c1。这个命令的组成部分如下:

  • XGROUP CREATE:这是 Redis 的一个子命令,用于创建新的消费者组。
  • streamTest:这是你要在其中创建消费者组的 Stream 的名称。
  • c1:这是你想要创建的新消费者组的名称。
  • 0-0:这是消费者组的起始消息 ID。在这个例子中,0-0 是一个特殊的 ID,表示消费者组将从 Stream 的第一个消息开始消费(假设 Stream 中已经存在消息)。然而,如果 Stream 是空的(即没有消息),这个 ID 实际上不会立即影响消费者组的行为,因为消费者组会等待新消息的到来。
  • $ 表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略

消息消费

有了消费组,自然还需要消费者,Stream提供了 xreadgroup 指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息 ID。

它同 xread 一样,也可以阻塞等待新消息。读到新消息后,对应的消息 ID 就会进入消费者的PEL(正在处理的消息) 结构里,客户端处理完毕后使用 xack 指令通知服务器,本条消息已经处理完毕,该消息 ID 就会从 PEL 中移除。

具体操作细节可以参考:xpending 命令 -- Redis中国用户组(CRUG)

命令XCLAIM[kleɪm]用以进行消息转移的操作,将某个消息转移到自己的Pending[ˈpendɪŋ]列表中。需要设置组、转移的目标消费者和消息ID,同时需要提供IDLE(已被读取时长),只有超过这个时长,才能被转移。

十三、Redis中Stream的实现

十四、Redis的Key和Value的数据结构组织

哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到。因为这个哈希表保存了所有的键值对,所以,我也把它称为全局哈希表。

哈希表的最大好处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对:我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。

但当你往 Redis 中写入大量数据后,就可能发现操作有时候会突然变慢了。这其实是因为你忽略了一个潜在 的风险点,那就是哈希表的冲突问题和 rehash 可能带来的操作阻塞。

当你往哈希表中写入更多数据时,哈希冲突是不可避免的问题。这里的哈希冲突,两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中。

Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

当然如果这个数组一直不变,那么hash冲突会变很多,这个时候检索效率会大打折扣,所以Redis就需要把数组进行扩容(一般是扩大到原来的两倍),但是问题来了,扩容后每个hash桶的数据会分散到不同的位置,这里设计到元素的移动,必定会阻塞IO,所以这个ReHash过程会导致很多请求阻塞。

渐进式rehash

为了避免这个问题,Redis 采用了渐进式 rehash。

首先、Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash。

1、给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍

2、把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中

3、释放哈希表 1 的空间

在上面的第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

在Redis 开始执行 rehash,Redis仍然正常处理客户端请求,但是要加入一个额外的处理:

处理第1个请求时,把哈希表 1中的第1个索引位置上的所有 entries 拷贝到哈希表 2 中

处理第2个请求时,把哈希表 1中的第2个索引位置上的所有 entries 拷贝到哈希表 2 中

如此循环,直到把所有的索引位置的数据都拷贝到哈希表 2 中。

这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

所以这里基本上也可以确保根据key找value的操作在O(1)左右。

不过这里要注意,如果Redis中有海量的key值的话,这个Rehash过程会很长很长,虽然采用渐进式Rehash,但在Rehash的过程中还是会导致请求有不小的卡顿。并且像一些统计命令也会非常卡顿:比如keys

按照Redis的配置每个实例能存储的最大的key的数量为2的32次方,即2.5亿,但是尽量把key的数量控制在千万以下,这样就可以避免Rehash导致的卡顿问题,如果数量确实比较多,建议采用分区hash存储。root

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值