redis 4.0和5.0 新功能介绍

Lazyfree

redis-4.0带来的Lazyfree机制可以避免del,flushdb/flushall,rename等命令引起的redis-server阻塞,提高服务稳定性。

unlink

在redis-4.0之前,redis执行del命令会在释放掉key的所有内存以后才会返回OK,这在key比较大的时候(比如说一个hash里头有1000W条数据),其他连接可能要等待很久。为了兼容已有的del语义,redis-4.0引入unlink命令,效果以及用法和del完全一样,但内存释放动作放到后台线程中执行。

UNLINKkey[key ...]

flushdb/flushall

flushdb/flushall在redis-4.0中新引入了选项,可以指定是否使用Lazyfree的方式来清空整个内存。

FLUSHALL[ASYNC]FLUSHDB[ASYNC]

rename

执行 rename oldkey newkey 时,如果newkey已经存在,redis会先删除,这也会引发上面提到的删除大key问题,如果想让redis在这种场景下也使用lazyfree的方式来删除,可以在控制台上打开如下配置:

lazyfree-lazy-server-del yes/no

其他场景

某些用户对数据设置过期时间,依赖redis的淘汰机制去删除已经过期的数据,这同样也存在上面提到的问题,淘汰某个大key会导致进程CPU出现抖动,redis-4.0提供了两个配置,可以让redis在淘汰或者逐出数据时也使用lazyfree的方式。

lazyfree-lazy-evictionyes/nolazyfree-lazy-expireyes/no

新增命令

swapdb

swapdb命令会交换两个db的数据,swapdb执行之后用户连接无需再执行select,即可看到新的数据。

127.0.0.1:6379>select0OK127.0.0.1:6379>setkeyvalue0OK127.0.0.1:6379>select1OK127.0.0.1:6379[1]>setkeyvalue1OK127.0.0.1:6379[1]>swapdb0 1OK127.0.0.1:6379[1]>getkey"value0" 127.0.0.1:6379[1]>select0OK127.0.0.1:6379>getkey"value1"

zlexcount

zlexcount命令用于sorted set中,和zrangebylex类似,不同的是zrangebylex返回member,而zlexcount是返回符合条件的member个数。

memory

redis-4.0之前只能通过info memory来了解redis内部有限的内存信息,4.0提供了memory命令,帮助用户全面了解redis的内存状态。

127.0.0.1:6379>memory help1)"MEMORY DOCTOR - Outputs memory problems report"2)"MEMORY USAGE <key> [SAMPLES <count>] - Estimate memory usage of key"3)"MEMORY STATS - Show memory usage details"4)"MEMORY PURGE - Ask the allocator to release memory"5)"MEMORY MALLOC-STATS - Show allocator internal stats"

  • memory usage


usage子命令可以查看某个key在redis内部实际占用多少内存,这里有两点需要说明:


  1. 不光key, value需要占用内存,redis管理这些数据还需要一部分内存
  2. 对于hash, list, set, sorted set这些类型,结果是采样计算的,可以通过SAMPLES 来控制采样数量
  • memory stats
27.0.0.1:6379> memory stats1)"peak.allocated"// redis从启动到现在,历史最多使用过多少内存2) (integer)4239959523)"total.allocated"//当前使用内存4) (integer)111303205)"startup.allocated"//redis启动初始化以后占用内存6) (integer)99429287)"replication.backlog"//主从复制断开重连时会用到,默认10MB8) (integer)10485769)"clients.slaves"// 主从复制用到的内存10) (integer)1685811)"clients.normal"//普通用户客户端的读写缓冲区12) (integer)4963013)"aof.buffer"//aof持久化使用的缓存和aofrewrite时产生的缓存之和14) (integer)325315)"db.0"//每个db的元数据所占用内存16)1)"overhead.hashtable.main"2) (integer)58083)"overhead.hashtable.expires"//管理带过期时间的数据所额外消耗内存4) (integer)10417)"overhead.total"//上面提到的各项内存消耗之和18) (integer)1106390419)"keys.count"//当前存储的key的总量20) (integer)9421)"keys.bytes-per-key"//当前内存中平均每个key大小22) (integer)1263123)"dataset.bytes"//用户数据所占用内存(= 总内存 - redis元数据所占内存)24) (integer)6641625)"dataset.percentage"//100 * dataset.bytes / (total.allocated - startup.allocated)26)"5.5934348106384277"27)"peak.percentage"// 100 * total.allocated / peak_allocated28)"2.6251003742218018"29)"fragmentation"//内存碎片率30)"1.1039986610412598"
  • memory doctor


主要用于给一些诊断建议,提前发现潜在问题。

Peakmemory: peak.allocated/total.allocated >1.5,此时内存碎片率可能比较高 High fragmentation: fragmentation >1.4,此时碎片率比较高 Big slave buffers: 每个slave缓冲区的平均内存超过10MB,原因可能是master写入流量过高 Big client buffers: 普通客户端缓冲区的平均内存超过200KB,原因可能是pipeline使用不当或者Pub/Sub客户端处理消息不及时导致

  • malloc stats & malloc purge


这两个用于操作jemalloc,只在使用jemalloc的时候才有效。

LFU&hotkey

redis-4.0新增了 allkey-lfu 和 volatile-lfu 两种数据逐出策略,同时还可以通过object命令来获取某个key的访问频度。

objectfreq user_key

基于LFU机制,用户可以使用 scan + object freq 来发现热点key,当然redis也一起发布了更好用的工具——redis-cli,使用实例如下所示。

$./redis-cli --hotkeys# Scanning the entire keyspace to find hot keys as well as# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec# per 100 SCAN commands (not usually needed).[00.00%] Hot key'counter:000000000002'found so farwithcounter87[00.00%] Hot key'key:000000000001'found so farwithcounter254[00.00%] Hot key'mylist'found so farwithcounter107[00.00%] Hot key'key:000000000000'found so farwithcounter254[45.45%] Hot key'counter:000000000001'found so farwithcounter87[45.45%] Hot key'key:000000000002'found so farwithcounter254[45.45%] Hot key'myset'found so farwithcounter64[45.45%] Hot key'counter:000000000000'found so farwithcounter93-------- summary ------- Sampled22keysinthe keyspace! hot key foundwithcounter:254keyname: key:000000000001hot key foundwithcounter:254keyname: key:000000000000hot key foundwithcounter:254keyname: key:000000000002hot key foundwithcounter:107keyname: mylist hot key foundwithcounter:93keyname: counter:000000000000hot key foundwithcounter:87keyname: counter:000000000002hot key foundwithcounter:87keyname: counter:000000000001hot key foundwithcounter:64keyname: myset

其他

redis-4.0还有一些其他新的特性对用户来说是透明的,未来阿里云也会在4.0的基础上为用户提供更加丰富的功能,敬请期待!

--------------------------------------------------------------------------------------------------------------------------------------------


Redis5.0新特性Stream


Redis5.0最近被作者突然放出来了,增加了很多新的特色功能。而Redis5.0最大的新特性就是多出了一个数据结构Stream,它是一个新的强大的支持多播的可持久化的消息队列,作者坦言Redis Stream狠狠地借鉴了Kafka的设计。

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


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

每个Stream都可以挂多个消费组,每个消费组会有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个Stream内唯一的名称,消费组不会自动创建,它需要单独的指令xgroup create进行创建,需要指定从Stream的某个消息ID开始消费,这个ID用来初始化last_delivered_id变量。

每个消费组(Consumer Group)的状态都是独立的,相互不受影响。也就是说同一份Stream内部的消息会被每个消费组都消费到。

同一个消费组(Consumer Group)可以挂接多个消费者(Consumer),这些消费者之间是竞争关系,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。每个消费者者有一个组内唯一名称。

消费者(Consumer)内部会有个状态变量pending_ids,它记录了当前已经被客户端读取的消息,但是还没有ack。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

消息ID
消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的ID要大于前面的消息ID。

消息内容
消息内容就是键值对,形如hash结构的键值对,这没什么特别之处。

增删改查
xadd 追加消息
xdel 删除消息,这里的删除仅仅是设置了标志位,不影响消息总长度
xrange 获取消息列表,会自动过滤已经删除的消息
xlen 消息长度
del 删除Stream
# *号表示服务器自动生成ID,后面顺序跟着一堆key/value
#  名字叫laoqian,年龄30岁
127.0.0.1:6379> xadd codehole * name laoqian age 30  
1527849609889-0  # 生成的消息ID
127.0.0.1:6379> xadd codehole * name xiaoyu age 29
1527849629172-0
127.0.0.1:6379> xadd codehole * name xiaoqian age 1
1527849637634-0
127.0.0.1:6379> xlen codehole
(integer) 3
# -表示最小值, +表示最大值
127.0.0.1:6379> xrange codehole - +
127.0.0.1:6379> xrange codehole - +
1) 1) 1527849609889-0
   2) 1) "name"
      2) "laoqian"
      3) "age"
      4) "30"
2) 1) 1527849629172-0
   2) 1) "name"
      2) "xiaoyu"
      3) "age"
      4) "29"
3) 1) 1527849637634-0
   2) 1) "name"
      2) "xiaoqian"
      3) "age"
      4) "1"
# 指定最小消息ID的列表
127.0.0.1:6379> xrange codehole 1527849629172-0 +  
1) 1) 1527849629172-0
   2) 1) "name"
      2) "xiaoyu"
      3) "age"
      4) "29"
2) 1) 1527849637634-0
   2) 1) "name"
      2) "xiaoqian"
      3) "age"
      4) "1"
# 指定最大消息ID的列表
127.0.0.1:6379> xrange codehole - 1527849629172-0
1) 1) 1527849609889-0
   2) 1) "name"
      2) "laoqian"
      3) "age"
      4) "30"
2) 1) 1527849629172-0
   2) 1) "name"
      2) "xiaoyu"
      3) "age"
      4) "29"
127.0.0.1:6379> xdel codehole 1527849609889-0
(integer) 1
# 长度不受影响
127.0.0.1:6379> xlen codehole
(integer) 3
# 被删除的消息没了
127.0.0.1:6379> xrange codehole - +
1) 1) 1527849629172-0
   2) 1) "name"
      2) "xiaoyu"
      3) "age"
      4) "29"
2) 1) 1527849637634-0
   2) 1) "name"
      2) "xiaoqian"
      3) "age"
      4) "1"
# 删除整个Stream
127.0.0.1:6379> del codehole
(integer) 1
独立消费
我们可以在不定义消费组的情况下进行Stream消息的独立消费,当Stream没有新消息时,甚至可以阻塞等待。Redis设计了一个单独的消费指令xread,可以将Stream当成普通的消息队列(list)来使用。使用xread时,我们可以完全忽略消费组(Consumer Group)的存在,就好比Stream就是一个普通的列表(list)。

# 从Stream头部读取两条消息
127.0.0.1:6379> xread count 2 streams codehole 0-0
1) 1) "codehole"
   2) 1) 1) 1527851486781-0
         2) 1) "name"
            2) "laoqian"
            3) "age"
            4) "30"
      2) 1) 1527851493405-0
         2) 1) "name"
            2) "yurui"
            3) "age"
            4) "29"
# 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息
127.0.0.1:6379> xread count 1 streams codehole $
(nil)
# 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来
127.0.0.1:6379> xread block 0 count 1 streams codehole $
# 我们从新打开一个窗口,在这个窗口往Stream里塞消息
127.0.0.1:6379> xadd codehole * name youming age 60
1527852774092-0
# 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容
# 而且还显示了一个等待时间,这里我们等待了93s
127.0.0.1:6379> xread block 0 count 1 streams codehole $
1) 1) "codehole"
   2) 1) 1) 1527852774092-0
         2) 1) "name"
            2) "youming"
            3) "age"
            4) "60"
(93.11s)
客户端如果想要使用xread进行顺序消费,一定要记住当前消费到哪里了,也就是返回的消息ID。下次继续调用xread时,将上次返回的最后一个消息ID作为参数传递进去,就可以继续消费后续的消息。

block 0表示永远阻塞,直到消息到来,block 1000表示阻塞1s,如果1s内没有任何消息到来,就返回nil

127.0.0.1:6379> xread block 1000 count 1 streams codehole $
(nil)
(1.07s)
创建消费组
Stream通过xgroup create指令创建消费组(Consumer Group),需要传递起始消息ID参数用来初始化last_delivered_id变量。
#  表示从头开始消费
127.0.0.1:6379> xgroup create codehole cg1 0-0
OK
# $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略
127.0.0.1:6379> xgroup create codehole cg2 $
OK
# 获取Stream信息
127.0.0.1:6379> xinfo codehole
 1) length
 2) (integer) 3  # 共3个消息
 3) radix-tree-keys
 4) (integer) 1
 5) radix-tree-nodes
 6) (integer) 2
 7) groups
 8) (integer) 2  # 两个消费组
 9) first-entry  # 第一个消息
10) 1) 1527851486781-0
    2) 1) "name"
       2) "laoqian"
       3) "age"
       4) "30"
11) last-entry  # 最后一个消息
12) 1) 1527851498956-0
    2) 1) "name"
       2) "xiaoqian"
       3) "age"
       4) "1"
# 获取Stream的消费组信息
127.0.0.1:6379> xinfo groups codehole
1) 1) name
   2) "cg1"
   3) consumers
   4) (integer) 0  # 该消费组还没有消费者
   5) pending
   6) (integer) 0  # 该消费组没有正在处理的消息
2) 1) name
   2) "cg2"
   3) consumers  # 该消费组还没有消费者
   4) (integer) 0
   5) pending
   6) (integer) 0  # 该消费组没有正在处理的消息
消费
Stream提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从PEL中移除。

# >号表示从当前消费组的last_delivered_id后面开始读
# 每当消费者读取一条消息,last_delivered_id变量就会前进
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"
   2) 1) 1) 1527851486781-0
         2) 1) "name"
            2) "laoqian"
            3) "age"
            4) "30"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
1) 1) "codehole"
   2) 1) 1) 1527851493405-0
         2) 1) "name"
            2) "yurui"
            3) "age"
            4) "29"
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole >
1) 1) "codehole"
   2) 1) 1) 1527851498956-0
         2) 1) "name"
            2) "xiaoqian"
            3) "age"
            4) "1"
      2) 1) 1527852774092-0
         2) 1) "name"
            2) "youming"
            3) "age"
            4) "60"
# 再继续读取,就没有新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole >
(nil)
# 那就阻塞等待吧
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
# 开启另一个窗口,往里塞消息
127.0.0.1:6379> xadd codehole * name lanying age 61
1527854062442-0
# 回到前一个窗口,发现阻塞解除,收到新消息了
127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole >
1) 1) "codehole"
   2) 1) 1) 1527854062442-0
         2) 1) "name"
            2) "lanying"
            3) "age"
            4) "61"
(36.54s)
# 观察消费组信息
127.0.0.1:6379> xinfo groups codehole
1) 1) name
   2) "cg1"
   3) consumers
   4) (integer) 1  # 一个消费者
   5) pending
   6) (integer) 5  # 共5条正在处理的信息还有没有ack
2) 1) name
   2) "cg2"
   3) consumers
   4) (integer) 0  # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1
   5) pending
   6) (integer) 0
# 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态
127.0.0.1:6379> xinfo consumers codehole cg1  # 目前还有1个消费者
1) 1) name
   2) "c1"
   3) pending
   4) (integer) 5  # 共5条待处理消息
   5) idle
   6) (integer) 418715  # 空闲了多长时间ms没有读取消息了
# 接下来我们ack一条消息
127.0.0.1:6379> xack codehole cg1 1527851486781-0
(integer) 1
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name
   2) "c1"
   3) pending
   4) (integer) 4  # 变成了5条
   5) idle
   6) (integer) 668504
# 下面ack所有消息
127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0
(integer) 4
127.0.0.1:6379> xinfo consumers codehole cg1
1) 1) name
   2) "c1"
   3) pending
   4) (integer) 0  # pel空了
   5) idle
   6) (integer) 745505
Stream消息太多怎么办
读者很容易想到,要是消息积累太多,Stream的链表岂不是很长,内容会不会爆掉就是个问题了。xdel指令又不会删除消息,它只是给消息做了个标志位。

Redis自然考虑到了这一点,所以它提供了一个定长Stream功能。在xadd的指令提供一个定长长度maxlen,就可以将老的消息干掉,确保最多不超过指定长度。

127.0.0.1:6379> xlen codehole
(integer) 5
127.0.0.1:6379> xadd codehole maxlen 3 * name xiaorui age 1
1527855160273-0
127.0.0.1:6379> xlen codehole
(integer) 3
我们看到Stream的长度被砍掉了。

消息如果忘记ACK会怎样
Stream在每个消费者结构中保存了正在处理中的消息ID列表PEL,如果消费者收到了消息处理完了但是没有回复ack,就会导致PEL列表不断增长,如果有很多消费组的话,那么这个PEL占用的内存就会放大。


PEL如何避免消息丢失
在客户端消费者读取Stream消息时,Redis服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是PEL里已经保存了发出去的消息ID。待客户端重新连上之后,可以再次收到PEL中的消息ID列表。不过此时xreadgroup的起始消息ID不能为参数>,而必须是任意有效的消息ID,一般将参数设为0-0,表示读取所有的PEL消息以及自last_delivered_id之后的新消息。

结论
Stream的消费模型借鉴了kafka的消费分组的概念,它弥补了Redis Pub/Sub不能持久化消息的缺陷。但是它又不同于kafka,kafka的消息可以分partition,而Stream不行。如果非要分parition的话,得在客户端做,提供不同的Stream名称,对消息进行hash取模来选择往哪个Stream里塞。如果读者稍微研究过Redis作者的另一个开源项目Disque的话,这极可能是作者意识到Disque项目的活跃程度不够,所以将Disque的内容移植到了Redis里面。这只是本人的猜测,未必是作者的初衷。如果读者有什么不同的想法,可以在评论区一起参与讨论。

说明:本文转自blog.csdn.net,用于学习交流分享,仅代表原文作者观点。如有疑问,请联系我们删除~