- A+
目录
Redis 基本数据类型
Redis 中,常用的数据类型有以下几种:
-
String:字符串类型,二进制安全字符串;
-
Hash:哈希表;
-
List 列表:链表结构,按照插入顺序排序的字符串元素的集合;
-
Set:集合,元素具有唯一性,未排序的字符串元素集合;
-
Sorted Set:有序集合;
-
Bit arrays:二进制数据;
Redis 的 Key
Redis 的键是二进制安全的,意味着无论 Key 中的内容是什么,在 Redis 都可以正常使用,例如 Key 中可以包含空格、\r\n
、¥
、$
等特殊字符,因为它们都会被转为二进制存储,它们不再是具有意义的字符串,而是一堆 01 组成的内容。你可以使用普通字符串做 Key ,也可以使用一张图片做 Key,只要 Key 小于 512MB 即可。
Redis Key 命名
Redis Key 的命名一般都是小写,命名主要以便于阅读为主,同时考虑缩短 Key,减少内存占用,例如 user:1000:followers
便于阅读,而 u1000flw
很短可以减少内存占用,但可读性不高。
Key 可以如果要表达有层次结构,则可以使用 :
组合 ,如要表达 租户(id1)=>技术部(id5)=>后端(id1)=>工号006
,每层对象在数据库中都有一个表存储,且每个对象都有一个 Id,则可以使用 tenant:01:department:05:group:01:user:006
做 Key,在某些工具下,Key 会被以树的形式显示,便于查找,其显示如下图所示:
如果 Key 某一个部分是多词字段,则可以使用 .
或 -
连接,如 comment:1234:reply.to
和comment:1234:reply-to
,表示 Id 为 1234 的评论被回复的信息列表。
由于 Redis 的 Key 是很宽松的,因此命名规则不必限定,可以根据团队内部讨论觉得使用何种分隔符分割层次、使用长命名还是缩短命名习惯等。
一般更加建议是使用 {对象名称}:{对象标识/id}:{对象属性}
表示,如 用户1
的消息列表表示为 user:1:messages
,如果是租户隔离,还可以表示用户相关信息为 tenant:1:user:1:messages
、tenant:1:user:1.messages
。
设置 Key 过期时间
Redis 的过期时间设置有四种形式:
-
EXPIRE 秒——设置指定的过期时间(秒),表示的是时间间隔。
-
PEXPIRE 毫秒——设置指定的过期时间,以毫秒为单位,表示的是时间间隔。
-
EXPIREAT 时间戳-秒——设置指定的 Key 过期的 Unix 时间,单位为秒,表示的是时间/时刻。
-
PEXPIREAT 时间戳-毫秒——设置指定的 Key 到期的 Unix 时间,以毫秒为单位,表示的是时间/时刻。
如设置一个 Key 在 5s 后过期:
127.0.0.1:6379> expire value 5 (integer) 1
设置 Key 在 2021年11月26日22时过期:
# 2021-11-26 22:00:00127.0.0.1:6379> expireat value 1637935200 (integer) 1
有些类型本身或类型的元素的命令参数可以设置过期时间,如 string 类型,可以不使用 expire 等相关命令。
使用 ttl 命令,可以查看一个 Key 的过期时间,返回剩余存活秒数。
# 2021-11-26 22:00:00127.0.0.1:6379> expireat value 1637935200 (integer) 1 127.0.0.1:6379> ttl value (integer) 4760
Redis 7.0 后 expire 命令有以下参数可用:
-
NX ——只有当密钥没有过期时才设置过期
-
XX -- 仅当键具有现有的过期时才设置过期
-
GT 仅当新的有效期大于当前有效期时才设置有效期
-
LT ——只有当新的有效期小于当前有效期时才设置有效期
笔者在编写这篇文章时,使用的 redis:latest
镜像,其版本是 6.2.6,因此暂未使用这些参数,你可以使用 info
命令查看 Redis 信息:
127.0.0.1:6379> info# Serverredis_version:6.2.6 ... ...
Redis 使用 expires 字典存储了所有 Key 的过期时间。
判断键是否存在
exist 命令可以判断一个 Key 是否存在,如果存在则返回 1,否则返回 0 。
redis> exist key1 (integer) 1 redis> exist nosuchkey (integer) 0
搜索 Key
keys 命令可以搜索符合条件的 Key,如 keys *
则返回全部 key。
# 搜索以 t 开头的所有 keykeys t*# 搜索包含 test 的 keykeys *test*
使用 dbsize 命令可以知道 Key 的数量:
127.0.0.1:6379> dbsize (integer) 5
scan 则可以指定搜索多少条符合条件的 key:
# 返回一条以 t 开头的 keyscan 0 match t* count 1
可参考: https://redis.io/commands/scan
can 命令格式入下
scan cursor [MATCH pattern] [COUNT count] [TYPE type]
cursor 是一个游标值,scan 每次结果的是在上一次迭代,表示开始位置,如果不注意,可能会导致查找结果与需要的不一样。如 Redis 中有三个 user:{id}
Key,我想搜索符合条件的这三个值:
127.0.0.1:6379> scan 0 match "user:*" count 51) "10"2) 1) "user:3" 2) "user:1"
搜索结果一直很奇怪。
这是因为是当前游标是 1) "10"
在 10;而且scan 只会返回部分数量的 Key,不会返回所有数量。所以如果要使用 scan 命令,我们要注意以下步骤。
笔者的完整 Key 如下:
1) "test" 2) "user:1" 3) "key" 4) "user:3" 5) "Sicily" 6) "test1" 7) "zset" 8) "deck" 9) "h"10) "year"11) "user:2"
注意 user:{id}
的位置。
重置游标:
127.0.0.1:6379> scan 01) "7"2) 1) "Sicily" 2) "user:3" 3) "deck" 4) "test" 5) "user:1" 6) "year" 7) "h" 8) "key" 9) "test1" 10) "zset"
注意,此时游标位置在 7,这个是 Redis 分配的,具有不确定性。
搜索:
127.0.0.1:6379> scan 0 match "user:*" count 1001) "0"2) 1) "user:3" 2) "user:1" 3) "user:2"
首先,当我们使用 scan 0
时,游标重新在 0 开始,因为没有设置值,因此 Redis 分配到了 7。另外游标的意思并不是下次从 7 开始搜索,而是指当前游标识别了 0-7 中的 Key,你下次搜索的结果将会在 0-7 中搜索!因此笔者给其设置了 count 100
,这样游标会一直往下走,直至找到符合数量的 Key 或 Key 已经检索完毕。
要注意,SCAN 命令并不保证每次执行都返回某个给定数量的元素。
如下所示,重置游标后,它自动检索到 7,默认最大 10,此时我们的关键字在 0-7中搜索,不加 count
默认只会找到两个 Key,那么我将 count 数量改成 3,那他不就可以找到三个元素了?这里我们直接设置为 5 试试:
127.0.0.1:6379> scan 0 1) "7"2) 1) "Sicily" 2) "user:3" 3) "deck" 4) "test" 5) "user:1" 6) "year" 7) "h" 8) "key" 9) "test1" 10) "zset"127.0.0.1:6379> scan 0 match "user:*" count 5 1) "10"2) 1) "user:3" 2) "user:1"
结果事与愿违,游标只走到 10 ,并且结果只有两个,而不是 3 个。
如果你把 count 设置大一点,可能便可以搜索到需要的 3 个 Key 了:
127.0.0.1:6379> scan 0 match "user:*" count 11 1) "0"2) 1) "user:3" 2) "user:1" 3) "user:2"
注意,scan cursor
跟 scan cursor match ...
命令不一样,前者是重置游标检索位置,将范围内的 Key 当搜索结果搜集起来;而 scan cursor match ...
指从哪个位置开始搜索。
在 Redis 的很多类型中,如列表、集合,都支持搜索,它们的命令格式中有个 pattern 字段,其支持 glob 风格的通配符区配格式,也使用这种风格区配 Key。其规则或说明如下:
符号 | 说明 |
---|---|
? | 表示一个任意字符,如 tes? ,test 符合结果; |
* | 区配任意数量的字符,如 * 表示所有;t* 表示以 t 开头; |
[] | 区配方括号间的任一个字符,可以使用 - 表示一个范围,与正则表达式类似;如 t[a-d]* ,以 t 开头,第二个字符是 a,b,cd 中的一个; |
\x | x 表示一个字符;用于将前面三个符号转义,使其失去特殊意义,如 \? 、\* 。 |
另外 Redis 的命令关键字不区分大小写。
判断键类型
type
命令可以获取一个 Key 的 Value 的类型:
127.0.0.1:6379> set value 123455 OK 127.0.0.1:6379> type value string 127.0.0.1:6379> rpush value2 abc (integer) 1 127.0.0.1:6379> type value2 list
删除键
del
命令可以删除一个 Key:
127.0.0.1:6379> del value2 (integer) 1 127.0.0.1:6379> del value2 value (integer) 1
返回删除的 Key 数量;如果 Key 不存在,del 命令不会报错,只会返回受影响的数量;
前面提到, Redis 支持模糊搜索 Key,可以很容易查找符合条件的 Key,但是 Redis 不支持模糊删除 Key。
RESP 协议
RESP 协议用于编写 Redis 客户端,它定义了 Redis 请求和响应的格式内容,当我们使用 redis-cli 工具连接 Redis 并执行命令时,返回的数据格式跟 RESP 有关系,这里简单说一下,Redis 响应的格式主要有:
-
对于简单字符串,回复的第一个字节是“+”
-
对于错误,回复的第一个字节是“-”
-
对于整数,回复的第一个字节是“:”
-
对于批量字符串,回复的第一个字节是“$”
-
对于数组,回复的第一个字节是“
*
”
在 redis-cli 或别的工具中,第一个符号可能不会显示,例如 "+ok"
,在工具中只给用户显示 "ok"
;响应包含 ok
则说明命令执行成功;nil 表示 Null Bulk String,也 nil 可以表达为 ok 的反义,即失败,但不代表发生错误,不同的编程语言客户端应将 nil 表示为其语言的相关空类型,例如 go 语言返回 nil,C# 返回 null,C 语言返回 NULL 等,在 redis-cli 中显示为 (nil)。
以上符号只是对响应内容进行初步解析,具体含义要根据发送的命令以及编程语言特点做二次处理。如 del bar
命令,删除一个 Key,响应内容:
127.0.0.1:6379> del bar (integer) 0
redis-cli 工具中看到的不是原始的消息内容,如果直接接收 TCP 消息,其内容应该是 :0
,客户端可以通过前面的 :
符号了解到后面的是数字,于是吧 0
截取处理,转为数字;但是这个数字结合 del 命令才有意义,这部分则要看编程语言的特点做处理。
这里不必深入了解 RESP 协议,只需要大概了解使用 redis-cli 等工具执行 Redis 命令时,响应结果代表什么意义即可。
字符串类型
Redis 的字符串类型也是二进制安全的,二进制安全并不是指线程安全,而是指无论你存储什么内容都可以,Redis string 最大可以存储 512 MB,可以往里面塞一些小姐姐视频、图片、网页、文件等都没问题。
面试题:Redis 相比 memcached 有哪些优势
memcached 只支持简单的字符串类型,而 Redis 支持多种类型;
Redis 速度更加快;
Redis 的数据可以持久化;
下面介绍一些 string 常用的指令。
使用 set
、 get
对单个 Key 进行写或读,使用 mset
、mget
对多个 Key 批量写或读。
> set mykey somevalue OK > get mykey"somevalue"
> mset a 10 b 20 c 30 OK > mget a b c 1) "10"2) "20"3) "30"
默认情况下,当 Key 已存在时,set
、mset
会替换其值;当 Key 不存在时,set
、mset
会创建新的 Key,而 Redis 提供了 NX、XX 两个参数,可以改变这种替换或新创建行为。如果一个 Key 存在并具有过期时间等属性时,如果使用 set 等命令替换 Key 时,过期时间等属性会自动消除。
NX:当 Key 不存在时才生效。
127.0.0.1:6379> set key1 666 nx"OK"127.0.0.1:6379> set key1 666 nx (nil)
Key 不存在时,set 命令正常;当 Key 不存在时,set 命令报 nil。
如果响应的信息以
-
开头,则表示一个错误。
XX:当 Key 存在时才生效。
127.0.0.1:6379> set key1 666 xx OK 127.0.0.1:6379> del key1 # 删除键(integer) 1 127.0.0.1:6379> set key1 666 xx (nil)
Key 存在时,set 命令正常;当 Key 不存在时,set 命令报 nil。
完整的 set 命令定义如下:
set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
下面介绍一下这些参数:
-
EX 秒——设置指定的过期时间(秒),表示的是时间间隔。
-
PX 毫秒——设置指定的过期时间,以毫秒为单位,表示的是时间间隔。
-
EXAT 时间戳-秒——设置指定的 Key 过期的 Unix 时间,单位为秒,表示的是时间/时刻。
-
PXAT 时间戳-毫秒——设置指定的 Key 到期的 Unix 时间,以毫秒为单位,表示的是时间/时刻。
-
NX ——当 Key 不存在时才设置值。
-
XX ——当 Key 存在时才设置值。
-
KEEPTTL ——保留设置前指定键的生存时间,即替换 Key 时,保留 Key 的过期时间设置。
-
GET ——如果 Key 已存在,使用 set 命令会替换 Key,加上 get 可以取得替换之前的值;如果 Key 不存在,则返回 nil。
EX、PX、EXAT、PXAT 都是设置时间的,其中 EX、PX 都是表示时间间隔,即自设置起还有多久此 Key 过期;而 EXAT 、PXAT 都是表示过期时刻,即什么时候过期,EXAT 是 10 位时间戳,如设置 2021-11-25 22:33:48
此 Key 过期,则时间戳为 1637850828;而 PXAT 是十三位的时间戳。
KEEPTTL 参数可以让 Key 继承旧 Key 的过期时间,如果一个 Key 设置了 100 秒后过期,那么当 set 命令替换 Key 前还有 90 秒过期,当替换后,新的 Key 会在 90 秒后过期。
示例:
127.0.0.1:6379> set key1 666 EX 10 get"666"127.0.0.1:6379> set key1 666 KEEPTTL get"666"
string 类型也可以使用原子操作,相当于 C# 的 Interlocked.Increment、Java 的 AtomicInteger、Go 的 atomic,在 Redis 中称为 atomic increment(原子增量)。
原子操作主要有 INCR、INCRBY、DECR、DECRBY 四种,前两种是增量,后两种是减量。
127.0.0.1:6379> set value 100 OK 127.0.0.1:6379> incr value # 自加 1(integer) 101 127.0.0.1:6379> incrby value 5 # 指定加量(integer) 106
INCR 可以用作统计访问量、注册账号递增的 ID 等。Redis 的原子操作对所有客户端生效,避免此客户端操作时,值被另一个客户端操作覆盖。
原子增量是双精确度类型,你可以使用 incrby value 5.0
甚至 incrby value 5E+4
加值。
string 类型具有以下列出的命令,有部分命令可能已经失效或在将来的版本中去除,本文只列举部分常用的命令,读者可参考官网文档说明。
Redis 命令有上百个,即使是常用的 Linux 命令也没有这么多,没必要强硬记住这些命令。
APPEND :追加字符串;
DECR:原子操作,减 1;
DECRBY:原子操作,减指定值;
GET:获取字符串值;
GETDEL:获取字符串值后删除 Key;
GETEX:获取字符串并设置过期时间,单位秒;
GETRANGE:获取字符串中的一部分字符;
GETSET:设置字符串值并返回旧字符串值;
INCR:原子操作,加 1;
INCRBY:原子操作,加指定值;
INCRBYFLOAT:原子操作,浮点数加指定值;
MGET:获取多个字符串 key;
MSET:同时设置多个字符串 ;
MSETNX:对多个字符串进行原子级别的设置值,这些 key 同时改变值;
PSETEX:获取字符串并设置过期时间,单位毫秒;
SET:设置字符串值;
SETEX:设置字符串并设置过期时间,单位秒;
SETNX:字符串不存在时才设置值;
SETRANGE:覆盖字符串的部分值,从偏移量 offset 设定的位置开始替换为新的字符串;
STRALGO:STRALGO LCS,不知道是什么东西;
STRLEN:获取字符串的字符数量;
位操作
位图不是实际的数据类型,而是在 String 类型上定义的一组面向位的操作,当然,从逻辑上也可以说 Bit 类型,前面提到过字符串是二进制安全的,它们的最大长度为 512 MB,使用二进制存储,因此有,因此它们适合设置为232个不同的位。
Redis 的字符串实现叫 简单动态字符串(Simple dynamic string),简称 SDS,按照存储空间的大小拆分成为了 sdshdr5
、sdshdr8
、sdshdr16
、sdshdr32
、sdshdr64
,其中 5、8、16、32、64 表示位数,例如 32 位,最大可以表示为 4GB。但是 Redis 中 Key 字符串值默认最大为 512MB,因此 sdshdr64 并没有实际使用到,sdshdr32 也是 ”残血“ 状态。
位操作主要有以下五个命令:
所述SETBIT命令采用作为第一个参数的比特数,和作为第二个参数的值以设置所述位,其为1或0的命令自动放大字符串,如果寻址位是当前字符串长度之外。
1,SETBIT:设置指定位的值;
2,GETBIT:仅返回指定索引处的位值,超出范围的位不会报错,会显示 0;
3,BITOP:在不同的字符串之间执行按位运算,提供的操作是 AND、OR、XOR 和 NOT。
4,BITCOUNT: 统计字符串的二进制位中 1 的个数;
5,BITPOS:返回字符串中设置为 1 或 0 的第一位的位置。
要注意,Redis 使用 C 语言编写,使用 char*
类型存储字符串,而在 C 语言中,char 是一个字节,而其他语言可能是两个字节;字符串存储的数字是字符串,以 ASCII 表示,因此,每位字符使用一个 char 表示,每个 char 8 位程度;但是中文等字符,不能按照此规则,例如 Unicode 使用4字节表示,UTF-8 使用3字节表示,那么中文的 帅
字,使用 UTF8 表示,其二进制为 11100101 10111000 10000101
。
Redis 的字符串是二进制安全的,当我们使用 C# 或 Go 语言编写时,需将字符串转为二进制数据,此时由编程语言编写的客户端决定了 Redis 中要存储的二进制数据,然后通过 TCP 发送二进制数据到 Redis 中,读者可参考 教你写个简单的 Redis Client 框架 。
首先,在字符串中,存储 1
这个字符串:
127.0.0.1:6379> set a1 1OK
在工具中查看 a1 的 16 进制表示,在 ASCII 中使用 31 表示 "1"
,二进制表示为 0011 0001
。
所以,使用 BITCOUNT 命令时,返回结果是 3:
# 127.0.0.1:6379> bitcount a1127.0.0.1:6379> bitcount a1 0 -1 (integer) 3
获取 1,3,7 位的值:
127.0.0.1:6379> getbit a1 0 (integer) 1 127.0.0.1:6379> getbit a1 1 (integer) 1 127.0.0.1:6379> getbit a1 2 (integer) 1 127.0.0.1:6379> getbit a1 7 (integer) 1
BITOP 可以让多个值之间进行位运算,即 与(&)
、或(|)
、异或(^)
、非(~)
四个基本操作,多个字符串值的二进制位数可以不相等。
BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN BITOP NOT destkey srckey
示例:
127.0.0.1:6379> set a1 1 OK 127.0.0.1:6379> set a2 2 OK 127.0.0.1:6379> set a2 帅 OK 127.0.0.1:6379> bitop and a1 a2 (integer) 3 127.0.0.1:6379> get a1"\xe5\xb8\x85"# 11100101 10111000 10000101 帅# AND# 00110001 1(ASCII 31)# 00100001 00000000 00000000 !# 11100101 10111000 10000101 帅# XOR# 00110001 1(ASCII 31)# 11010100 10111000 10000101 (无对于中文)
BITFIELD 也是一个很有用的命令,可以指定在某些位置填充字符。
BITFIELD 命令格式如下:
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
在 Redis 中,整型可以使用 i8、i16 等表示,其中 i8 表示 8 位二进制组成的数字,值在 0-127 之间;而无符号使用 u8、u16 等表示。使用 BITFIELD 命令时,会返回上一次的值。
127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 2001) (integer) 0 2) (integer) 0 127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 2001) (integer) 100 2) (integer) -56
#
后面的数字表示字节偏移量,SET i8 #0 100
表示将第一个字节设置为值为 i8 表示的 100。由于 i8 范围在 0-127,因此 200 使用 u8 表示,发生溢出,结果为 -56。但是不代表有问题,因为存储的时候 i8 和 u8 表示 200 都是 11001000(0xc8),存储二进制时不会区分正负,但是当你设置了i8
,则它在返回旧值的时候,按照给定的数据类型转换,因此 11001000 会显示 -56,但是正负不影响存入结果。
如果值过大,则会发生溢出。如:
... ... 127.0.0.1:6379> BITFIELD mystring SET i8 #0 100 SET i8 #1 2571) (integer) 100 2) (integer) 1
BITFIELD 还有个好玩的地方是可以在某一位上使用原子增量,格式示例 incrby i8 0 1
。
127.0.0.1:6379> BITFIELD mystring SET i8 #0 1 SET i8 #1 21) (integer) 1 2) (integer) 2 127.0.0.1:6379> BITFIELD mystring incrby i8 0 1 1) (integer) 2
incrby 参数后面可以带上溢出控制,避免自增后的数溢出,有 WRAP、SAT、FAIL,默认是 WRAP 模式,溢出了也没问题。
而 WRAP 会从负数到正数范围内取值,如 i8 则为 -127~+128;而 SAT 模式在递增时,如果即将发生溢出,那么他不会执行此操作,将值一直保持为 127。
# WRAP127.0.0.1:6379> BITFIELD mystring SET i8 #0 1271) (integer) 127 127.0.0.1:6379> BITFIELD mystring incrby i8 0 1 1) (integer) -128 127.0.0.1:6379> BITFIELD mystring incrby i8 0 257 1) (integer) -127# SAT127.0.0.1:6379> BITFIELD mystring SET i8 #0 1271) (integer) 127 127.0.0.1:6379> BITFIELD mystring overflow sat incrby i8 #0 11) (integer) 127 127.0.0.1:6379> BITFIELD mystring overflow sat incrby i8 #0 11) (integer) 127
如果溢出控制模式为 FAIL,会对检测到的上溢或下溢执行任何操作。相应的返回值设置为 NULL 以向调用者发出条件信号:
127.0.0.1:6379> BITFIELD mystring overflow fail incrby i8 #0 1281) (nil)
列表类型
Redis 的 list 类型是链表,区别与一些语言的 List 类型,例如 C# 的 List 、Go 的切片类型内部使用数组实现。因为 Redis list 是链表,所以 list 类型最平常的操作是头部或尾部添加/移除元素,头部或尾部的操作速度和时间跟元素数量不相关,1百万个元素和1千万个元素的操作速度是相同的。当然 list 的查找速度比数组慢。一个 list 最多可以包含 232- 1 个元素(4294967295)。
list 使用 lpush 和 rpush 命令在头部或尾部插入元素,使用 lpop 和 rpop 命令在头部或尾部移除元素:
127.0.0.1:6379> lpush list a b c d (integer) 4 127.0.0.1:6379> lrange list 0 -1 1) "d"2) "c"3) "b"4) "a"127.0.0.1:6379> rpush list 1 2 3 4 (integer) 8 127.0.0.1:6379> lrange list 0 -1 1) "d"2) "c"3) "b"4) "a"5) "1"6) "2"7) "3"8) "4"
lpop 和 rpop 移除元素时,可以指定弹出的元素数量,如果不指定,默认数量是 1:
127.0.0.1:6379> lpop list 21) "d"2) "c"
要注意,
lpush list 1 2 3
,结果是3 2 1
,而不是1 2 3
,因为每一个元素都会从左边插入,相当于跑过第一,就是你第一。插入过程:
1---21---321
除了 LPOP、RPOP,还有其它弹出头部和尾部的命令。
BLPOP、BRPOP:从多个键的头部或尾部弹出一个元素;
LMPOP(Redis 7.0 后可用):在多个键中弹出多个元素,示例:LMPOP 2 mylist mylist2 right count 3
;
BLMPOP:阻塞版本的 LMPOP;
lrange 表示从 list 的头部取一定范围的元素,其格式是 lrange {key} start stop
,start stop
表示元素下标范围,如取下标为 0-5 的六个元素:
127.0.0.1:6379> lrange list 0 5 1) "d"2) "c"3) "b"4) "a"5) "1"6) "2"
如果要获取全部元素,stop 取值为 -1:
127.0.0.1:6379> lrange list 0 -11) "d"2) "c"3) "b"4) "a"5) "1"6) "2"7) "3"8) "4"
lset 可以通过指定索引设置元素的值:
127.0.0.1:6379> lrange list 0 -1 1) "a" 2) "1"127.0.0.1:6379> lset list 0 b OK127.0.0.1:6379> lrange list 0 -1 1) "b" 2) "1"
可以使用 llen 获取 list 的元素数量:
127.0.0.1:6379> llen list (integer) 8
lindex 可以获取指定索引下标的元素的值:
1) "d"2) "c"3) "b"4) "a"5) "1"6) "2"7) "3"8) "4"127.0.0.1:6379> lindex list 0"d"
lrem 命令可以从 list 的左边或右边开始扫描,移除 N 个值为 value 的元素:
127.0.0.1:6379> lrem list 2 b (integer) 1
lrem 的命令格式为 lrem {key} [count] {value}
,如果 count 为 0 ,则表示移除全部值 {value}
的元素;如果 count > 0
,则从左边开始扫描,移除对应数量的元素;如果 count < 0
,则从右边开始扫描,移除 |count|
个对应的元素。
linsert
可以在指定元素值前或后插入一个新的值,其命令格式如下:
linsert key BEFORE|AFTER pivot element# privot 元素值
127.0.0.1:6379> lpush list 3 2 1 1 2 3 3 2 2 1 1(integer) 11127.0.0.1:6379> lrange list 0 -1 1) "1" 2) "1" 3) "2" 4) "2" 5) "3" 6) "3" 7) "2" 8) "1" 9) "1"10) "2"11) "3"127.0.0.1:6379> linsert list before 1 a (integer) 12127.0.0.1:6379> lrange list 0 -1 1) "a" 2) "1" 3) "1" 4) "2" 5) "2" 6) "3" 7) "3" 8) "2" 9) "1"10) "1"11) "2"12) "3"
lmove
命令可以从一个 list 中弹出头部或尾部,然后压入另一个 list 中,其格式:LMOVE source destination LEFT|RIGHT LEFT|RIGHT
。
127.0.0.1:6379> lrange list 0 -1 1) "a"2) "b"3) "c"4) "d"127.0.0.1:6379> lrange test 0 -1 1) "1"2) "2"3) "3"4) "4"127.0.0.1:6379> lmove list test right right"d"127.0.0.1:6379> lrange list 0 -1 1) "a"2) "b"3) "c"127.0.0.1:6379> lrange test 0 -1 1) "1"2) "2"3) "3"4) "4"5) "d"
示例中,由于两个位置参数都是 right,因此只处理 "list" 中的尾部元素,并压入到 "testt" 中。
list: a b c dtest: 1 2 3 4---list: a b ctest: 1 2 3 4 d
而 blmove 命令是 lmove 的阻塞版本,类似 Go 语言的 chan,如果 "list" 没有元素,那么会被阻塞;当 "list" 有元素后,马上移除并压入到 "test" 中。
其命令示例如下:
# list 中没有元素时;5 是阻塞超时时间127.0.0.1:6379> blmove list test right right 5(nil) (5.10s)
blmove 命令只会阻塞一次,不会一直阻塞,如果 "list" 压入了任一元素,则会马上处理。
如果只插入了一个元素,即使是
left right
,也会马上处理。
可以利用 Redis 制作消息队列,使用 rpop 命令从 未处理列表 的尾部插入元素,而使用 lmove 或 blmove 命令从 list 左侧移除消息并放到移除已处理列表中。
list 类型具有以下列出的命令,有部分命令可能已经失效或在将来的版本中去除,本文只列举部分常用的命令,读者可参考官网文档说明。
BLMOVE :阻塞式的 lmove;
BLMPOP:阻塞式的 lmpop;
BLPOP::从多个键的头部弹出一个元素,阻塞式;
BRPOP:从多个键的尾部弹出一个元素,阻塞式;
BRPOPLPUSH:阻塞式的 RPOPLPUSH;
LINDEX:获取指定索引下标的元素的值;
LINSERT:指定位置插入元素;
LLEN:获取 list 的元素数量;
LMOVE:原子地返回并移除 list 的第一个/最后一个元素,并压入到另一个list 中。
LMPOP:在多个 list 中弹出多个元素;
LPOP:在一个 list 中弹出多个元素;
LPOS:返回 list 中匹配元素的索引;
LPUSH:从头部压入多个元素;
LPUSHX:与 LPUSH 命令相似,如果 key 不存在,则 LPUSHX 不会创建新的 key;
LRANGE:获取一定范围的元素;
LREM:移除 N 个值为 value 的元素
LSET:通过指定索引设置元素的值;
LTRIM:list 上推送一个新元素,同时确保列表不会增长到超过从指定索引开始的元素数量;
RPOP:在一个 list 中弹出多个元素;
RPUSH:从尾部压入多个元素;
RPUSHX:与 RPUSH 命令相似,如果 key 不存在,则 LPUSHX 不会创建新的 key;
哈希类型
哈希类型可以存储多个键值对,并且元素数量是没有限制的,哈希中每一行存储一个键值对,每行的 key 称为一个字段。
HSET、HGET 用于设置或获取哈希的字段的值。
# hset 可以同时设置多个字段的值,但 hget 只能获取一个字段的值;hmget 可以同时获取多个字段的值;127.0.0.1:6379> hset h a 1 b 2 c 3(integer) 3127.0.0.1:6379> hget h a"1"127.0.0.1:6379> hmget h a b c1) "1"2) "2"3) "3"
另外,哈希中也有类似字符串的原子操作 HINCRBY、HINCRBYFLOAT 命令,这里就不在展开讲解。
HSCAN 命令可以从哈希中搜索符合条件的字段的值,其使用格式如下:
HSCAN key cursor [MATCH pattern] [COUNT count]
-
cursor :游标,开始位置,从上一次搜索结果的第几条结果开始再进行搜索。
-
pattern :匹配的模式。
-
count :指定从数据集里返回多少元素,实际返回个数会围绕该数波动,默认值为 10 。
示例:
hscan test 0 match "a*" count 2
hash 类型具有以下列出的命令,有部分命令可能已经失效或在将来的版本中去除,本文只列举部分常用的命令,读者可参考官网文档说明。
集合
Redis Set 是无序字符串集合,其内部使用哈希表实现,因此添加,删除,查找的复杂度都是 O(1),最多可以包含 232- 1 个元素(4294967295),其内部的元素不能重复。
集合的主要操作是执行多个集合的交集、并集、差集等。例如用户的消息列表中,可以使用一个集合存放用户的所有消息,另一集合存放用户已读消息,两者的差集即为未读消息。除了交集、并集、差集,还可以提取随机元素等。
其结构形式如图所示。
SADD 命令可以往集合中添加值,如果值已经存在,则会被忽略,即不会替换旧的值。
127.0.0.1:6379> sadd set 5 (integer) 1 127.0.0.1:6379> sadd set 5 (integer) 0
SADD 命令可以同时设置多个元素值:
127.0.0.1:6379> sadd set 5 1 2 4 5 6 (integer) 1
SDIFF 可以获取第一个集合与其他集合的差集并返回结果;而 SDIFFSTORE 获取第二个集合与后面集合的差集,并存储到第一个集合中。
SDIFF 命令:
key1 = {a,b,c,d} key2 = {c} key3 = {a,c,e} SDIFF key1 key2 key3 = {b,d}
SDIFFSTORE 命令:
key1 = {a,b,c,d}key2 = {c}key3 = {a,c,e}SDIFFSTORE key1 key3 key2 = {a,b,c,d}
使用 SDIFFSTORE 命令时,如果 key1 中有元素,则会被清空,然后存储差集的结果。
SINTER 可以让多个集合生成交集并返回交集;而 SINTERCARD 获取交集中的元素数量;
key1 = {a,b,c,d}key2 = {c}key3 = {a,c,e}SINTER key1 key2 key3 = {c}key1 = {a,b,c,d}key2 = {c}key3 = {a,c,e}SINTER key1 key2 key3 = {c}
打牌游戏
以打牌为例,一副牌中有 52 张牌,我们使用一些字符表示:
sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK
然后系统开始随机给每个玩家派牌,每个用户一张牌,那么需要每次从牌中删除一个元素,并返回到客户端中,SPOP 可以随机删除一个元素并返回到客户端,但是如果没有存储记录,那么后面无法记牌,因此需要使用将记录放到用户集合中。
# 随机取 5 个元素,但是不处理集合127.0.0.1:6379> SRANDMEMBER deck 51) "HQ"2) "S9"3) "H9"4) "SK"5) "H7"# 将这 5 个牌存储到 user:1 中# 这个步骤需要写代码,可以结合 SMOVE 等命令来做,这里手动操作;127.0.0.1:6379> sadd user:1 HQ S9 H9 SK H7 (integer) 5# 接着,生成差集,并存储回 deck 中127.0.0.1:6379> SDIFFSTORE deck deck user:1(integer) 47
如法炮制,将 15 个牌分配到三个用户中。
127.0.0.1:6379> SRANDMEMBER deck 51) "D6"2) "C4"3) "C1"4) "S6"5) "S4"127.0.0.1:6379> sadd user:2 D6 C4 C1 S6 S4 (integer) 5127.0.0.1:6379> SDIFFSTORE deck deck user:2(integer) 42127.0.0.1:6379> SRANDMEMBER deck 51) "CJ"2) "CK"3) "H8"4) "SJ"5) "H2"127.0.0.1:6379> sadd user:3 CJ CK H8 SJ H2 (integer) 5127.0.0.1:6379> 127.0.0.1:6379> SDIFFSTORE deck deck user:3(integer) 37
set 类型具有以下列出的命令,有部分命令可能已经失效或在将来的版本中去除,本文只列举部分常用的命令,读者可参考官网文档说明。
SADD:将一个或多个
member
元素加入到集合key
当中,已经存在于集合的member
元素将被忽略;SCARD:返回集合
key
的基数(集合中元素的数量);SDIFF:返回由第一个集合和所有后续集合之间的差异产生的集合成员;
SDIFFSTORE:此命令等于SDIFF,但不是返回结果集,而是存储在
destination
;SINTER:返回由所有给定集合的交集产生的集合成员。
SINTERCARD:类似 SINTER。返回给定集合的交集 的元素数量;
SINTERSTORE:类似 SINTER,但它不返回结果集,将结果存到第一个 Key 中;
SISMEMBER:判断是否为集合的成员;
SMEMBERS:返回存储在集合的所有成员的值;
SMISMEMBER:判断多个值是否在此集合中;
SMOVE:将一个值从集合中移动到另一个集合,操作是原子性的;
SPOP:从集合中删除并返回一个或多个随机成员
key
;如SPOP myset 3
随机删除三个值;SRANDMEMBER:如果命令执行时,只提供了
key
参数,那么返回集合中的一个随机元素;较为复杂,请查看文档;SREM:移除集合
key
中的一个或多个member
元素,不存在的member
元素会被忽略;SSCAN:搜索元素;
SUNION:并集;
SUNIONSTORE:生成并集存储到第一个集合中;
有序集合
有序集合(sorted set) 与集合类似,不允许元素重复,有序集合的每个元素可以设置一个 score 属性值,score 越小,元素的位置越靠前,有序集合通过 score 对元素进行排序。不同元素的 score 值可以相同,如果 score 相同,则接着比较元素的值大小。
127.0.0.1:6379> zadd test 1 B 1 A (integer) 2
如上命令所示,创建有序集合时,创建顺序是 B、A,且 score 值相同,但是 Redis 会接着比较元素的值,进行排序。
另外 score 是浮点类型,可以设置小数。
ZADD 命令可以创建有序集合,zrange 命令可以获取指定范围的元素。
127.0.0.1:6379> zrange test 0 -11) "A"2) "B"
zrange 可以加上 WITHSCORES 参数,获取元素的同时返回 score 值。
127.0.0.1:6379> zrange test 0 -1 WITHSCORES1) "A"2) "1"3) "B"4) "1"
score 也可以赋予一定的含义,如出生年份。
例如 A、B 两人在 1950 年出生,C、D 两人在 1951 年出生,其有序集合显示如下:
zrangebyscore 可以根据 score 值对元素进行筛选,其命令格式如下:
zrangebyscore year min max [WITHSCORES] [LIMIT offset count]
例如,获取 1950-1951 出生的 1 个人:
127.0.0.1:6379> zrangebyscore year 1950 1951 1) "A"2) "B"3) "C"4) "D"127.0.0.1:6379> zrangebyscore year 1950 1951 limit 0 11) "A"
limit 0 1
表示偏移量为0,数量为1。
如果要表达小于或大于,可以使用 (
符号,例如范围在 1950-1951之间,但是不包含 1950,则可以使用 (1950 1951
,示例:
127.0.0.1:6379> zrangebyscore year (1950 19511) "C"2) "D"127.0.0.1:6379> zrangebyscore year 1950 (19511) "A"2) "B"127.0.0.1:6379> zrangebyscore year (1950 (1951(empty array)
如果要获取有序集合中的所有元素,可以将 min max
的值设置为-inf +inf
,示例:
127.0.0.1:6379> zrangebyscore year -inf +inf1) "A"2) "B"3) "C"4) "D"
如果要表示小于 1951,示例如下:
127.0.0.1:6379> zrangebyscore year -inf 19511) "A"2) "B"3) "C"4) "D"
zrangebylex 命令可以根据元素的第一个字母进行范围筛选,如获取首字母在 A、C 范围内的元素:
127.0.0.1:6379> zrangebylex year [A [C1) "A"2) "B"3) "C"
另外,可以使用 -
、+
表示负无穷和正无穷,示例:
127.0.0.1:6379> zrangebylex year - [C1) "A"2) "B"3) "C"
有序集合的命令比较多,本文只列举部分常用的命令,读者可参考官网文档说明。
字符串、哈希、列表、集合、有序集合是 Redis 的基本数据类型,在此基础上,Redis 增加了地理位置、位图、日志等多种功能或命令,读者有兴趣请参考官方文档。
Redis 功能
事务
Redis 的事务是多个命令的集合,但是 Redis 的事务不具备失败回滚功能,甚至命令执行失败也不会主动取消事务的执行。客户端使用 MULTI 命令进入 Redis 事务,Redis 总是响应 "OK"
,此时,客户端可以发出多个命令,Redis 不执行这些命令,而是将它们排队,一旦调用 EXEC,将执行所有命令。
Redis 的事务主要命令有四个:MULTI, EXEC, DISCARD 和 WATCH,Redis 事务的设计理念倾向于快,因此缺少很多保障。
Redis 事务的设计目标主要有两个:
1,事务中的所有命令都按顺序序列化和执行。
2,要么处理所有命令,要么不处理命令,因此 Redis 事务也是原子的。
一个简单的事务操作如下,每个命令都将加入到事务的命令队列中:
> MULTI OK > INCR foo QUEUED > INCR bar QUEUED > EXEC
事务中执行的命令出现错误时,事务不会终止,而是一直执行下去,与此同时,Redis 的事务也不支持回滚,示例命令如下:
127.0.0.1:6379(TX)> BITFIELD mystring SET i8 #0 127QUEUED 127.0.0.1:6379(TX)> BITFIELD mystring overflow fail incrby i8 #0 128QUEUED 127.0.0.1:6379(TX)> get mystring QUEUED 127.0.0.1:6379(TX)> exec1) 1) (integer) 127 2) 1) (nil) 3) "\x7f"
127.0.0.1:6379(TX)> get 123 QUEUED 127.0.0.1:6379(TX)> get mystring QUEUED 127.0.0.1:6379(TX)> exec1) (nil) 2) "\x7f"
如果加入命令的时候命令格式不正确,则不会加入到命令队列中。
127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> incr mystring 1 1 1 (error) ERR wrong number of arguments for 'incr' command
在加入事务队列命令时,可以使用 DISCARD
取消事务队列,中止事务,但是如果事务已经执行,则不能停止此事务的执行。
Redis 的事务原子性的,事务中的命令要么执行,要么不执行;另外一个客户端的事务在加入命令队列的过程中,不会被其他客户端干扰,每个客户端创建的队事务对象都是其自身可见;但是每个命令的操作不是原子性的,例如 A 客户端的事务正在修改 mystring
的值,此时,B 客户端也可以直接修改值,或也通过事务修改值。
如果事务执行过程中,有部分 Key 修改会影响事务的执行,可以使用 watch 命令监听 Key,如果事务执行期间之前或执行期间, Key 被除自己外的客户端改动或删除,则事务会被终止。
A 客户端监控 Key 的值,但还没有执行事务:
127.0.0.1:6379> set tran 1 OK 127.0.0.1:6379> watch tran OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set tran 2 QUEUE
B 客户端修改 Key 的值:
127.0.0.1:6379> set tran 2 OK
此时,A 客户端执行事务:
127.0.0.1:6379(TX)> exec(nil)
如果是客户端自己对 Key 进行操作,则不会终止事务:
127.0.0.1:6379> watch tran OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set tran 3 QUEUED 127.0.0.1:6379(TX)> get tran QUEUED 127.0.0.1:6379(TX)> exec1) OK 2) "3"
发布订阅
Redis 的发布订阅也是很强大的,速度也快,在需求不是很严格的情况下,使用 Redis 做发布订阅更简单易用。
首先,A 客户端订阅一个信道,在 Redis 中称为信道(channel),在 MQTT 和一些消息队列中间件中一般称为 topic,Redis 订阅消息示例如下:
127.0.0.1:6379> subscribe chan1 Reading messages... (press Ctrl-C to quit) 1) "subscribe"2) "chan1"3) (integer) 1
此时 B 客户端可以向 通道发布消息,所有订阅者都可以收到相同的消息:
127.0.0.1:6379> publish chan1 测试 (integer) 1
因为笔者是在 redis-cli 操作,所以传输的消息内容都是些简单的,一般在程序中传递的消息都是具有一定格式的,如 json,订阅者收到消息后,使用工具进行反序列化为对象。
在 redis-cli 中,订阅消息后,则当前窗口会被阻塞,但是使用 TCP 直接连接 Redis ,订阅消息后,客户端不会被阻塞,可以继续发送命令到 Redis 中,如果收到消息推送,则 Redis 会发送消息到客户端。不同编程语言的处理细节不一样,具体细节可以参考编程语言的类库。当客户端想取消订阅时,可以使用 unsubscribe
命令。
通道的 Key 是独立存放的,不会跟基础类型的 Key 冲突,另外通道的 Key 也可以使用 {对象类型}:{对象标识/id}:{属性名称}
进行命名,以一类标识做通道名称方便订阅者订阅。
如网站中有多个栏目多篇文章,其中小明是负责《动物世界》专栏的主编,因此小明希望订阅部分重点文章的最新反馈情况,这里假设,小明要订阅所有文章的状态,那么我们可以把每篇文章使用一个通道标识:columns:1:article:1
、columns:1:article:2
... ...
那么小明可以批量订阅:
127.0.0.1:6379> subscribe columns:1:article:* Reading messages... (press Ctrl-C to quit) 1) "subscribe"2) "columns:1:article:*"3) (integer) 1
慢查询
一条命令的生命周期:
1,发送命令
2,命令排队
3,命令执行
4,返回结果
每条命令执行时,都会消耗一定的时间,如果我们能够获取每条命令的执行时间或筛选那些执行时间较大的命令执行记录,然后通过工具或其他方式找到这些命令,便可以进一步优化它,而 Redis 便提供了一个称为 慢查询日志的功能。
所谓慢查询日志就是记录每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息记录下来。慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。慢查询日志并不是指只记录查询相关的命令,而是包含所有命令。
你可以通过配置文件的形式配置慢查询设置,也可以通过以下命令快速设定:
# config set slowlog-log-slower-than 20000config set slowlog-log-slower-than 2 config set slowlog-max-len 1000 config rewrite
如果你使用配置文件启动 Redis,那么
config rewrite
会将配置刷新到配置文件中,如果是直接启动,则不需要执行此命令;
slowlog-log-slower
是慢查询阈值,执行时间超过此值,因为 Redis 命令的执行速度很快,我们这里数量并没有多少,不能默认生产环境大量数据的情况,因此这里直接设置为 2ns,可以很容易收集到数据。
然后随便执行一些命令,如 keys *
,然后查看慢查询日志:
# slowlog get [n]slowlog get 2
1) 1) (integer) 6 2) (integer) 1638100161 3) (integer) 12 4) 1) "keys" 2) "*" 5) "127.0.0.1:59586" 6) ""2) 1) (integer) 5 2) (integer) 1638100155 3) (integer) 6 4) 1) "config" 2) "set" 3) "slowlog-log-slower-than" 4) "2" 5) "127.0.0.1:59586" 6) ""
慢日志中会显示多个属性信息,这些属性信息的含义如下:
1) 1) (integer) 6 # Id 2) (integer) 1638100161 # 执行命令的时间戳 3) (integer) 12 # 命令耗时 4) 1) "keys" # 命令 2) "*" # 参数 5) "127.0.0.1:59586" # 客户端 6) ""
获取慢查询日志数量:
127.0.0.1:6379> slowlog len (integer) 8
重置慢查询日志记录:
slowlog reset
通过 slowlog 命令,可以帮助我们找到 Redis 可能存在的性能瓶颈。据一些文档的建议,slowlog-log-slower-than 可以设置认定 10ms 为慢查询。因为 Redis 是单线程执行命令,假设当慢命令执行时间是 10ms 时,那么这个 Redis 的 OPS 是 100,如果慢命令执行时间是 1ms 时,这个系统的 OPS 是 1000。
Redis 性能测试
redis-benchmark 是 Redis 的一个基准性能测试工具,在安装了 Redis 的系统中,一般会自带。它有以下几个命令可以帮助用户连接到处于其他位置的 Redis Server。
-h <hostname> Server hostname (default 127.0.0.1) -p <port> Server port (default 6379) -s <socket> Server socket (overrides host and port) -a <password> Password for Redis Auth --user <username> Used to send ACL style 'AUTH username pass'. Needs -a.
如连接到本地 Redis 服务:
redis-benchmark -h 127.0.0.1 -p 6379 -a 123456 -n 200000 -c 20
加上
--csv
参数,可以将执行结果放到 Excel 中。
它主要有两个测试参数:
-
-c
:选项代表客户端的并发数量,默认是50; -
-n
:选项代表客户端请求总量,默认是100000;
====== MSET (10 keys) ====== 200000 requests completed in 2.51 seconds 20 parallel clients 3 bytes payload keep alive: 1 host configuration "save": 3600 1 300 100 60 10000 host configuration "appendonly": no multi-thread: noSummary: throughput summary: 79744.82 requests per second latency summary (msec): avg min p50 p95 p99 max 0.216 0.056 0.199 0.311 0.631 5.919
可以看到,笔者的 Redis 实例 79744.82 requests per second
,即每秒处理了近8w个请求,平均每个命令耗时 0.216 ms,粗略计算,1s 可以处理 4,629
个命令。
redis-benchmark 会发送很多命令,这些命令都是具有一定功能的,能够很好模拟正常的操作,测试完成后,这些命令不会对你的 Redis 实例产生影响,但是会留下三个 Key,删除掉即可。
使用 slowlog get 100
查看都执行了什么命令:
因为笔者的 Redis 实例平均执行命令时间是 0.216,因此可以将 config set slowlog-log-slower-than
设置大一点,例如 1ms。
redis-benchmark 在测试的时候不会插入很多键,如果有需要,可以使用 -r
参数,生成更多键和模拟命令。
redis-benchmark -h 127.0.0.1 -p 6379 -a 123456 -n 200000 -c 20 -r 1000
此时,Key 达到了惊人的 2000 数量:
127.0.0.1:6379> dbsize (integer) 2009
上面的测试数据看起来不错,但并不是真实的网络请求,而在真实情况中,跨主机跨子网参数数据的时间消耗比较惊人的!网络 IO 会消耗主机较大的性能,也会占用较多的时间,如果客户端与 Redis 服务之间的 ping 是 20ms(时延),那么 200000/20 个客户端,需要 10000ms,如果每个客户端开启 10 个线程并发发送,可能也需要 1s。