文章目录
- 1. 简介
- 1.1 功能介绍
- 1.1.1 分布式缓存
- 1.1.2 内存存储和持久化(RDB+AOF)
- 1.1.3 高可用架构搭配
- 1.1.4 缓存穿透、击穿、雪崩
- 1.1.5 分布式锁
- 1.1.6 队列
- 1.2 数据类型
- String
- List
- Hash
- Set
- ZSet
- GEO
- HyperLogLog
- Bitmap
- Bitfield
- Stream
- 2. 命令
- 2.1 通用命令
- copy
- del
- dump
- exists
- expire
- 2.2 String
- append
- decr
- decrby
- get
- getdel
- getex
- getrange
- getset
- incr
- incrby
- incrybyfloat
- lcs
- mget
- mset
- msetnx
- psetex
- set
- setex
- setnx
- setrange
- strlen
- substr
- 2.3 List
- blmove
- blmpop
- blpop
- brpop
- brpoplpush
- lindex
- linsert
- llen
- lmove
- lmpop
- lpop
- lpos
- lpush
- lpushx
- lrange
- lrem
- lset
- ltrim
- rpop
- rpoplpush
- rpush
- rpushx
- 2.4 Hash
- hdel
- hexists
- hexpire
- hexpireat
- hexpiretime
- hget
- hgetall
- hincrby
- hincrbyfloat
- hkeys
- hlen
- hmget
- hmset
- hpersist
- hpexpire
- hpexpireat
- hpexpiretime
- hpttl
- hrandfield
- hscan
- hset
- hsetnx
- hstrlen
- httl
- hvals
- 2.5 set
- sadd
- scard
- sdiff
- sdiffstore
- sinter
- sintercard
- sinterstore
- sismember
- smembers
- smismember
- smove
- spop
- srandmember
- srem
- sscan
- sunion
- sunionstore
- 2.6 sorted set
- bzmpop
- zadd
- zcard
- zcount
- zdiff
- zdiffstore
- zincrby
- zinter
- zintercard
- zinterstore
- zmpop
- zrange
- zrank
- zrem
- zrevrank
- zscore
- 2.7 bitmap
- bitcount
- bitop
- getbit
- setbit
- strlen
- 2.8 hyperloglog
- pfadd
- pfcount
- pfmerge
- 2.9 geo
- geoadd
- geodist
- geohash
- geopos
- georadius
- georadiusbymember
- 2.10 stream
- xadd
- xdel
- xlen
- xrange
- xread
- xrevrange
- xtrim
- 2.10.1 消费组相关指令
- 3. 持久化
- 3.1 RDB
- 3.1.1 配置文件
- 3.1.2 触发
- 自动触发
- 手动触发
- 3.1.3 优缺点
- 3.1.4 检查修复rdb文件
- 3.1.5 禁用快照
- 3.1.6 rdb优化参数
- 3.2 AOF
- 3.2.1 AOF 持久化工作流程
- 3.2.2 写回策略
- 3.2.3 配置文件
- 3.2.4 恢复
- 3.2.5 优缺点
- 3.2.6 AOF 重写机制
- 3.3 AOF + RDB
- 3.4 纯缓存模式
- 4. 事务
- 4.1 命令
- 4.2 实例
- 4.2.1 正常执行
- 4.2.2 放弃执行
- 4.2.3 全体连坐
- 4.2.4 冤头债主
- 4.2.5 watch 监控
- 5. 管道
- 5.1 操作
- 5.2 注意
- 6. 发布订阅
- 7. 复制
- 7.1 介绍
- 7.2 操作
- 7.2.1 基本命令
- 7.2.2 架构
- 7.3 配置文件
- 7.4 常用
- 7.4.1 一主二仆
- 配置文件
- 手动配置
- 7.4.2 薪火相传
- 7.4.3 反客为主
- 7.5 复制流程
- 7.6 缺点
- 8. 哨兵
- 8.1 作用
- 8.2 实例
- 8.2.1 配置文件说明
- 8.2.2 开启哨兵
- 8.2.3 测试哨兵
- 8.3 运行流程
- 8.4 使用建议
- 9. 集群
- 9.1 定义
- 9.2 功能
- 9.3 算法
- 9.3.1 槽位
- 9.3.2 分片
- 9.3.3 优势
- 9.3.4 槽位映射
- 9.4 实例
- 9.4.1 环境搭建
- 9.4.2 测试
- 9.4.3 主从容错切换
- 9.4.4 集群扩容
- 9.4.5 集群缩容
- 9.4.6 其他说明
- 10. 集成
- 10.1 Jedis
- 10.1.1 集成
- 10.2 lettuce
- 10.2.1 集成
- 10.3 RedisTemplate
- 10.3.1 连接单机
- 10.3.2 连接集群
- 解决问题
- 1. 有线图标丢失
登录 redis
redis-cli -a redis # redis 为密码
1. 简介
- 基于内存的 k-v 键值对的数据库
- C语言写的
- redis 数据库操作主要是在内存,而 mysql 主要存储在硬盘上
- redis 是 k-v 数据库(nosql), mysql 是关系型数据库
- redis 在某些场景使用中明显优于 mysql, 比如计数器、排行榜等方面
1.1 功能介绍
1.1.1 分布式缓存
目前对于数据库的操作,百分之八十为查询,百分之二十为写入。如果每次都查数据库,速度较慢,且数据库压力很大,redis 可以用来减缓数据库压力。
在执行数据库查询操作时,先去 redis 数据库里看看有没有,没有再去查数据库并写入 redis 数据库。查询速度要快的多了。
🍪 之前项目中所做的一个操作(点赞、收藏),我们只是简单的更新一下数据库,但同时也要考虑数据一致性,考虑是否修改成功。这样是很慢的,而 redis 可以更好的操作。
1.1.2 内存存储和持久化(RDB+AOF)
redis 支持异步将内存中的数据写到硬盘上,同时不影响继续使用。
比如我们电脑断电了,那内存的数据是没有了,下次电脑开机,我们又需要将数据重新读入内存中,比较麻烦。而 redis 自身提供持久化操作,可以把内存的这部分数据存入硬盘,断电重启后,我们直接从硬盘将数据读入内存即可继续使用。
1.1.3 高可用架构搭配
- 单机
- 主从
- 哨兵
- 集群
如果只有一个 redis 数据库,那这个gg后,还是会所有请求冲向 mysql 数据库,所以就建立多个 redis 数据库来做缓冲,那就需要解决同步等一系列问题。
1.1.4 缓存穿透、击穿、雪崩
1.1.5 分布式锁
1.1.6 队列
redis 提供 list 和 set 操作,可以作为一个很好的消息队列平台来提供。
如常通过 redis 的队列功能来做购买限制。比如到节假日或者推广期间,进行一些活动,对用户购买行为进行限制,比如一天只能购买一次等。
优势
- 性能极高-Redis读的速度是110000次/秒,写的速度是81000次/秒
- Redis数据类型丰富,不仅仅支持简单的Key-Value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
- Redis支持数据的备份,即master-slave模式的数据备份
1.2 数据类型
String
- String 类型是二进制安全的,意思是 redis 的 string 可以包含任何数据,比如 jpg 图片或者序列化的对象
- 一个 redis 字符串的 value 最多可以是 512M。
List
- redis 列表是简单的字符串列表,按照插入顺序排序,可以添加一个元素到列表的头部和尾部
- 底层实际是个双端列表,最多可以包含 2 32 − 1 2^{32}-1 232−1 个元素。
Hash
- redis 的 hash 是一个 string 类型的 field 和 value 的映射表,特别适合用来存储对象
- 每个 hash 可以存储
2
32
−
1
2^{32}-1
232−1 个键值对,
k1 field v1
k1 作为键值,field v1
为 k-v 键值对作为值。
Set
- Redis 的 Set 是 String 类型的无序集合,集合成员是唯一的。
- 集合对象的编码可以是 intset 或者 hashtable
- Redis 的 Set 是通过哈希表实现的,所以添加、删除、查找的复杂的都是 O ( 1 ) O(1) O(1)
- 集合中最大的成员数为 2 32 − 1 2^{32}-1 232−1
ZSet
-
和 Set 一样,也是 String 类型元素的集合,且不允许重复的成员
-
和 Set 不同的是,每个元素都会关联一个 double 类型的分数,redis 通过这个分数来为集合中的元素从小到大排序
-
ZSet 的成员是唯一的,但是分数(score)可以重复
-
底层通过哈希表实现,所以添加、删除、查找的复杂度都是 O ( 1 ) O(1) O(1), 集合中最大的成员数为 2 32 − 1 2^{32}-1 232−1
-
k1 score v1 score v2
GEO
地理位置,主要用于存储地理位置信息,并对存储的信息进行操作,包括添加地理位置的坐标,获取地理位置的坐标,计算两个位置之间的距离,根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
HyperLogLog
它是用来做基数统计的算法,HyperLogLog 的优点是在输入元素的数量或者体积非常非常大时,计算基数所需的空间是很小的。(比如计算某个页面的访问量,IP可以作为基数,即唯一数)
在 redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2 64 2^{64} 264 个不同的元素的基数。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
Bitmap
位图,一个由 0 和 1 状态表现的二进制位的 bit 数值。可以用来实现签到、打卡等功能。
Bitfield
通过 bitfield 命令可以一次性操作多个比特位域(指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。
Stream
主要用于消息队列(MQ, Message Queue)。Redis 本身是有一个 Redis 发布订阅来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会丢失。而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失
2. 命令
https://redis.com.cn/commands.html
命令不区分大小写,key 区分大小写
2.1 通用命令
copy
COPY source destination [DB destination-db] [REPLACE]
时间复杂度: O ( N ) O(N) O(N),
将源键中的值拷贝到目标键中,默认的,目标键是在连接使用的逻辑数据库中创建的。DB选项允许为目标键指定替代逻辑数据库索引。当目标位置该键已经存在时会返回 0.使用 REPLACE
选项可以在进行复制前将该键进行移除。
SET dolly "sheep"
COPY dolly clone
GET clone
返回值
成功复制返回 1
,失败返回 0
del
DEL key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为键的数量。当移除的是单个值的时候,如 string,那复杂度就是 O ( 1 ) O(1) O(1),当删除的键是一个复杂的个体,比如 hash,那就是 O ( M ) O(M) O(M),看里面元素个数
如果键不存在,就忽略
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> DEL key1 key2 key3
(integer) 2
返回值
删除的键数量
dump
DUMP key
exists
EXISTS key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为键的数量
redis> SET key1 "Hello"
"OK"
redis> EXISTS key1
(integer) 1
redis> EXISTS nosuchkey
(integer) 0
redis> SET key2 "World"
"OK"
redis> EXISTS key1 key2 nosuchkey
(integer) 2
返回值
指定参数中存在的键数量
expire
2.2 String
append
APPEND key value
时间复杂度: O ( 1 ) O(1) O(1)
向字符串末尾追加, 如果 key 不存在则会创建
redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"
返回值
完成 append
操作后字符串的长度
decr
DECR key
时间复杂度: O ( 1 ) O(1) O(1)
字符串中的数字做减一操作,如果键不存在,则在操作前将键值设置为 0。如果键的类型不对,或者字符串类型无法转换为整数类型则会报错。这个操作仅限于 64 位有符号整数
# mykey 不存在
redis> decr mykey
(integer) "-1"
redis> get mykey
"-1"
redis> set mykey "2222222222222222222222222222222222222222222"
OK
redis> decr mykey
(error) ERR value is not an integer or out of range
返回值
自减后值的大小
decrby
DECRBY key decrement
时间复杂度: O ( 1 ) O(1) O(1)
同 decr
,只不过减小的大小可以自己规定,如果键不存在,则在操作前将键值设置为 0。如果键的类型不对,或者字符串类型无法转换为整数类型则会报错。这个操作仅限于 64 位有符号整数
redis> SET mykey "10"
"OK"
redis> DECRBY mykey 3
(integer) 7
返回值
减去指定大小后该值的大小
get
GET key
时间复杂度: O ( 1 ) O(1) O(1)
获取键的值,如果这个键不存在,返回一个特殊的值 nil
。如果存储的值不是 string
类型会报错,因为 GET
只能处理 string
类型。
redis> GET nonexisting
(nil)
redis> SET mykey "Hello"
"OK"
redis> GET mykey
"Hello"
返回值
如果键存在则返回值,否则返回 nil
getdel
GETDEL key
时间复杂度:
O
(
1
)
O(1)
O(1),支持版本从 6.2.0
开始
获取一个键的值然后删除该键。当且仅当取值成功后才会删除,也要求键的类型为 string
redis> SET mykey "Hello"
"OK"
redis> GETDEL mykey
"Hello"
redis> GET mykey
(nil)
返回值:如果该键存在,返回键值。如果不存在或者类型不为 string
,返回 nil
getex
GETEX key [EX seconds | PX milliseconds | EXAT unix-time-seconds |
PXAT unix-time-milliseconds | PERSIST]
时间复杂度: O ( 1 ) O(1) O(1)
获取键值的 value
,并可以选择性的设置过期时间。相当于 get
上多加一个额外的操作。(可以取代 get
了)
-
EX seconds
: 以秒为单位设置过期时间 -
PX milliseconds
: 以毫秒为单位设置过期时间 (1秒即1000毫秒) -
EXAT time-seconds
: 设置以秒为单位的UNIX时间戳所对应的时间为过期时间 -
PXAT time-milliseconds
: 设置以毫秒为单位的UNIX时间戳所对应的时间为过期时间 -
PERSIST
:去除该键的过期时间设置,设置为永不过时(-1
)
redis> SET mykey "Hello"
"OK"
redis> GETEX mykey
"Hello"
redis> TTL mykey
(integer) -1
redis> GETEX mykey EX 60
"Hello"
redis> TTL mykey
(integer) 60
返回值
如果键存在则返回值,否则返回 nil
getrange
GETRANGE key start end
时间复杂度: O ( N ) O(N) O(N),当字符串比较短时,可以认为是 O ( 1 ) O(1) O(1)
返回字符串子串,start
和 end
所处位置的字符都包含在内,可以为负数,-1
代表最后一个字符,-2
则是倒数第二个。如果 end
下标超过字符串,不会报错,而是取到字符串末尾。
redis> SET mykey "This is a string"
"OK"
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"
redis> GETRANGE mykey 10 100
"string"
返回值
返回子串,如果键不存在则返回空串,类型不对则报错
getset
从 6.2.0 之后 deprecated
,可以被 set
代替了
GETSET key value
时间复杂度
O
(
1
)
O(1)
O(1),可以使用 set
代替
SET key value GET
返回旧值并设置新值,如果值不为字符串会报错
redis> SET mykey "Hello"
"OK"
redis> GETSET mykey "World"
"Hello"
redis> GET mykey
"World"
❓ 它使用在什么场景呢?
比如取值一个数后,将值进行重置的时候。
redis> INCR mycounter
(integer) 1
redis> GETSET mycounter "0"
"1"
redis> GET mycounter
"0"
每次对 mycounter
进行加 1
操作,当满足某些条件后进行重置为 0
返回值
如果键存在则返回旧值,如果键不存在则返回 nil
并创建该键进行赋值。
incr
INCR key
同 decr
String incrResult1 = jedis.set("mykey", "10");
System.out.println(incrResult1); // >>> OK
long incrResult2 = jedis.incr("mykey");
System.out.println(incrResult2); // >>> 11
String incrResult3 = jedis.get("mykey");
System.out.println(incrResult3); // >>> 11
incrby
INCRBY key increment
同 decrby
incrybyfloat
INCRBYFLOAT 键增量
时间复杂度: O ( 1 ) O(1) O(1)
-
可以增加或减少,同前面的一样,如果键不存在,则创建并赋值为
0
,然后进行加减操作。如果该键的值不为字符串或者键值以及键增量无法转为float
会进行报错,操作成功后返回的为字符串类型 -
末尾的零总是会被移除,如
5.30
会被改为5.3
-
键增量可以使用指数形式
redis> SET mykey 10.50
"OK"
redis> INCRBYFLOAT mykey 0.1
"10.6"
redis> INCRBYFLOAT mykey -5
"5.6"
redis> SET mykey 5.0e3
"OK"
redis> INCRBYFLOAT mykey 2.0e2
"5200"
返回值
变更后的值的大小
lcs
LCS key1 key2 [LEN] [IDX] [MINMATCHLEN min-match-len] [WITHMATCHLEN]
时间复杂度:
O
(
N
∗
M
)
O(N*M)
O(N∗M),时间复杂度还是很高的,从 7.0.0
版本开始支持
lcs
是不是很熟悉,在算法中指代最长公共子序列,但是这里在字符串中匹配的字符不需要是连续的。例如,“foo” 和 “fao” 的 LCS 是 “fo”,因为从左到右扫描两个字符串,最长的公共字符集合是由第一个 “f” 和后面的 “o” 组成的。如果字符串代表的是某个用户编辑的文本,LCS 可以表示新文本与旧文本的不同程度。
> MSET key1 ohmytext key2 mynewtext
OK
> LCS key1 key2
"mytext"
🐯 如果我们只想知道长度
> LCS key1 key2 LEN
(integer) 6
🐯 如果我们想知道每个匹配序列的下标
> LCS key1 key2 IDX
1) "matches"
2) 1) 1) 1) (integer) 4
2) (integer) 7
2) 1) (integer) 5
2) (integer) 8
2) 1) 1) (integer) 2
2) (integer) 3
2) 1) (integer) 0
2) (integer) 1
3) "len"
4) (integer) 6
含义
2) 1) 1) 1) (integer) 4
2) (integer) 7
2) 1) (integer) 5
2) (integer) 8
2) 1) 1) (integer) 2
2) (integer) 3
2) 1) (integer) 0
2) (integer) 1
第一层的 2)
没啥用哈,我们称为第 0 层吧
- 第一层
1)
,2)
用来代表有几个匹配的连续序列,此处指代my
和text
两个 - 第二层
1)
,2)
代表字符串,用来指示第几个字符串,此处为1)
指ohmytext
,2)
指mynewtext
- 第三次的
1)(integer)4
,2)(integer)7
用来指代首尾下标
1) 1) 1) (integer) 4
2) (integer) 7
2) 1) (integer) 5
2) (integer) 8
它的含义就是第一个匹配序列为,第一个字符串的 4-7
与第二个字符串的 5-8
相同。
小细节:底层实现的遍历循环是从尾部到首进行遍历的。
🐯 如果我们想限制每个匹配序列的长度
> LCS key1 key2 IDX MINMATCHLEN 2
1) "matches"
2) 1) 1) 1) (integer) 4 # 子列长度大于2时才算匹配成功,如果设置为4,则只有 text, 无 my
2) (integer) 7
2) 1) (integer) 5
2) (integer) 8
2) 1) 1) (integer) 2
2) (integer) 3
2) 1) (integer) 0
2) (integer) 1
3) "len"
4) (integer) 6
也可以获取每个子列的长度
> lLCS key1 key2 IDX MINMATCHLEN 2 WITHMATCHLEN
1) "matches"
2) 1) 1) 1) (integer) 4
2) (integer) 7
2) 1) (integer) 5
2) (integer) 8
3) (integer) 4
2) 1) 1) (integer) 2
2) (integer) 3
2) 1) (integer) 0
2) (integer) 1
3) (integer) 2
3) "len"
4) (integer) 6
mget
MGET key [key ...]
时间复杂度: O ( N ) O(N) O(N),n 为键的个数
返回每个键所对应的值,如果键不存在或者不为 string
类型,返回 nil
redis> SET key1 "Hello"
"OK"
redis> SET key2 "World"
"OK"
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)
返回值
值列表
mset
MSET key value [key value ...]
时间复杂度: O ( N ) O(N) O(N),N 为键的数量
相当于多个 set
,对于未存在的键会创建,已存在的键会覆盖旧值。如果不想重写已经存在的键,可以看看 msetnx
mset
命令是原子的,所以该命令会将所有给定的键进行赋值,不存在某些更新了而另一些不变化
redis> MSET key1 "Hello" key2 "World"
"OK"
redis> GET key1
"Hello"
redis> GET key2
"World"
返回值
总是 ok
,因为不存在就新建,存在就覆盖
msetnx
MSETNX key value [key value ...]
时间复杂度: O ( N ) O(N) O(N),N 为键数量
对于所有键赋值给定的值。只要有一个键已经存在,msetnx
就不会执行任何操作。由于这种语义,可以使用 MSETNX 来设置代表唯一逻辑对象的不同字段的不同键,以确保设置所有字段或根本不设置任何字段。msetnx
也是一个原子命令。
redis> MSETNX key1 "Hello" key2 "there"
(integer) 1
redis> MSETNX key2 "new" key3 "world"
(integer) 0
redis> MGET key1 key2 key3
1) "Hello"
2) "there"
3) (nil)
返回值
0
代表命令失败,有键存在。如果为 1
代表所有值都 set
成功。
psetex
时间复杂度:
O
(
1
)
O(1)
O(1),从 2.6.12
就 deprecated 了
set
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds |
EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
时间复杂度: O ( 1 ) O(1) O(1)
将值设置为 string
类型。如果该键已经有值,那会直接覆盖,先前的过期时间也会被丢弃。
EX seconds
设置过期时间,单位为秒PX milliseconds
设置过期时间,单位为毫秒EXAT timestamp-seconds
: 设置以秒为单位的UNIX时间戳所对应的时间为过期时间PXAT timestamp-milliseconds
: 设置以毫秒为单位的UNIX时间戳所对应的时间为过期时间NX
仅当键不存在时才执行XX
仅当键存在时才执行KEEPTTL
保留键的过期时间(因为默认会去除过期时间,变为-1
)GET
返回键原来存放的值,如果键不存在则返回nil
。如果存储的值不是string
类型,则set
命令会终止
set
命令可以替代 setnx
、setex
、psetex
、getset
,所以在未来可能会过时或者被移除(官方说的)
redis> set mykey "hello" ex 60 get
(nil)
redis> ttl mykey
(integer) 51
redis> set mykey "world" get
"hello"
redis> ttl mykey
(integer) -1
解释
KEEPTTL
场景如下
> SET k1 v1 EX 30
OK
> TTL k1
(integer) 27
> set k1 v1kk
OK
> TTL k1
(integer) -1
由于如果不指定过期时间,则默认永不过期,如果存在场景,想要修改值但过期时间不变,则可以使用 KEEPTTL
> SET k1 v1kk KEEPTTL
History
- Starting with Redis version 2.6.12: Added the
EX
,PX
,NX
andXX
options. - Starting with Redis version 6.0.0: Added the
KEEPTTL
option. - Starting with Redis version 6.2.0: Added the
GET
,EXAT
andPXAT
option. - Starting with Redis version 7.0.0: Allowed the
NX
andGET
options to be used together.
❓ 如何获取 UNIX 时间戳
Long.toString(System.currentTimeMillis()/1000L);
setex
SETEX key seconds value
时间复杂度: O ( 1 ) O(1) O(1),从 2.6.12 版本就过期了,可以使用如下命令
SET key value EX seconds
setnx
SETNX key value
时间复杂度: O ( 1 ) O(1) O(1),从 2.6.12 版本就过期了,可以使用如下命令
SET key value NX
setrange
SETRANGE key offset value
时间复杂度: O ( 1 ) O(1) O(1),字符串较长时认为是 O ( N ) O(N) O(N),N 为字符串长度
从指定 offset
位置处开始重写,如果偏移量大于字符串长度,那么会用 0
进行填充(即看作空字符),如果键不存在,则会被看作空字符串。可以设置的最大偏移量为 2^29 - 1(536870911),因为 Redis 字符串的大小限制为 512 MB。如果需要超过这个大小,可以使用多个键。
redis> SET key1 "Hello World"
"OK"
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"
redis>
redis> SETRANGE key2 6 "Redis"
(integer) 11
redis> GET key2
"\x00\x00\x00\x00\x00\x00Redis"
返回值
返回修改后的字符串长度
strlen
STRLEN key
时间复杂度: O ( 1 ) O(1) O(1)
redis> SET mykey "Hello world"
"OK"
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0
返回值
返回字符串的长度,键不存在时返回 0
,不为字符串时会报错。
substr
SUBSTR key start end
时间复杂度:
O
(
1
)
O(1)
O(1),从 2.0.0
版本后过时,可以用 getrange
代替
2.3 List
blmove
可参看 lmove
BLMOVE source destination <LEFT | RIGHT> <LEFT | RIGHT> timeout
时间复杂度:
O
(
1
)
O(1)
O(1),从 6.2.0
后开始支持。该操作等于 lmove
+ 阻塞操作
timeout:超时时间(秒)。如果源列表为空,命令将会阻塞并等待列表中有新元素被推入,直到超时时间为止。如果在超时时间内没有任何元素被移动,命令返回 nil
。超时时间设置为 0,代表永久阻塞。
BLMOVE list1 list2 LEFT RIGHT 5
从 list1
的左端弹出元素,并将它推送到 list2
的右端。如果 list1
为空,它将会等待最多 5 秒,直到有新的元素进入 list1
。
blmpop
BLMPOP timeout numkeys key [key ...] <LEFT | RIGHT> [COUNT count]
时间复杂度:
O
(
N
+
M
)
O(N+M)
O(N+M),N 为提供的键数,M 为返回的元素个数。从 7.0.0
版本才开始支持
参看 lmpop
,只不过键列表中全部为空列表时会阻塞,而不是返回 nil
blpop
BLPOP key [key ...] timeout
时间复杂度: O ( N ) O(N) O(N),N 为提供的键数量
当传递多个列表键时,BLPOP
会按照提供的顺序依次检查这些列表。如果都为空,它会阻塞并等待,直到找到第一个非空的列表,然后从该列表的头部弹出一个元素。一旦找到并弹出一个元素,命令会立即返回,其他列表不会再被检查。超时时间可以设置为 0
,表示无限等待。
> rpush list1 1 2 3
(integer) 3
> rpush list2 4 5 6
(integer) 3
> blpop list3 list1 list2 0
1) "list1"
2) "1"
> flushdb
OK
> blpop list1 list2 list3 0 # 进程阻塞
返回值
- 没有元素可以弹出并且
time expired
:nil - 返回 key + popped element
brpop
BRPOP key [key ...] timeout
同 blpop
,只不过从尾部弹出
brpoplpush
BRPOPLPUSH source destination timeout
从 6.2.0
版本后过期,可以使用 blmove
代替
BLMOVE source destination RIGHT LEFT
lindex
LINDEX key index
时间复杂度: O ( N ) O(N) O(N)
返回列表中指定下标的元素,下标可以为负数。如果键类型不为 list
,会报错。如果下标超出列表长度,返回 nil
。
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LINDEX mylist 0
"Hello"
redis> LINDEX mylist -1
"World"
redis> LINDEX mylist 3
(nil)
linsert
LINSERT key <BEFORE | AFTER> pivot element
时间复杂度: O ( N ) O(N) O(N),插入列表最左侧,时间复杂度为 O ( 1 ) O(1) O(1),最右侧则为 O ( N ) O(N) O(N)
在指定列表中某元素位置前后插入一个元素
redis> RPUSH mylist "Hello"
(integer) 1
redis> RPUSH mylist "World"
(integer) 2
redis> LINSERT mylist BEFORE "World" "There" # World 前插入
(integer) 3
redis> LRANGE mylist 0 -1
1) "Hello"
2) "There"
3) "World"
返回值
- 成功插入:插入后的列表长度
- 键不存在:0
- 列表中的指定元素不存在:-1
- 键类型不为列表,
error
llen
LLEN key
时间复杂度: O ( 1 ) O(1) O(1)
返回列表的长度,如果键不存在,就返回 0
。如果键类型不为列表,则报错
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSH mylist "Hello"
(integer) 2
redis> LLEN mylist
(integer) 2
lmove
LMOVE source destination <LEFT | RIGHT> <LEFT | RIGHT>
时间复杂度:
O
(
1
)
O(1)
O(1),从 6.2.0
后开始支持
将源的首/尾元素移到目标的首/尾位置。
如果源不存在,那么返回值为 nil
,不进行任何操作。如果源和目标是一样的,那么就是值的 rotation
。该命令已经可以替代已过时的 RPOPLPUSH
,可以用如下命令代替
LMOVE ... ... RIGHT LEFT
redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LMOVE mylist myotherlist RIGHT LEFT
"three"
redis> LMOVE mylist myotherlist LEFT RIGHT
"one"
redis> LRANGE mylist 0 -1
1) "two"
redis> LRANGE myotherlist 0 -1
1) "three"
2) "one"
返回值:被操作的元素,如果源为空,返回 nil
lmpop
LMPOP numkeys key [key ...] <LEFT | RIGHT> [COUNT count]
时间复杂度:
O
(
N
+
M
)
O(N+M)
O(N+M),N 为提供的键数,M 为返回的元素个数。从 7.0.0
版本才开始支持
从第一个不为空的列表移除一个或多个元素
-
lpop/rpop
会从一个键中移除一个或多个元素 -
blpop/brpop
可以选择多个键,但只能从一个键中移除一个元素 -
lmpop
从第一个非空键中移除多个元素
COUNT
默认值为 1
redis> LMPOP 2 non1 non2 LEFT COUNT 10
(nil)
redis> LPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LMPOP 1 mylist LEFT
1) "mylist"
2) 1) "five"
redis> LRANGE mylist 0 -1
1) "four"
2) "three"
3) "two"
4) "one"
redis> LMPOP 1 mylist RIGHT COUNT 10
1) "mylist"
2) 1) "one"
2) "two"
3) "three"
4) "four"
redis> LPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LPUSH mylist2 "a" "b" "c" "d" "e"
(integer) 5
redis> LMPOP 2 mylist mylist2 right count 3
1) "mylist"
2) 1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 0 -1
1) "five"
2) "four"
redis> LMPOP 2 mylist mylist2 right count 5
1) "mylist"
2) 1) "four"
2) "five"
redis> LMPOP 2 mylist mylist2 right count 10
1) "mylist2"
2) 1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
redis> EXISTS mylist mylist2
(integer) 0
列表元素清理干净后,键会被删除
返回值
- 没有元素可以被移除:nil
- 展示键名及移除的元素列表
lpop
LPOP key [count]
时间复杂度: O ( N ) O(N) O(N),N 为返回的元素个数
移除并返回列表中第一个元素。还可以指定 count
,从列表中移除多个
redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> LPOP mylist
"one"
redis> LPOP mylist 2
1) "two"
2) "three"
redis> LRANGE mylist 0 -1
1) "four"
2) "five"
返回值
- 键不存在:
nil
- 不指定数量:返回第一个元素,若列表为空,返回
nil
- 指定数量:返回元素列表,若
count > 列表长度
,则会返回已有元素列表,如 count=5,列表长度为 3,则返回 3 个元素,不会跟着两个nil
。若列表为空,才返回nil
lpos
LPOS key element [RANK rank] [COUNT num-matches] [MAXLEN len]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为元素列表长度,当要查找的元素接近首元素或尾元素,或者 MAXLEN
参数提供了,那么该命令就是常数时间复杂度。
该命令返回匹配元素的下标。默认的,当没有其他选项给出时,它会从头到尾扫描列表查找第一个匹配的元素并返回下标,若没有匹配的,返回 nil
。
> RPUSH mylist a b c 1 2 3 c c
> LPOS mylist c
2
可以使用 rank
参数指定返回第几个匹配的元素下标
> LPOS mylist c RANK 2 # 返回第二个匹配的元素下标
6
rank
可以为负数,这样查找就会从尾到头进行扫描
> LPOS mylist c RANK -1
7
如果想返回多个匹配的元素下标,可以使用 COUNT
选项
> LPOS mylist c COUNT 2
[2,6]
RANK
和 COUNT
结合时,代表从第几个匹配的元素开始取 COUNT
个
> LPOS mylist c RANK -2 COUNT 3
[6,2]
RANK
为负数时,所返回元素下标是从大到小的- 当
COUNT
大于所能匹配的元素数量时,那就有多少返回多少个
当想获取列表中所有匹配的元素时,可以设置 COUNT
值为 0(理论上,如果设置 COUNT
值很大,也是可以取到所有,但是用 0 更好)
> LPOS mylist c COUNT 0
[2,6,7]
这里有个有趣的地方,就是使用 COUNT
和不使用时,若没有匹配的元素返回值是不同的
> lpos list d
(nil) # nil
> lpos list d count 1
(empty array) # 空列表
MAXLEN
用于限制最大比较次数,因为查找就是遍历循环,如果我们指定的 RANK
为正数,那就是从头找,如果为负数,就是从尾开始找。如果我们确定元素一定在开头 100 个,我们就可以设定 MAXLEN
为 100,它可以减少比较的时间。(比如我们想找前 100 个中的所有匹配项,但是列表很大,有100000多个元素)
当我们设置 MAXLEN 0
时,代表无限次比较
> LPOS list c MAXLEN 2
(nil)
> lpos list c count 0 maxlen 7
[2,6]
MAXLEN 限制了比较次数从而找不到 c 了
返回值
- 没有匹配项:
nil
- 单个匹配项:匹配下标或
nil
- 多个匹配项:指定了
COUNT
,返回匹配的下标列表,如果没有匹配项,返回一个空列表
lpush
LPUSH key element [element ...]
时间复杂度: O ( N ) O(N) O(N),N 为插入的元素个数
向列表头部插入新元素列表。如果键不存在,则会创建一个空列表然后再进行插入,如果键的值类型不为列表,则报错。
> LPSH mylist a b c
> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"
多个元素时,插入顺序是从第一个开始,第一个放入头结束后,然后把第二个放入头,结果为最后一个元素作为头。
返回值
操作结束后列表的长度
lpushx
LPUSHX key element [element ...]
时间复杂度: O ( N ) O(N) O(N),N 为插入的元素个数
向列表头部插入新元素列表。如果键不存在,则不会进行任何操作,如果键的值类型不为列表,则报错。与 lpush
相比就是键不存在时不会进行任何操作
redis> LPUSH mylist "World"
(integer) 1
redis> LPUSHX mylist "Hello"
(integer) 2
redis> LPUSHX myotherlist "Hello"
(integer) 0
redis> LRANGE mylist 0 -1
1) "Hello"
2) "World"
redis> LRANGE myotherlist 0 -1
(empty array)
返回值
键不存在时返回 0
,存在时且为列表返回操作完成后的列表长度,存在但不为列表时则报错
lrange
LRANGE key start stop
时间复杂度: O ( S + N ) O(S+N) O(S+N),S 为 start 到首或尾的最小距离,N 为要查找的列表范围元素的数量
返回该范围内的元素列表,注意返回的是 [start, stop]
,边界也包括。
如果起始位置大于列表长度,那么返回空列表,如果结束位置大于列表长度,则有多少返回多少,如果键不存在,也返回空列表。
redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LRANGE mylist 0 0
1) "one"
redis> LRANGE mylist -3 2
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist -100 100
1) "one"
2) "two"
3) "three"
redis> LRANGE mylist 5 10
(empty array)
lrem
LREM key count element
时间复杂度: O ( N + M ) O(N+M) O(N+M),N 为列表长度,M 为要移除的元素数量
- count > 0:从头到尾移除 count 个
- count < 0:从尾到头移除 -count 个
- count = 0:全部移除
如果键不存在,则看作空列表,返回值为 0
redis> RPUSH mylist "hello"
(integer) 1
redis> RPUSH mylist "hello"
(integer) 2
redis> RPUSH mylist "foo"
(integer) 3
redis> RPUSH mylist "hello"
(integer) 4
redis> LREM mylist -2 "hello"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello"
2) "foo"
返回值
移除的元素个数
lset
LSET key index element
时间复杂度: O ( N ) O(N) O(N),N 为列表长度,如果操作的是第一个或最后一个元素,时间复杂度为 O ( 1 ) O(1) O(1)
设置列表指定下标的值,如果下标范围不对,会报错
redis> RPUSH mylist "one"
(integer) 1
redis> RPUSH mylist "two"
(integer) 2
redis> RPUSH mylist "three"
(integer) 3
redis> LSET mylist 0 "four"
"OK"
redis> LSET mylist -2 "five"
"OK"
redis> LRANGE mylist 0 -1
1) "four"
2) "five"
3) "three"
返回值
下标正确就是 “ok”,否则报错
ltrim
LTRIM key start stop
时间复杂度: O ( N ) O(N) O(N),N 为该操作移除元素的数量
保留指定范围内的元素,将其余元素移除。可以为负数
当下标超出列表大小时不会报错
- 如果
start
大于列表长度或者start > end
,那么该键会被删除,结果会返回空列表 - 如果
end
大于列表长度,则取到最后一个元素
> RPUSH list a b c e f
(integer) 6
> LTRIM list 100 101
OK
> EXISTS list
(integer) 0 # 键不存在了
> RPUSH list a b c e f
(integer) 6
> LTRIM list 0 2
OK
> LRANGE list 0 -1
1) "a"
2) "b"
3) "c"
一个常见的用法就是结合 lpush/rpush
LPUSH mylist someelement
LTRIM mylist 0 99
它可以保证列表元素个数不会大于 100 个,当使用 Redis 存储日志时是非常有用的。并且以这种方式使用 ltrim
的时间复杂度为
O
(
1
)
O(1)
O(1),因为平均每次只会从尾部移除一个元素。
返回值
“OK”
rpop
RPOP key [count]
时间复杂度: O ( N ) O(N) O(N),N 为返回的元素个数
移除并返回列表中第一个元素。还可以指定 count
,从列表中移除多个
redis> RPUSH mylist "one" "two" "three" "four" "five"
(integer) 5
redis> RPOP mylist
"five"
redis> RPOP mylist 2
1) "four"
2) "three"
redis> LRANGE mylist 0 -1
1) "one"
2) "two"
返回值
- 键不存在:
nil
- 不指定数量:返回最后一个元素,若列表为空,返回
nil
- 指定数量:返回元素列表,若
count > 列表长度
,则会返回已有元素列表,如 count=5,列表长度为 3,则返回 3 个元素,不会跟着两个nil
。若列表为空,才返回nil
rpoplpush
RPOPLPUSH source destination
时间复杂度:
O
(
1
)
O(1)
O(1),从 6.2.0
后就过时了,参考 lmove
LMOVE source destination RIGHT LEFT
rpush
RPUSH key element [element ...]
时间复杂度: O ( N ) O(N) O(N),N 为插入的元素个数
向列表尾部插入新元素列表。如果键不存在,则会创建一个空列表然后再进行插入,如果键的值类型不为列表,则报错。
redis> RPUSH mylist "hello"
(integer) 1
redis> RPUSH mylist "world"
(integer) 2
redis> LRANGE mylist 0 -1
1) "hello"
2) "world"
返回值
操作结束后列表的长度
rpushx
RPUSHX key element [element ...]
时间复杂度: O ( N ) O(N) O(N),N 为插入的元素个数
向列表尾部插入新元素列表。如果键不存在,则不进行任何操作,如果键的值类型不为列表,则报错。
redis> RPUSH mylist "Hello"
(integer) 1
redis> RPUSHX mylist "World"
(integer) 2
redis> RPUSHX myotherlist "World"
(integer) 0
redis> LRANGE mylist 0 -1
1) "Hello"
2) "World"
redis> LRANGE myotherlist 0 -1
(empty array)
返回值
操作结束后列表的长度
2.4 Hash
hdel
HDEL key field [field ...]
时间复杂度: O ( N ) O(N) O(N), N 为删除的字段数量
删除指定键中的字段,该键中不存在的字段会忽略。如果键中没有任何字段了,该键也会被删除。如果键不存在,它会被视为空 hash,然后命令返回 0
redis> HSET myhash field1 "foo"
(integer) 1
redis> HDEL myhash field1
(integer) 1
redis> HDEL myhash field2
(integer) 0
redis> EXISTS myhash
(integer) 0 # 键中值清空后,键就不存在了
返回值
成功删除的字段数,不包括指定了字段但是键中没有的
hexists
HEXISTS key field
时间复杂度: O ( 1 ) O(1) O(1)
如果键中该值存在则进行返回
redis> HSET myhash field1 "foo"
(integer) 1
redis> HEXISTS myhash field1
(integer) 1
redis> HEXISTS myhash field2
(integer) 0
返回值
该键不存在或键中的字段不存在:0
键中该字段存在:1
hexpire
HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields
field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),从 7.4.0
后可用
为 hash 键中一个或多个字段设置过期时间。使用时至少指定一个字段。过期后该字段会自动从键中删除。
字段的过期时间只会在删除或覆盖哈希字段内容的命令执行时被清除,包括 HDEL
和 HSET
命令。这意味着,所有在概念上修改哈希键字段中存储的值而不使用新值替换的操作,都不会影响 TTL(即不会重置或清除过期时间)。
可以使用 HPERSIST
清除哈希字段的过期时间,让其变为永久有效。
注意:调用 HEXPIRE
/ HPEXPIRE
并设置 TTL 为 0,或者调用 HEXPIREAT
/ HPEXPIREAT
并设置一个过去的时间,将导致该哈希字段被删除。
参数选项
NX
:对于指定的字段,只有它们没有过期时间时设置过期时间XX
:对于指定的字段,只有它们有过期时间时设置过期时间GT
:对于指定的字段,只有新过期时间大于当前过期时间时进行设置LT
:对于指定的字段,只有新过期时间小于当前过期时间时进行设置
使用 GT、LT 时,没有设置过期时间的被视为具有无限的TTL。NX
、XX
、GT
、LT
之间是互斥的,不能同时使用
redis> HEXPIRE no-key 20 NX FIELDS 2 field1 field2
(nil)
redis> HSET mykey field1 "hello" field2 "world"
(integer 2)
redis> HEXPIRE mykey 10 FIELDS 3 field1 field2 field3
1) (integer) 1
2) (integer) 1
3) (integer) -2
redis> HGETALL mykey
(empty array)
返回值
- 返回值为一个数组类型,对于每个字段
- 键不存在或者键中给定的字段不存在:-2
- 指定的
NX|XX|GT|LT
条件不满足:0 - 过期时间被设置或更新:1
hexpire/hpexpire
命令用参数 0 或hexpireat/hpexpireat
用过去的 unix 时间进行设置:2
- 报错
- 键类型不为哈希
- 必需的参数缺失、指定了未知的参数、参数值类型错误或超出范围。
hexpireat
HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields
field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为参数个数,从 7.4.0
版本开始支持
与 hexpire
类似,不过使用 unix 时间戳,如果设置了一个过去的时间戳,那么该字段会被立即删除。
redis> HSET mykey field1 "hello" field2 "world"
(integer 2)
redis> HEXPIREAT mykey 1715704971 FIELDS 2 field1 field2
1) (integer) 1
2) (integer) 1
redis> HTTL mykey FIELDS 2 field1 field2
1) (integer) 567
2) (integer) 567
hexpiretime
HEXPIRETIME key FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为参数个数,从 7.4.0
版本开始支持
返回给定键的字段们的以秒为单位的 unix 过期时间
redis> HSET mykey field1 "hello" field2 "world"
(integer) 2
redis> HEXPIRE mykey 300 FIELDS 2 field1 field2
1) (integer) 1
2) (integer) 1
redis> HEXPIRETIME mykey FIELDS 2 field1 field2
1) (integer) 1715705914
2) (integer) 1715705914
返回值
- 数组,对于每个字段
- 键不存在或字段不存在:-2
- 字段存在但没有设置过期时间:-1
- 字段存在且有过期时间:以秒为单位的 unix 时间戳
hget
HGET key field
时间复杂度: O ( 1 ) O(1) O(1)
返回哈希键中对应字段的值
redis> HSET myhash field1 "foo"
(integer) 1
redis> HGET myhash field1
"foo"
redis> HGET myhash field2
(nil)
Map<String, String> hGetExampleParams = new HashMap<>();
hGetExampleParams.put("field1", "foo");
long hGetResult1 = jedis.hset("myhash", hGetExampleParams);
System.out.println(hGetResult1); // >>> 1
String hGetResult2 = jedis.hget("myhash", "field1");
System.out.println(hGetResult2); // >>> foo
String hGetResult3 = jedis.hget("myhash", "field2");
System.out.println(hGetResult3);
返回值
字段存在:字段值
字段不存在:nil
hgetall
HGETALL key
时间复杂度: O ( N ) O(N) O(N),N 为 hash 的大小
返回所有字段及对应值,先说字段名,再说值。
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "World"
键不存在时返回 empty array
hincrby
HINCRBY key field increment
时间复杂度: O ( 1 ) O(1) O(1)
执行加减操作,值只能为整数,如果键不存在,就新建该键,如果该字段不存在,也直接新建并设置初始值为0。
redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HINCRBY myhash field -10
(integer) -5
返回值
加减后值的大小
hincrbyfloat
HINCRBYFLOAT key field increment
时间复杂度: O ( 1 ) O(1) O(1)
若该字段不存在,则赋值为0然后再进行操作。
以下情况时会报错
- 键不为哈希类型
- 该字段值不能转换为浮点类型
可参考 incrbyfloat
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"
redis> HINCRBYFLOAT mykey field -5
"5.6"
redis> HSET mykey field 5.0e3
(integer) 0
redis> HINCRBYFLOAT mykey field 2.0e2
"5200"
hkeys
HKEYS key
时间复杂度: O ( N ) O(N) O(N),N 为 hash 的大小
返回哈希表中所有的字段名,键不存在时返回空列表
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HKEYS myhash
1) "field1"
2) "field2"
hlen
HLEN key
时间复杂度: O ( 1 ) O(1) O(1)
返回哈希中存储的字段个数,键不存在时返回 0
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HLEN myhash
(integer) 2
hmget
HMGET key field [field ...]
时间复杂度: O ( N ) O(N) O(N),N 为给定的字段数
返回命令中给定的字段列表所对应的值列表,对于每个字段,如果不存在则返回 nil
。当键不存在时,由于会将其看作空哈希,所以会返回一个 nil
列表
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HMGET myhash field1 field2 nofield
1) "Hello"
2) "World"
3) (nil)
> hmget non-key f1 f2 f3
1) (nil)
2) (nil)
3) (nil)
hmset
HMSET key field value [field value ...]
从 4.0.0
之后 deprecated
hpersist
HPERSIST key FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为给定的字段数,从 7.4.0
开始支持
移除给定字段的过期时间设置,全部变为永不过期
redis> HSET mykey field1 "hello" field2 "world"
(integer 2)
redis> HEXPIRE mykey 300 FIELDS 2 field1 field2
1) (integer) 1
2) (integer) 1
redis> HTTL mykey FIELDS 2 field1 field2
1) (integer) 283
2) (integer) 283
redis> HPERSIST mykey FIELDS 1 field2
1) (integer) 1
redis> HTTL mykey FIELDS 2 field1 field2
1) (integer) 268
2) (integer) -1
返回值
数组列表
- 字段不存在或键不存在:-2
- 字段存在但是原来就无过期时间:-1
- 过期时间被移除:1
hpexpire
HPEXPIRE key milliseconds [NX | XX | GT | LT] FIELDS numfields
field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为给定参数的个数,从 7.4.0
版本后开始支持
同 hexpire
类似,不过是以毫秒为单位的时间
redis> HSET mykey field1 "hello" field2 "world"
(integer 2)
redis> HPEXPIRE mykey 2000 FIELDS 2 field1 field2 # 2 秒的过期时间
1) (integer) 1
2) (integer) 1
redis> HGETALL mykey
(empty array)
hpexpireat
HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT]
FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为参数个数,从 7.4.0
版本开始支持
与 hexpireat
类似,只不过 unix 时间戳以毫秒为单位
redis> HSET mykey field1 "hello" field2 "world"
(integer 2)
redis> HPEXPIREAT mykey 1715704971000 FIELDS 2 field1 field2
1) (integer) 1
2) (integer) 1
redis> HPTTL mykey FIELDS 2 field1 field2
1) (integer) 303340
2) (integer) 303340
hpexpiretime
HPEXPIRETIME key FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为参数个数,从 7.4.0
版本开始支持
返回给定键的字段们的以毫秒为单位的 unix 过期时间
redis> HSET mykey field1 "hello" field2 "world"
(integer) 2
redis> HEXPIRE mykey 300 FIELDS 2 field1 field2
1) (integer) 1
2) (integer) 1
redis> HPEXPIRETIME mykey FIELDS 2 field1 field2
1) (integer) 1715705913659
2) (integer) 1715705913659
hpttl
HPTTL key FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为给定的字段数,从 7.4.0
版本开始支持
与 httl
类似,只不过返回的是以毫秒为单位
redis> HPTTL no-key FIELDS 3 field1 field2 field3
(nil)
redis> HSET mykey field1 "hello" field2 "world"
(integer) 2
redis> HEXPIRE mykey 300 FIELDS 2 field1 field3
1) (integer) 1
2) (integer) -2
redis> HPTTL mykey FIELDS 3 field1 field2 field3
1) (integer) 292202
2) (integer) -1
3) (integer) -2
hrandfield
HRANDFIELD key [count [WITHVALUES]]
时间复杂度:
O
(
N
)
O(N)
O(N),从 6.2.0
版本后开始支持
当只给定一个 key
参数时,返回一个随机的字段。如果提供一个正数 count,将返回一个字段数组,字段不可重复,数组大小为 min(count, hash大小)
。但是返回的顺序实际上并不是随机的,如果需要客户端 shuffle
它们–不理解
如果提供一个负数 count,则代表可以返回一个字段多次。真正的随机
WITHVALUES
可以设置携带值进行返回。
redis> HSET coin heads obverse tails reverse edge null
(integer) 3
redis> HRANDFIELD coin
"heads"
redis> HRANDFIELD coin
"edge"
redis> HRANDFIELD coin -5 WITHVALUES
1) "heads"
2) "obverse"
3) "heads"
4) "obverse"
5) "tails"
6) "reverse"
7) "heads"
8) "obverse"
9) "tails"
10) "reverse"
hscan
HSCAN key cursor [MATCH pattern] [COUNT count] [NOVALUES]
时间复杂度: O ( 1 ) O(1) O(1),完整迭代的时间复杂度为 O ( N ) O(N) O(N)
参看 SCAN
,NOVALUES
选项从 7.4.0
之后才开始支持
hset
HSET key field value [field value ...]
时间复杂度: O ( N ) O(N) O(N),N 为字段数
如果字段已存在,则会重写它的值
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HGET myhash field1
"Hello"
redis> HSET myhash field2 "Hi" field3 "World"
(integer) 2
redis> HGET myhash field2
"Hi"
redis> HGET myhash field3
"World"
redis> HGETALL myhash
1) "field1"
2) "Hello"
3) "field2"
4) "Hi"
5) "field3"
6) "World"
hsetnx
HSETNX key field value
时间复杂度: O ( 1 ) O(1) O(1)
当键不存在时会创建该键,如果字段存在,则命令失效,如果不存在,则新建字段并存储其值。
redis> HSETNX myhash field "Hello"
(integer) 1
redis> HSETNX myhash field "World"
(integer) 0
redis> HGET myhash field
"Hello"
返回值
成功:1
失败:0
hstrlen
HSTRLEN key field
时间复杂度: O ( 1 ) O(1) O(1)
返回字段对应的字符串长度,如果键或字段不存在返回0
redis> HSET myhash f1 HelloWorld f2 99 f3 -256
(integer) 3
redis> HSTRLEN myhash f1
(integer) 10
redis> HSTRLEN myhash f2
(integer) 2
redis> HSTRLEN myhash f3 # 数字也看作字符串
(integer) 4
httl
HTTL key FIELDS numfields field [field ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为给定的字段数,从 7.4.0
版本开始支持
返回键中某些字段的过期时间,以秒为单位
redis> HTTL no-key FIELDS 3 field1 field2 field3
(nil)
redis> HSET mykey field1 "hello" field2 "world"
(integer) 2
redis> HEXPIRE mykey 300 FIELDS 2 field1 field3
1) (integer) 1
2) (integer) -2
redis> HTTL mykey FIELDS 3 field1 field2 field3
1) (integer) 283
2) (integer) -1
3) (integer) -2
返回值
数组形式
- 字段不存在或键不存在:-2
- 字段存在但没有过期时间:-1
- 字段存在且有过期时间:以秒为单位的过期时间
hvals
HVALS key
时间复杂度: O ( N ) O(N) O(N)
返回所有字段对应的值(不含字段名),键不存在时就是一个空列表
redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"
2.5 set
sadd
SADD key member [member ...]
时间复杂度: O ( N ) O(N) O(N),N 为给定的元素数量
如果键不存在,会创建一个空集合然后将元素放入,如果集合中该元素已经存在,则忽略。
若该键已存在且不为集合类型,则报错
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SADD myset "World"
(integer) 0
redis> SMEMBERS myset
1) "Hello"
2) "World"
返回值
成功添加的元素个数,集合中已经存在的不算
scard
SCARD key
时间复杂度: O ( 1 ) O(1) O(1)
返回集合中元素的个数
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SCARD myset
(integer) 2
返回值
如果键不存在,返回 0,否则返回集合中元素的个数(也可能为 0)
sdiff
SDIFF key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为所有给定集合中所有元素的总和
假设第一个集合为 A,那返回值为 A - B - C - D...
,键不存在的视为空集合
key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SDIFF key1 key2 key3 = {b,d}
返回值
结果集合中的成员列表,该命令不会影响任何集合中的元素,而是返回一个新集合
sdiffstore
SDIFFSTORE destination key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为所给集合中所有元素的个数总和
这个命令和 sdiff
一样,只不过返回的集合会存入 destination
中,如果目标键已存在,则会被覆盖。
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SDIFFSTORE key key1 key2
(integer) 2
redis> SMEMBERS key
1) "a"
2) "b"
返回值
结果集合中的元素个数
sinter
SINTER key [key ...]
时间复杂度: O ( N ∗ M ) O(N*M) O(N∗M),最坏的情况下,N 是最小集合的元素个数,M 是集合个数
返回给定集合的交集,不存在的键被视为空集合,当一个键是空集合时,那结果集也总是空的。
key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SINTER key1 key2 key3 = {c}
返回值
结果集合的成员数组
sintercard
SINTERCARD numkeys key [key ...] [LIMIT limit]
时间复杂度:
O
(
N
∗
M
)
O(N*M)
O(N∗M),最坏情况下,N 为最小集合的元素个数,M 为集合个数,从 7.0.0
后开始支持
与 sinter
类似,不过返回的是结果集的元素个数
默认情况下,LIMIT
的值为 0,代表没有限制。如果指定了值,那么在计算交集时,当找到了这个数量的元素后就会停止计算,从而加快速度。
redis> SADD key1 "a" "b" "c" "d"
redis> SADD key2 "c" "d" "e"
redis> SINTER key1 key2
1) "c"
2) "d"
redis> SINTERCARD 2 key1 key2
(integer) 2
redis> SINTERCARD 2 key1 key2 LIMIT 1
(integer) 1
返回值
结果集中元素的个数
sinterstore
SINTERSTORE destination key [key ...]
时间复杂度: O ( N ∗ M ) O(N*M) O(N∗M),最坏情况下,N 为最小集合的元素个数,M 为集合个数
这个命令等价于 sinter
,但是不是返回结果集,而是将他存入 destination
,如果目标键已经存在,则会进行覆盖重写。
redis> SADD key1 "a" "b" "c"
redis> SADD key2 "c" "d" "e"
redis> SINTERSTORE key key1 key2
(integer) 1
redis> SMEMBERS key
1) "c"
返回值
结果集的元素个数
sismember
SISMEMBER key member
时间复杂度: O ( 1 ) O(1) O(1)
判断某个值是否是该集合的元素
redis> SADD myset "one"
(integer) 1
redis> SISMEMBER myset "one"
(integer) 1
redis> SISMEMBER myset "two"
(integer) 0
返回值
键不存在或者该元素不是集合的成员:0
元素是集合的成员:1
smembers
SMEMBERS key
时间复杂度: O ( 1 ) O(1) O(1),N 为集合中元素的个数
返回键中包含的所有的成员值。和给定一个键值的 SINTER
返回值一样
redis> SADD myset "Hello"
(integer) 1
redis> SADD myset "World"
(integer) 1
redis> SMEMBERS myset
1) "Hello"
2) "World"
smismember
SMISMEMBER key member [member ...]
时间复杂度:
O
(
N
)
O(N)
O(N),N 为给定的 member 参数个数。从 6.2.0
版本后开始支持
返回一个列表,1 代表是集合的元素,0 代表不是集合的元素
redis> SADD myset "one"
(integer) 1
redis> SADD myset "one"
(integer) 0
redis> SMISMEMBER myset "one" "notamember"
1) (integer) 1
2) (integer) 0
smove
SMOVE source destination member
时间复杂度: O ( 1 ) O(1) O(1)
将源 set
的元素移动到目标 set
中。这个操作是原子的。在任一时刻都只能是源或目标 set
的一个成员。
如果源的键不存在或者不包含指定元素 member
,那么就不做任何操作并且返回 0。否则,将元素从源 set
移除,加入到目标 set
,如果目标 set
已存在,则只进行移除。
如果源或目标键不为 set
,那就报错
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myotherset "three"
(integer) 1
redis> SMOVE myset myotherset "two"
(integer) 1
redis> SMEMBERS myset
1) "one"
redis> SMEMBERS myotherset
1) "three"
2) "two"
spop
SPOP key [count]
时间复杂度: O ( N ) O(N) O(N),N 即 count
从 set
中移除并返回一个或多个随机成员,和 srandmember
类似,只不过它只返回而不移除。
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myset "three"
(integer) 1
redis> SPOP myset
"two"
redis> SMEMBERS myset
1) "one"
2) "three"
redis> SADD myset "four"
(integer) 1
redis> SADD myset "five"
(integer) 1
redis> SPOP myset 3
1) "one"
2) "four"
3) "five"
redis> SMEMBERS myset
1) "three"
返回值
键不存在:nil
未指定 count:一个随机成员值
数组:移除的成员列表值
srandmember
SRANDMEMBER key [count]
时间复杂度: O ( N ) O(N) O(N),N 为 count
当指定 count 为一个正值时,返回一个数组,大小为 min(集合大小, count)
如果 count 为一个负数,允许返回同一个元素多次
redis> SADD myset one two three
(integer) 3
redis> SRANDMEMBER myset
"three"
redis> SRANDMEMBER myset 2
1) "two"
2) "three"
redis> SRANDMEMBER myset -5
1) "three"
2) "three"
3) "one"
4) "two"
5) "one"
srem
SREM key member [member ...]
时间复杂度: O ( N ) O(N) O(N)
移除 set 中指定的成员,如果指定的值不存在则忽略。如果键不存在被视为空集合,则该命令返回 0
当键的类型不为 set 时会报错
redis> SADD myset "one"
(integer) 1
redis> SADD myset "two"
(integer) 1
redis> SADD myset "three"
(integer) 1
redis> SREM myset "one"
(integer) 1
redis> SREM myset "four"
(integer) 0
redis> SMEMBERS myset
1) "two"
2) "three"
返回值
返回从集合中移除的成员数量,不包含不存在的成员。
sscan
SSCAN key cursor [MATCH pattern] [COUNT count]
参看 SCAN
sunion
SUNION key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为所有给定的键中所有元素的总个数
返回所有给定集合的并集
key1 = {a,b,c,d}
key2 = {c}
key3 = {a,c,e}
SUNION key1 key2 key3 = {a,b,c,d,e}
不存在的 key 被视为空集合
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SUNION key1 key2
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
返回值为一个数组
sunionstore
SUNIONSTORE destination key [key ...]
时间复杂度: O ( N ) O(N) O(N),N 为给定键所有元素的总数
这个命令和 sunion
类似,不过会将结果存储在 destination
如果目标键已经存在,那就会被重写。
redis> SADD key1 "a"
(integer) 1
redis> SADD key1 "b"
(integer) 1
redis> SADD key1 "c"
(integer) 1
redis> SADD key2 "c"
(integer) 1
redis> SADD key2 "d"
(integer) 1
redis> SADD key2 "e"
(integer) 1
redis> SUNIONSTORE key key1 key2
(integer) 5
redis> SMEMBERS key
1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
返回值
结果集的成员个数
2.6 sorted set
在 set 的基础上,每个 value 值前加入 score 分数值。可用来做排行榜(热销商品、热销主播、打赏榜)
bzmpop
BZMPOP timeout numkeys key [key ...] <MIN | MAX> [COUNT count]
时间复杂度: O ( k ) + O ( M ∗ l o g ( N ) ) O(k)+O(M*log(N)) O(k)+O(M∗log(N)) ,K 为提供的键的数量,N 为要排序集合的元素数量,M 是要弹出的元素个数
zadd
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member...]
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N)),对于每个要添加的元素时间复杂度均如此。N 是集合中元素的个数。
将具有指定分数的元素加入排序集合。如果指定的成员已经是排序集的成员,则更新分数并将元素重新插入到正确的位置以确保正确的排序。
如果 key
不存在,则创建一个空的新的排序集,并将指定的成员加入。如果键存在但不是有序集,则返回错误。
分数值应该是双精度浮点数的字符串表示形式。+inf
和 -inf
也是有效值。
一些参数选项如下:
xx
:只更新已存在的元素。不添加新元素nx
:只添加新元素,不更新已经存在的元素lt
:如果元素已存在且新的分数小于当前分数才更新。这个参数并不会阻止添加新的元素gt
:如果元素已存在且新的分数大于当前分数才更新。这个参数并不会阻止添加新的元素ch
:将返回值从添加的新元素数修改为更改的元素总数(CH 是changed 的缩写)。更改的元素是添加的新元素和更新分数的已存在元素。因此,在命令行中指定的元素若与过去的已存在的元素具有相同分数则不被计算在内。注意:通常ZADD的返回值只统计添加的新元素的数量。incr
:当这个选项与zadd
搭配时就像zincrby
。在这种情况下,只有一个分数-元素可以被指定。
GT、LT 和 NX 选项是互斥的。
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 1 "uno"
(integer) 1
redis> ZADD myzset 2 "two" 3 "three"
(integer) 2
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "uno"
4) "1"
5) "two"
6) "2"
7) "three"
8) "3"
从 6.2.0
开始加入了 GT
和 LT
选项。
返回值
nil:如果操作由于 xx/nx/lt/gt
选项之一冲突而终止。
新添加元素数:当没有使用 ch
选项
新添加或更新分数的元素数:使用 ch
选项
字符串:incr
选项使用时成员更新的分数
zcard
ZCARD key
时间复杂度: O ( 1 ) O(1) O(1)
返回排序集的大小
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZCARD myzset
(integer) 2
返回值
排序集的大小,如果键不存在,则返回 0
zcount
ZCOUNT key min max
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N)),N 是排序集中元素的个数
返回排序集中元素分数位于 min-max 的元素个数
min
和 max
参数和 zrangebyscore
描述的有相同的语义
zdiff
ZDIFF numkeys key [key ...] [WITHSCORES]
时间复杂度: O ( L + ( N − K ) l o g ( N ) ) O(L+(N-K)log(N)) O(L+(N−K)log(N)),最坏情况下,L 是所有集合中元素的总数,N 是第一个集合的大小,K 是结果集的大小
该命令与 zdiffstore
类似,只不过不是存储结果集,而是返回客户端。
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset1 3 "three"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZDIFF 2 zset1 zset2
1) "three"
redis> ZDIFF 2 zset1 zset2 WITHSCORES
1) "three"
2) "3"
返回值
二者的差集,可以带上 WITHSCORES
选项。
zdiffstore
ZDIFFSTORE destination numkeys key [key ...]
时间复杂度:
O
(
L
+
(
N
−
K
)
l
o
g
(
N
)
)
O(L+(N-K)log(N))
O(L+(N−K)log(N)),最坏情况下,L 是所有集合中元素的总数,N 是第一个集合的大小,K 是结果集的大小。从 6.2.0
版本后开始支持
计算第一个排序集和其余排序集的差集并将结果存入 destination
,numkeys
指定输入键的总数。即只在第一个排序集并且不在其余排序集中存在的元素。
当键不存在时被认为是空排序集。如果目标 destination
已经存在则会被重写。
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset1 3 "three"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZDIFFSTORE out 2 zset1 zset2
(integer) 1
redis> ZRANGE out 0 -1 WITHSCORES
1) "three"
2) "3"
返回值
结果集中成员的个数
zincrby
ZINCRBY key increment member
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N)),N 为排序集中元素的个数
对键中指定成员的分数进行增加。如果在排序集中该成员不存在,则将其原来分数看作 0.0
,然后添加增量。如果键不存在,则创建一个排序集并将该元素作为唯一成员。
当键存在但并不是排序集时会报错
分数值应该是数值的字符串表示形式,并接受双精度浮点数。可以提供负值来减少分数。
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZINCRBY myzset 2 "one"
"3"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "two"
2) "2"
3) "one"
4) "3"
返回值
双精度浮点数的新元素分数
zinter
ZINTER numkeys key [key ...] [WEIGHTS weight [weight ...]]
[AGGREGATE <SUM | MIN | MAX>] [WITHSCORES]
时间复杂度: O ( N ∗ K ) + O ( M ∗ l o g ( M ) ) O(N*K)+O(M*log(M)) O(N∗K)+O(M∗log(M)) 最坏程度,N 是最小的排序集大小,K 是排序集个数,M 为结果集大小
命令与 zinterstore
类似,只是不存储,而是返回结果集
WEIGHTS
和 AGGREGATE
选项可以参看 zunionstore
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZADD zset2 3 "three"
(integer) 1
redis> ZINTER 2 zset1 zset2
1) "one"
2) "two"
redis> ZINTER 2 zset1 zset2 WITHSCORES
1) "one"
2) "2"
3) "two"
4) "4"
返回值
数组,返回交集,可以包括分数。
zintercard
ZINTERCARD numkeys key [key ...] [LIMIT limit]
时间复杂度:
O
(
N
∗
K
)
O(N*K)
O(N∗K),最坏情况是 N 是最小的排序集,K 是给的排序集数量,从 7.0.0
版本开始支持
和 zinter
类似,只不过返回的是结果集的大小
键不存在时认为是空集。如果有一个排序集是空寂,那么结果就是空集。
当给定 limit
参数时(默认为 0,即没有限制),当交集数量达到 limit
限制时,该命令会终止并产生 limit
大小的结果集。该实现可以让在 limit
大小低于实际交集大小的查询更快。
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZADD zset2 3 "three"
(integer) 1
redis> ZINTER 2 zset1 zset2
1) "one"
2) "two"
redis> ZINTERCARD 2 zset1 zset2
(integer) 2
redis> ZINTERCARD 2 zset1 zset2 LIMIT 1
(integer) 1
当只关心交集有没有到达 limit
大小时,可以使用这个 limit
选项。
zinterstore
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]
时间复杂度: O ( N ∗ K ) + O ( M ∗ l o g ( M ) ) O(N*K)+O(M*log(M)) O(N∗K)+O(M∗log(M)),N 是最小排序集大小,K 是排序集个数,M 是结果集大小
计算所有给定排序集的交集,并将结果存入目标排序集。
默认情况下,元素的结果分数是其所在排序集中的分数之和。因为交集要求一个元素是每个给定排序集的成员,所以这会导致结果排序集中每个元素的分数等于输入排序集的数量。
redis> ZADD zset1 1 "one"
(integer) 1
redis> ZADD zset1 2 "two"
(integer) 1
redis> ZADD zset2 1 "one"
(integer) 1
redis> ZADD zset2 2 "two"
(integer) 1
redis> ZADD zset2 3 "three"
(integer) 1
redis> ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3
(integer) 2
redis> ZRANGE out 0 -1 WITHSCORES
1) "one"
2) "5"
3) "two"
4) "10"
如果结果集已存在,则重写
zmpop
ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]
时间复杂度:
O
(
K
)
+
O
(
M
∗
l
o
g
(
N
)
)
O(K)+O(M*log(N))
O(K)+O(M∗log(N)),K 为提供的键数量,N 是排序集中的元素个数,M 是弹出的元素个数,从 7.0.0
之后开始支持
从第一个不为空的排序集中弹出一个或多个元素
当 MIN
选项被使用时,抛出的元素是从第一个非空排序集中最小分数的元素。COUNT
指定有多少个元素抛出,默认为 1。弹出元素的数量是排序集元素个数与 COUNT
值中的最小值。删干净后该键也会被移除。
redis> ZMPOP 1 notsuchkey MIN
(nil)
redis> ZADD myzset 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZMPOP 1 myzset MIN
1) "myzset"
2) 1) 1) "one"
2) "1"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "two"
2) "2"
3) "three"
4) "3"
redis> ZMPOP 1 myzset MAX COUNT 10
1) "myzset"
2) 1) 1) "three"
2) "3"
2) 1) "two"
2) "2"
redis> ZADD myzset2 4 "four" 5 "five" 6 "six"
(integer) 3
redis> ZMPOP 2 myzset myzset2 MIN COUNT 10
1) "myzset2"
2) 1) 1) "four"
2) "4"
2) 1) "five"
2) "5"
3) 1) "six"
2) "6"
redis> ZRANGE myzset 0 -1 WITHSCORES
(empty array)
redis> ZMPOP 2 myzset myzset2 MAX COUNT 10
(nil)
redis> ZRANGE myzset2 0 -1 WITHSCORES
(empty array)
redis> EXISTS myzset myzset2
(integer) 0
zrange
ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]
时间复杂度: O ( l o g ( N ) + M ) O(log(N)+M) O(log(N)+M),N 为排序集元素的个数,M 为要返回的元素个数
可以通过索引(排名),分数或者字典序进行返回
从 6.2.0
开始,这个命令可以替代 zrevrange,zrangebyscore,zrevrangebyscore,zrangebylev和zrevrangebylex
元素排序是按照分数从小往大排序,如果元素有相同的分数则按照字典序排序。
REV
选项可以反转排序,让元素按分数从大到小排序,如果元素有相同的分数则按照字典序反向排序。
LIMIT
参数用于从匹配的元素中获得子范围(和SQL中的 SELECT LIMIT offset, count
)。负数返回中的所有元素。请记住,如果很大,则需要先遍历排序集以查找元素,然后才能返回要返回的元素,这可能会增加
O
(
N
)
O(N)
O(N) 时间复杂度。
WITHSCORES
可以返回元素的同时补充上分数,列表包含 value1, score1, ..., valueN, scoreN
redis> ZADD myzset 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZRANGE myzset 0 -1
1) "one"
2) "two"
3) "three"
redis> ZRANGE myzset 2 3
1) "three"
redis> ZRANGE myzset -2 -1
1) "two"
2) "three"
下标范围
start 和 stop 参数指定后,范围为 [start, stop]
,为闭区间。下标可以为负数
下标超出范围时不会报错。如果 start
大于 stop
或者大于集合的长度,会返回一个空集合。如果 stop
比集合的长度大,则返回的内容截止到集合最后一个元素。
分数范围
如果指定了 byscore
参数,那么命令就和 zrangebyscore
一样,返回分数位于 [start, stop]
的元素
start
和 stop
可以为 -inf
和 +inf
,用来代表最高分数和最低分数,比如我们可以获取 [2, +inf] 范围内的元素
默认的,start
和 stop
都是闭合的,会包含这个分数的元素。我们也可以在下标前加上 (
来表示为开区间
ZRANGE zset (1 5 BYSCORE # (1, 5]
范围翻转
当使用了 REV
选项后,0
下标所指代的元素就是最高分数的元素
默认的,start 必须小于等于 stop 才能返回一些东西。然而,如果 byscore
,bylev
被选择,那 start 就指代最高分数,stop 就认为是最低分数,因此,start 必须大于等于 stop 来返回一些元素。
ZRANGE zset 5 10 REV
返回逆转后,[5, 10] 下标范围内的元素
ZRANGE zset 10 5 REV BYSCORE
返回分数 [10, 5] 的元素
字典排序
bylex
选项使用后,这个命令就类似于 zrangebylex
并且返回和字典闭合范围间隔之间的排序集中的元素范围。
不过只是对相同分数的元素按照字典序排序,但是对于不同分数的,元素顺序不确定
有效的 start
和 stop
必须以 (
或 [
开头来表示指定范围的边界是包含的还是排除的
特殊的值,+
,-
代表无限字符串。如
ZRANGE myzset - + BYLEX
保证返回排序集中的所有元素,前提是所有元素具有相同的分数。
zrank
ZRANK key member [WITHSCORE]
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N))
返回在某个排序集中元素的排名,按照分数从小到大的排序。从 0 开始排
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZRANK myzset "three"
(integer) 2
redis> ZRANK myzset "four"
(nil)
redis> ZRANK myzset "three" WITHSCORE
1) (integer) 2
2) "3"
redis> ZRANK myzset "four" WITHSCORE
(nil)
从 7.2.0
开始可以使用 WITHSCORE
参数。
zrem
ZREM key member [member ...]
时间复杂度: O ( M ∗ l o g ( N ) ) O(M*log(N)) O(M∗log(N)),N 为排序集元素个数,M 是要移除的元素数量
移除指定排序集的某个成员,如果不存在则忽略,如果键不存在或不为排序集则报错。
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREM myzset "two"
(integer) 1
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "1"
3) "three"
4) "3"
zrevrank
ZREVRANK key member [WITHSCORE]
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N))
按照从高到底的分数进行排序,查看某个元素的下标
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZADD myzset 3 "three"
(integer) 1
redis> ZREVRANK myzset "one"
(integer) 2
redis> ZREVRANK myzset "four"
(nil)
redis> ZREVRANK myzset "three" WITHSCORE
1) (integer) 0
2) "3"
redis> ZREVRANK myzset "four" WITHSCORE
(nil)
从 7.2.0
开始可以使用 WITHSCORE
参数。
zscore
ZSCORE key member
时间复杂度: O ( 1 ) O(1) O(1)
获取排序集中元素的分数,如果成员不存在或者键不存在,返回 nil
redis> ZADD myzset 1 "one"
(integer) 1
redis> ZSCORE myzset "one"
"1"
2.7 bitmap
-
0,1 数组
-
使用场景:用户是否登录过,每日签到,打卡
-
bitmap 的底层数据结构就是 String,bitmap 可以看作是可以进行位操作的字符串
-
bitmap 支持的最大位数是 2 32 2^{32} 232 位,可以极大的节约存储空间,使用512M内存就可以存储多达42.9亿( 2 32 2^{32} 232)的字节信息
bitcount
BITCOUNT key [start end [BYTE | BIT]]
时间复杂度: O ( N ) O(N) O(N)
返回字符串中有多少个位为 1
不存在的键被认为是空串,结果返回 0。
默认情况下,附加参数 start 和 end 指定字节索引。我们可以使用附加参数 BIT 来指定位索引。所以 0 是第一位,1 是第二位,依此类推。对于负值,-1 是最后一位,-2 是倒数第二位,依此类推。
redis> SET mykey "foobar"
"OK"
redis> BITCOUNT mykey
(integer) 26
redis> BITCOUNT mykey 0 0 # 第一个字节
(integer) 4
redis> BITCOUNT mykey 1 1 # 第二个字节
(integer) 6
redis> BITCOUNT mykey 1 1 BYTE
(integer) 6
redis> BITCOUNT mykey 5 30 BIT
(integer) 17
bitop
BITOP <AND | OR | XOR | NOT> destkey key [key ...]
时间复杂度: O ( N ) O(N) O(N)
在多个键之间执行按位运算并将结果存储在目标键中。
NOT
操作时只能给一个 key。
redis> HSET uid:map 0 userId0 1 userId1 # 键为0,1 值为用户ID
(integer) 2
redis> SETBIT 20241001 0 1 # 指代某一天,下标为谁来了,下标对应用户的ID去 uid:map 查
(integer) 0
redis> SETBIT 20241001 1 1
(integer) 0
redis> SETBIT 20241001 2 1
(integer) 0
redis> SETBIT 20241001 3 1
(integer) 0
redis> SETBIT 20241002 0 1
(integer) 0
redis> SETBIT 20241002 2 1
(integer) 0
redis> BITOP AND des 20241001 20241002
(integer) 1 # 表示操作成功
redis> BITCOUNT des # 0 和 2 两个人连续两天都签到了
(integer) 2
同时,查看一个月签到登录几天
redis> SETBIT sign:usr1 0 1
(integer) 0
redis> SETBIT sign:usr1 30 1
(integer) 0
redis> BITCOUNT sign:usr1
(integer) 2 # 该月打卡两天
如果每天使用 1 个 1亿位的 bitmap 约占 12MB 的内存,内存压力不算太高。在实际使用时,最好对 Bitmap 设置过期时间,让 redis 自动删除不再需要的签到记录以节省内存开销。
getbit
GETBIT key offset
时间复杂度: O ( 1 ) O(1) O(1)
返回偏移量位置处的比特值。当偏移量超出字符串长度时,字符串被假定为具有 0 位的连续空间。当 key 不存在时,它被假定为空字符串,因此 offset 总是超出范围,并且 value 也被假定为 0 位的连续空间。
redis> SETBIT mykey 7 1
(integer) 0
redis> GETBIT mykey 0
(integer) 0
redis> GETBIT mykey 7
(integer) 1
redis> GETBIT mykey 100
(integer) 0
setbit
SETBIT key offset value
时间复杂度: O ( 1 ) O(1) O(1)
value 只能取 0 或 1。偏移量从 0 开始。
如果键不存在,一个新的字符串值会被创建,字符串会增长以确保它可以在偏移处容纳一位比特。偏移量需大于等于0且小于 2 32 2^{32} 232,当字符串长度增长时,将添加的位默认设置为 0。
redis> SETBIT mykey 7 1
(integer) 0
redis> SETBIT mykey 7 0
(integer) 1
redis> tpye mykey
string
redis> GET mykey
" "
返回值
原来该位置上的比特值
strlen
strlen 用于统计字符串一共占了几个字节
redis> SETBIT k1 0 1
(integer) 0
redis> STRLEN k1
(integer) 1
redis> SETBIT k1 8 1
(integer) 0
redis> STRLEN k1
(integer) 2 # 2 个字节了
2.8 hyperloglog
需求
-
统计某个网站的UV,某个文字的UV
UV(Unique Visitor) 独立访客,一般理解为客户端IP -
用户搜索网站关键词的数量
-
统计用户每天搜索不同词条个数
-
hyperloglog 也是字符串类型
在 redis 里面,每个 hyperloglog 键只需要花费 12KB 内存,就可以计算接近 2 64 2^{64} 264 个不同元素的基数,但是会存在 0.81% 左右的误差。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是 hyperloglog 只会根据输入元素计算基数(是去重后的),而不会存储输入元素本身。
pfadd
PFADD key [element [element ...]]
时间复杂度: O ( 1 ) O(1) O(1),添加每个元素都是如此
如果执行该命令后 HyperLogLog 估计的近似基数发生变化,则 PFADD 返回 1,否则返回 0。如果指定的键不存在,该命令会自动创建一个空的 HyperLogLog 结构。
调用不带元素但变量名有效的命令,如果变量已存在,则将不执行任何操作,如果键不存在,则仅创建该数据结构(在后一种情况下返回 1) 。
redis> PFADD hll a b c d e f g
(integer) 1
redis> PFCOUNT hll
(integer) 7
pfcount
PFCOUNT key [key ...]
时间复杂度: O ( 1 ) O(1) O(1),键多时是 O ( N ) O(N) O(N)
当只有一个键时,返回的是键中元素的个数,如果键不存在则会返回 0
当使用多个键调用时,通过在内部将存储在提供的键处的 HyperLogLog 合并到临时 HyperLogLog 中,返回传递的 HyperLogLog 并集的近似基数。
redis> PFADD hll foo bar zap
(integer) 1
redis> PFADD hll zap zap zap
(integer) 0
redis> PFADD hll foo bar
(integer) 0
redis> PFCOUNT hll
(integer) 3
redis> PFADD some-other-hll 1 2 3
(integer) 1
redis> PFCOUNT hll some-other-hll
(integer) 6
pfmerge
PFMERGE destkey [sourcekey [sourcekey ...]]
时间复杂度: O ( N ) O(N) O(N),N 为提供的键数,但是每一个都要耗费很长的常数时间
如果目标键存在,则将其视为源集之一,并且其基数将包含在计算的 hyperloglog 的基数中。
redis> PFADD hll1 foo bar zap a
(integer) 1
redis> PFADD hll2 a b c foo
(integer) 1
redis> PFMERGE hll3 hll1 hll2
"OK"
redis> PFCOUNT hll3
(integer) 6
返回值
OK
2.9 geo
-
通过经纬度记录地理位置
-
底层是 zset 类型
geoadd
GEOADD key [NX | XX] [CH] longitude latitude member [longitude
latitude member ...]
时间复杂度: O ( l o g ( N ) ) O(log(N)) O(log(N)) 对于每个要添加的元素, N 为排序集中元素的个数。
合法的经度是 [-180, 180]。合法的维度是 [-85.05112878, 85.05112878]
XX
:仅当存在时更新元素,一定不会新增元素NX
:不更新已存在的元素,仅进行添加CH
:将返回值从添加的新元素数修改为更改的元素总数(CH 是changed 的缩写)。更改的元素是添加的新元素和已更新坐标的已存在元素。因此,在命令行中指定的与过去具有相同分数的元素不被计算在内。通常情况下,GEOADD的返回值只统计新增元素的数量。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEODIST Sicily Palermo Catania
"166274.1516"
redis> GEORADIUS Sicily 15 37 100 km
1) "Catania"
redis> GEORADIUS Sicily 15 37 200 km
1) "Palermo"
2) "Catania"
可以使用 ZREM
来移除里面的元素,因为 Geo 索引结构就是排序集。
从 6.2.0
开始才支持 CH, NX 和 XX 选项。
geodist
GEODIST key member1 member2 [m | km | ft | mi]
时间复杂度: O ( 1 ) O(1) O(1)
返回两个地理坐标的距离,如果有一个成员不存在,则返回 NULL。
因为计算距离时假设地球是一个完美的球体,因此在边缘情况下可能存在高达 0.5% 的误差。
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEODIST Sicily Palermo Catania
"166274.1516"
redis> GEODIST Sicily Palermo Catania km
"166.2742"
redis> GEODIST Sicily Palermo Catania mi
"103.3182"
redis> GEODIST Sicily Foo Bar
(nil)
geohash
GEOHASH key [member [member ...]]
时间复杂度: O ( 1 ) O(1) O(1)
返回每个地理位置的 hash 表示(地球是三维的,经纬度是二维的,在此用hash表示地理位置,base32编码)。该命令会返回一个 11 位的字符串。可以用此来代表经纬度,这样读取存储都比较方便(经纬度那个是两个浮点数,不如一个字符串方便)
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOHASH Sicily Palermo Catania
1) "sqc8b49rny0"
2) "sqdtr74hyu0"
geopos
GEOPOS key [member [member ...]]
时间复杂度: O ( 1 ) O(1) O(1) 对于每个元素
返回排序集中所有指定成员的位置(经度、维度)
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEOPOS Sicily Palermo Catania NonExisting
1) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "15.08726745843887329"
2) "37.50266842333162032"
3) (nil)
地理位置通常不会非常精确,和存入时有一点小误差
georadius
GEORADIUS key longitude latitude radius <m | km | ft | mi>
[WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC]
[STORE key | STOREDIST key]
从 6.2.0
开始 deprecated, 可以被 geosearch 代替
时间复杂度: O ( N + l o g ( M ) ) O(N+log(M)) O(N+log(M)),其中 N 是由中心和半径界定的圆形区域的边界框内的元素数量,M 是索引内的项目数量。
WITHDIST
:将位置元素与中心之间的距离也一并返回WITHCOORD
:将位置元素的经纬度一并返回WITHHASH
:以 52 位有符号整数的 geohash 编码返回,实际中很少用,一般用于调试或底层应用COUNT
:限定返回的记录数
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUS Sicily 15 37 200 km WITHDIST
1) 1) "Palermo"
2) "190.4424"
2) 1) "Catania"
2) "56.4413"
redis> GEORADIUS Sicily 15 37 200 km WITHCOORD
1) 1) "Palermo"
2) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "Catania"
2) 1) "15.08726745843887329"
2) "37.50266842333162032"
redis> GEORADIUS Sicily 15 37 200 km WITHDIST WITHCOORD
1) 1) "Palermo"
2) "190.4424"
3) 1) "13.36138933897018433"
2) "38.11555639549629859"
2) 1) "Catania"
2) "56.4413"
3) 1) "15.08726745843887329"
2) "37.50266842333162032"
georadiusbymember
GEORADIUSBYMEMBER key member radius <m | km | ft | mi> [WITHCOORD]
[WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] [STORE key
| STOREDIST key]
也是从 6.2.0
后 deprecated, 可以使用 geosearch 代替。该命令与 georadius
类似,只不过可以使用该坐标集合中某个成员作为查找中心,而不需要指定经纬度
redis> GEOADD Sicily 13.583333 37.316667 "Agrigento"
(integer) 1
redis> GEOADD Sicily 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania"
(integer) 2
redis> GEORADIUSBYMEMBER Sicily Agrigento 100 km
1) "Agrigento"
2) "Palermo"
2.10 stream
- Java 中的流和这个没有任何关系,这个 stream 就是 redis 版本的MQ,消息队列+阻塞队列。
- 实现消息队列,支持消息的持久化、支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠
xadd
XADD key [NOMKSTREAM] [<MAXLEN | MINID> [= | ~] threshold
[LIMIT count]] <* | id> field value [field value ...]
时间复杂度: O ( 1 ) O(1) O(1) 对于每个要添加的键值对
将给定的 entry 加入流,如果键不存在,则新建一个空流,可以使用 NOMKSTREAM
指定若键不存在则禁用该命令
里面存储的是一个 field-value 列表,使用 XRANGE
或 XREAD
读取时与 XADD
加入时的顺序一样
XADD 是唯一可以向流添加数据的 Redis 命令,但还有其他命令(例如 XDEL 和 XTRIM)可以从流中删除数据。
-
*
指代生成ID时为自动生成,否则就需要自己指定1526919030474-55 # 毫秒级的时间戳-该毫秒内产生的第几个消息
redis> XADD mystream * name Sara surname OConnor
"1727834940186-0"
redis> XADD mystream * field1 value1 field2 value2 field3 value3
"1727834940187-0"
redis> XLEN mystream
(integer) 2
redis> XRANGE mystream - +
1) 1) "1727834940186-0"
2) 1) "name"
2) "Sara"
3) "surname"
4) "OConnor"
2) 1) "1727834940187-0"
2) 1) "field1"
2) "value1"
3) "field2"
4) "value2"
5) "field3"
6) "value3"
xdel
XDEL key id [id ...]
时间复杂度: O ( 1 ) O(1) O(1) 对于流中每一个要删除的元素
根据ID进行删除
> XADD mystream * a 1
1538561698944-0
> XADD mystream * b 2
1538561700640-0
> XADD mystream * c 3
1538561701744-0
> XDEL mystream 1538561700640-0
(integer) 1
127.0.0.1:6379> XRANGE mystream - +
1) 1) 1538561698944-0
2) 1) "a"
2) "1"
2) 1) 1538561701744-0
2) 1) "c"
2) "3"
返回值为成功删除的数量
xlen
XLEN key
时间复杂度: O ( 1 ) O(1) O(1)
redis> XADD mystream * item 1
"1727836635459-0"
redis> XADD mystream * item 2
"1727836635459-1"
redis> XADD mystream * item 3
"1727836635460-0"
redis> XLEN mystream
(integer) 3
键不存在或流为空时返回 0。可以使用 TYPE
或 EXISTS
命令检测该键是否存在
当流中没有 entry 后该键不会自动删除,因为可能具有与其关联的消费者组
xrange
XRANGE key start end [COUNT count]
时间复杂度: O ( N ) O(N) O(N),N 为要返回的元素个数
返回的是ID大小处于 [start, end]
的 entries。
-
代表最小ID,+
代表最大ID,当使用如下命令时,代表返回流中所有的 entry
XRANGE key - +
redis> XADD writers * name Virginia surname Woolf
"1727835876126-0"
redis> XADD writers * name Jane surname Austen
"1727835876126-1"
redis> XADD writers * name Toni surname Morrison
"1727835876126-2"
redis> XADD writers * name Agatha surname Christie
"1727835876127-0"
redis> XADD writers * name Ngozi surname Adichie
"1727835876127-1"
redis> XLEN writers
(integer) 5
redis> XRANGE writers - + COUNT 2
1) 1) "1727835876126-0"
2) 1) "name"
2) "Virginia"
3) "surname"
4) "Woolf"
2) 1) "1727835876126-1"
2) 1) "name"
2) "Jane"
3) "surname"
4) "Austen"
xread
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id
[id ...]
$
代表特殊ID,表示以前Stream已经存储的最大的ID作为最后一个ID,当前Stream中不存在大于当前最大ID的消息,此时返回 nil
。0-0
代表从最小的ID开始获取Stream中的消息,当不指定 count
,将会返回Stream中的所有消息,注意也可以使用 0
(00/000/0000…) 也都是可以的
Count
代表读取几个数据,如果不指定,那就是全部读取。STREAMS
是必须指定的,从那个流中读取,这个 count 是指总共读取的条目,从第一个流中开始。里面的参数 id [id ...]
为每个流对应的起始 ID,表示从该 ID 之后开始读取条目。可以使用特定的条目 ID,或者使用特殊 ID 0
来读取所有历史条目,或者使用 $
只读取新条目。$
一般配合阻塞队列进行使用,表示从当前时间开始读取 count 条
XREAD COUNT 1 BLOCK 0 STREAMS mystream $ # 监听 mystream 的下一条新数据
xrevrange
XREVRANGE key end start [COUNT count]
时间复杂度: O ( N ) O(N) O(N),N 为要返回的元素个数
与 xrange
类似,只不过要返回逆序的,所以传递范围时,要把大的ID放前面,小的ID放后面
XREVRANGE somestream + -
XREVRANGE somestream + - COUNT 1
xtrim
XTRIM key <MAXLEN | MINID> [= | ~] threshold [LIMIT count]
时间复杂度: O ( N ) O(N) O(N)
截取流
MAXLEN
:保留最新的threshold
个 entryMINID
:保留比threshold
ID值大的 entry
redis> XADD mystream * field1 A field2 B field3 C field4 D
"1727836833889-0"
redis> XTRIM mystream MAXLEN 2
(integer) 0
redis> XRANGE mystream - +
1) 1) "1727836833889-0"
2) 1) "field1"
2) "A"
3) "field2"
4) "B"
5) "field3"
6) "C"
7) "field4"
8) "D"
返回值为从流中删除的 entries 个数。
2.10.1 消费组相关指令
redis> XGROUP CREATE mystream groupA $
OK
redis> XGROUP CREATE mystream groupB 0
OK
$
表示从 stream 尾部开始消费
0
表示从 stream 头部开始消费
创建消费组的时候必须指定ID,ID 为 0 表示从头开始消费,为 $
表示只消费新的消息
3. 持久化
- RDB(Redis DataBase):将数据做一份快照
- AOP(Append Only File):将所有命令按照顺序重新执行一遍
第一种相当于只看结果,不看过程。第二种就是把所有过程都执行一遍。这是两种持久化的方案。
redis 服务器如果不重启的话,是不会读取 rdb, aop 文件的。
3.1 RDB
实现类似照片记录效果的方式,就是把某一时刻(如每间隔10s)的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,恢复时再将硬盘快照文件直接读回到内存里,数据的可靠性也就得到了保证。这个快照文件就成为RDB文件(默认名为 dump.rdb), 其中,RDB就是Redis DataBase的缩写。
Redis 的数据都在内存中,保存备份时它执行的是全量快照,把内存中所有数据记录到硬盘中。
3.1.1 配置文件
Redis 6.2 之前
在 Redis.conf 配置文件中的 SNAPSHOTTING 下配置 save 参数,来触发 Redis 的 RDB持久化条件
##################################### SNAPSHOTTING ##############################
# Save the DB on disk:
# save <seconds> <changes>
#
#
#
save 900 1
save 300 10
save 60 10000
save m n
表示 m 秒内数据集存在 n 次修改时,自动触发保存
save 900 1: 每隔 900s,如果有超过 1 个 key 发生了变化,就写一份新的 RDB 文件
save 300 10: 每隔 300s,如果有超过 10 个 key 发生了变化,就写一份新的 RDB 文件
save 60 10000: 每隔 60s,如果有超过 10000 个 key 发生了变化,就写一份新的 RDB 文件
Redis 6.2 及 Redis 7 之后
Redis 7 之后降低了保存频率
# save <seconds> <changes> [<seconds> <changes>...]
save 3600 1 300 100 60 10000
3.1.2 触发
自动触发
修改配置文件,如5秒内2次修改触发保存
save 5 2
修改 dump 文件保存路径,在 SNAPSHOTTING 下有默认文件保存路径,默认为
dir ./
# 可修改如下, 文件夹必须要存在, 需要自己提前创建
dir /home/liu/redis-7.4.0/dumpfiles # 设置为绝对路径
可以看到之前的路径下是存过 dump.rdb 的。
指定 dump.rdb 文件名
# 默认文件名
dbfilename dump.rdb
# 修改为
dbfilename dump6379.rdb # 加上端口号,当有多个 redis 实例时用于区分
可以在命令端口直接查看配置,通过
config get ... # 查看
config set ... # 设置
config set save "3600 1 300 100 60 10000 5 2" # 设置快照时间, 保存默认的设置情况下新增一个 5 2
❓ 如何重启
在 redis 命令行内部使用 shutdown
然后使用命令
redis-server /home/liu/redis-7.4.0/redis.conf # 指定配置文件进行启动
❓ 如何修改配置文件
sudo nano redis.conf
我们测试一下,刚开始都是空的,没有键,也没有 rdb 文件
注意:如果不是5秒内修改两次,而是两次修改间隔超过了5秒,那也不会备份
恢复
将备份文件(dump.rdb)移动到redis安装目录启动服务即可
⚠️ 备份成功后可以先 flushdb 清空 redis,然后看看是否可以恢复数据
执行 flushall/flushdb
命令也会产生 dump.rdb 文件,但里面是空的,无意义
当使用 shutdown
关闭 redis 时,也会产生一个 dump.rdb 文件
如下,原来的 rdb 文件是存了数据的,保留备份后,flushdb,然后会产生一个新的rdb文件,但是它是空的,没有意义。
重启 redis
好,这里只是说明的一个注意点而已,然后我们如何恢复呢?(正常情况下,应该是将备份文件拷贝到这里进行恢复)
重启后恢复了
🌞 物理恢复,一定服务和备份分机隔离
不要把备份文件 dump.rdb 和生产 redis 服务器放在同一台机器,必须分开各自存储,以防生产机物理损坏后备份文件也挂了
保存方式都是下面说的 bgsave,使用子进程进行保存
手动触发
场景:当前的数据很重要,即使没达到自动触发条件,也想立刻保存
redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
save
在主程序中执行保存,会阻塞当前 redis 服务器,直到持久化工作完成,执行 save 命令期间,Redis 不能处理其他命令。所以生产环境下禁止使用,也就是说就当这个命令不存在
bgsave
redis 在后台异步进行快照操作,不阻塞 redis 服务器主进程,可以响应客户端请求,该触发方式会 fork 一个子进程,由子进程复制持久化过程。
如下,5秒内只添加了一个k1, 没法达到自动触发的条件,进行 bgsave 后可以看到 rdb 文件大小从 88 变为了 100,说明存储进去了
还可以使用 lastsave 命令获取最后一次成功执行快照的时间戳
使用
date -d @时间戳 # 可以获取时间戳对应的时间
3.1.3 优缺点
优点
- RDB 是 redis 数据的一个非常紧凑的单文件时间点表示。RDB 文件非常适合备份。例如,希望在最近的24小时内每小时归档一次RDB文件,并在30天内每天保存一个RDB快照。这可以在发生灾难时轻松恢复不同版本的数据集
- RDB非常适合灾难恢复,它是一个可以传输到远程数据中心或 Amazon S3(亚马逊云服务器)的压缩文件
- RDB最大限度的提高了 redis 的性能,因为 redis 父进程为了持久化而需要做的唯一工作就是派生一个将完成所有其余工作的子进程。父进程永远不会执行磁盘 I/O 或类似操作 (bgsave的好处)
- 与 AOF 相比,RDB 在恢复大数据集时更快一些y
- 在副本上,RDB 支持重启和故障转移后的部分重新同步
🐯 总结
- 适合大规模的数据备份
- 按照业务定时备份
- 对数据完整性和一致性要求不高
- RDB文件在内存中的加载速度要比AOF快得多
缺点
- 如果需要在 redis 停止工作时(例如断电后)将数据丢失的可能性降到最低,那么 RDB 并不好。我们可以配置生产RDB的不同保存点(例如,在对数据集至少5分钟和100次写入之后,可以有多个保存点)。但是,通常会每5分钟或更长时间创建一次RDB快照,因此,如果 redis 由于任何原因在没有正常关闭的情况下停止工作,就会丢失最新几分钟的数据。
- RDB 需要经常 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,fork() 可能会很耗时,因为 fork() 也要占用内存,并且如果数据集很大并且CPU性能不是很好,可能会导致 redis 停止位客户端服务几毫秒甚至一秒钟。AOF也需要 fork() 但频率较低,如果不需要对持久性进行任何权衡,可以调整要重写日志的频率
快照丢失案例演示
5秒内两次写入,k1,k2,可见快照被保存了,再写入 k3, 并没有被保存,然后我们关掉 redis
重启 redis,可以看到 k3 丢失
3.1.4 检查修复rdb文件
在往rdb文件中写入或者迁移的时候是可能出现文件破损的,如如下命令
set k1 v11111111111111111111 # 写到 v111 后面还没写完时电脑宕机
那么这条数据的破损会导致整个文件读不了
切换到 /user/local/bin
目录下,该目录下可以使用 redis 的命令
使用 redis-check-rdb
修复 rdb 文件,如果修复不了,那很倒霉,只能想其他办法了
3.1.5 禁用快照
哪些情况下会触发 RDB 快照?
- 配置文件中默认的快照配置
- 手动 save/bgsave 命令
- 执行 flushall/flushdb 命令也会产生 dump.rdb 文件,只不过里面是空的
- 执行 shutdown 且没有设置开启 AOF 持久化
- 主从复制时,主节点自动触发
🐯 如何禁用快照,即关闭 RDB 持久化。
在配置文件里进行修改
# 修改默认配置为如下
save ""
3.1.6 rdb优化参数
SNAPSHOTTING 模块的其他参数配置
stop-writes-on-bgsave-error yes
默认为 yes,表示写入操作失败时,就停止写操作,避免导致数据丢失,就用 yes 就行
rdbcompression yes
对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用LZF算法进行压缩。
如果不想消耗CPU来进行压缩的话,可以设置关闭此功能。也是用 yes 就行。
rdbchecksum yes
在存储快照后,还可以让 redis 使用CRC64算法来进行数据校验(保证数据的一致性,完整性),但是这样做会增加大约10%的性能消耗,如果希望得到最大的性能提升,可以关闭此功能。也是用 yes 就行
rdb-del-sync-files no
(RDB主从复制中用到)在没有持久化的情况下删除复制中使用的RDB文件启用。默认情况下是 no,用 no 就行。
3.2 AOF
前面说的RDB可能会出现最新的数据没有持久化,所以出现了AOF。它以日志的形式来记录每个写操作,将 redis 执行过的所有写指令记录下来(读操作不记录),只需追加文件但不可以改写文件(此处文件指aof文件),redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 默认情况下,redis 是没有开启 AOF(append only file) 的。开启AOF功能需要设置配置:
appendonly yes
AOF 保持的文件名为 appendonly.aof。
3.2.1 AOF 持久化工作流程
- Client 作为命令的来源,会有多个源头以及源源不断的请求命令 (比如Java客户端)
- 在这些命令到达 redis server 以后并不是直接写入 AOF 文件,会将其这些命令先放入AOF缓存中进行保存。这里的AOF缓存区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘IO操作
- AOF缓冲会根据AOF缓冲区同步文件的三种写回策略将命令写入磁盘上的AOF文件。
- 随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称AOF重写),从而起到AOF文件压缩的目的
- 当 redis server 服务器重启的时候会从AOF文件载入数据
3.2.2 写回策略
配置文件如下
# appendfsync always
appendfsync everysec(默认的写回策略)
# appendfsync no
always
同步写回,每个写命令执行完立刻同步地将日志写回磁盘everysec
每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入磁盘no
操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘,如 linux 系统默认是 30 秒(好长)
配置项 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
always | 同步写回 | 可靠性高,数据基本不丢失 | 每个写命令都要写入磁盘,性能影响较大 |
everysec | 每秒写回 | 性能适中 | 宕机时丢失1秒内的数据 |
no | 操作系统控制的写回 | 性能好 | 宕机时丢失数据较多 |
3.2.3 配置文件
开启AOF
AOF 默认是关闭的,需要在配置文件中进行设置,在 APPEND ONLY MODE
模块下
appendonly no
# 修改为如下
appendonly yes
写回策略配置
# appendfsync always
appendfsync everysec
# appendfsync no
文件保存路径
🐯 redis6 时,AOF保存文件的位置和RDB保存文件的位置一样,都是通过 redis.conf 配置文件中的 dir 配置,如下
# The Append Only File will also be created inside this directory
#
# Note that you must specify a directory here, not a file name.
dir /myredis/dumpfiles
如此,appendonly.aof
与 dump.rdb
都在目录 /myreids/dumpfiles
下
🐯 redis7 之后
加入了属性 appenddirname
,它会在 dir 下新建一个 appenddirname 的文件夹用于存放 aof 文件
appenddirname "appendonlydir"
如此,appendonly.aof
会存放在 /myredis/dumpfiles/appendonlydir
下,而 dump.rdb
放在 /myredis/dumpfiles
中。
文件名
redis6 之前只有一个 appendonly.aof 文件
appendfilename "appendonly.aof"
redis7 之后进行了优化,采用 multi part AOF。从一个文件变为了三个,不过配置项还是一个,只不过后面产生的文件会有多个
# - appendonly.aof.1.base.rdb as a base file.
# - appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof as incremental files.
# - appendonly.aof.manifest as a manifest file.
appendfilename "appendonly.aof"
MP-AOF 就是将原来的单个AOF文件拆分成多个AOF文件。在MP-AOF中,我们将AOF分为三种类型
- BASE:表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个
- INCR:表示增量AOF,它一般会在AOFRW开始执行时被创建,该文件可能存在多个
- HISTORY:表示历史AOF,它有BASE和INCR AOF变化而来,每次AOFRW成功完成时,本次AOFRW之前对应的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被redis自动删除。
为了管理这些AOF文件,引入了一个 manifest(清单)文件来跟踪、管理这些AOF。同时,为了便于AOF备份和拷贝,我们将所有的AOF文件和manifest文件放入一个单独的文件目录中,目录名由appenddirname配置(redis7.0新增配置项)决定
1. 基本文件
appendonly.aof.1.base.rdb
2. 增量文件
appendonly.aof.1.incr.aof
appendonly.aof.2.incr.aof
3. 清单文件
appendonly.aof.manifest
AOF 重写期间是否同步
使用默认的就可以
no-appendfsync-on-rewrite no
3.2.4 恢复
正常恢复
记得修改默认配置 appendonly no 改为 yes,然后我也把 dir 改为 /home/liu/redis-7.4.0/bakfiles
(原来叫dumpfiels, 见名知意,只适合保存RDB文件) 。
第一步,先验证 backfiles 里面为空的,然后5s内存入两个键,触发了 rdb,看第三步,可以看到 bakfiles 中出现了 dump6379.rdb 文件,而文件夹 appendonlydir 中也有了 aof 文件
为了验证 aof 的恢复功能,我们测试如下
- 存储3个键值
- 删除产生的 rdb 文件
- 关闭 redis 服务器
- 删除产生的 rdb 文件(shutdown也会产生rdb文件)
- 查看 aof 文件
- 开启 redis
- 恢复成功
注意:flushdb 等操作也同样会修改 aof 文件
在此基础上继续实验
可以看到,如果新添加数据,incr.aof
文件会增大,而其他文件大小不变。查询数据不会改变文件大小
异常恢复
比如,我们在往磁盘aof文件中写入时,没写完而宕机了。那么aof文件就会异常
我们先查看一下 incr.aof
文件中有什么
其实就是记录的操作嗷,我们可以模拟一下错误,在最后加一段乱七八槽的东西
关闭服务器后再重启,发现如果 aof 文件错误就根本启动不了
我们可以使用异常修复命令:redis-check-aof --fix
进行修复,只需要修复 incr.aof 文件即可,记得确认
然后我们可以看一下 incr.aof 文件的内容,最后一行已经被它清理掉了
然后再重启 redis,发现成功了,且数据存在
3.2.5 优缺点
优点
-
使用 AOF Redis 更加持久:可以使用不同的 fsync 策略:no, everysec, always。使用 everysec fsync 策略,写入性能仍然很棒。fsync 是使用后台线程执行的,当没有 fsync 正在进行时,主线程将努力执行写入,因此最多只能丢失一秒钟的写入
-
AOF 日志是一个仅附加日志,因此不会出现寻道错误,也不会在断电时出现损失问题。即使由于某种原因(磁盘已满或其他原因)日志以写一半的命令结尾,redis-check-aof 工具也能够轻松修复它
-
当 AOF 变得太大时,redis 能够在后台自动重写 AOF。重写是完全安全地,因为当 redis 继续添加到旧文件时,会使用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,redis 就会切换两者并开始附加到新的那一个(可以说是减肥计划)
-
AOF 以易于理解和解析的格式一次包含所有操作的日志。甚至可以轻松导出AOF文件。例如,不小心使用该 flushall 命令刷新了所有内容,只要在此期间没有执行日志重写(就是上面所说的减肥计划),我们可以通过停止服务器、删除最新命令(打开那个 incr.aof 删除那个命令)并重新启动redis来保存数据集。
劣势
- 相同数据集的数据而言,aof 文件要远大于 rdb 文件,恢复速度慢于 rdb
- aof 运行效率要慢于 rdb, everysec 同步策略效率较好,no 策略和 rdb 差不多一样快
3.2.6 AOF 重写机制
由于 AOF 持久化是 redis 不断将写命令记录到 AOF 文件中,随着 redis 不断的运行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。
为了解决这个问题,redis 新增了重写机制,当 AOF 文件的大小超过所设定的峰值时,redis 就会自动启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
或者,可以手动使用命令 bgrewriteaof
来重写。
自动重写
auto-aof-rewrite-percentage 100 # 相对于上次重写后文件增大多少倍才重写
auto-aof-rewrite-min-size 64mb # 同时满足才会重写
该配置的意思是,文件大小相比 上次文件rewrite后的大小 增长了1倍,并且文件大小超过64mb时就会重写。
🐯 解释一下保留可恢复数据的最小指令集
比如有个 key,执行了三次 set 操作
set k1 v1
set k1 v2
set k1 v3
如果不重写,那么这3条语句都在aof文件中,内容不仅占空间且恢复时要执行3条命令,而实际效果只需要 set k1 v3
这一条。所以开启重写后,只需要保存 set k1 v3
就可以了,只需要保存最后一次修改值。
AOF 重写不仅降低了文件的占用空间,同时更小的AOF也可以更快地被redis加载
触发重写后,原来的文件名及大小会变更
0 appendonly.aof.1.base.aof
940 appendonly.aof.1.incr.aof
变为
126 appendonly.aof.2.base.aof
0 appendonly.aof.2.incr.aof
重写后,数据存入 base.aof,incr.aof
清空,然后在此 base 上继续往下加的记录在 incr.aof
中
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。每次重写都会生成新的 aof 文件并删除原来的。
重写原理
- 在重写开始前,redis 会创建一个重写子进程,这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
- 与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
- 当重写子进程完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中
- 当追加结束后,redis 就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中
- 重写 aof 文件的操作,并没有读取旧的 aof 文件,而是将整个内存中的数据库内容用命令的方式重写一个新的 aof 文件,这点和快照类似
第二个很聪明,在重写过程中,如果有新的写指令,往旧的AOF文件中写,并且缓冲区写一份,当重写完毕后,也就是新的base文件,然后将缓冲区的内容写到新的incr文件,这就是一份新的内容,可以完美代替原来的了。即使重写过程中出现问题,旧的还未被替换掉,仍然可以用。
🐯 还有一个关于 aof-rdb 的配置项
aof-use-rdb-preamble yes # 默认开启aof-rdb混合
3.3 AOF + RDB
RDB是默认使用的数据备份方式,AOF需要手动开启,二者可以同时存在。(开启AOF和RDB,但是没开启混合模式)
但是重启时只会加载 aof 文件,不会加载 rdb 文件,因为通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。那既然如此要不要只是用AOF呢?不建议,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个以防外一的手段。
好,上面那个就是废话,有更好的,开启RDB+AOF混合持久化,当然你也得先开启AOF
# 当重写 AOF 文件时,Redis能够在 AOF 文件中使用 RDB 前导以实现更快的重写和恢复。当开启此选项时,
# 重写后的 AOF 文件由两个不同的部分组成:
# [RDB 文件][AOF 尾部]
# 当加载 Redis 时,它会识别 AOF 文件以"REDIS"字符串开头,并加载带有前缀的 RDB 文件,然后继续加载 # AOF 的尾部。
aof-use-rdb-preamble yes
也就是 RDB 作为 base 文件,AOF 作为增量文件。混合持久化结合了RDB和AOF持久化的优点,开头为RDB格式,可以使Redis启动的更快,同时结合AOF的优点,又降低了大量数据丢失的风险。
3.4 纯缓存模式
不进行持久化,只用来作高速缓存,也就是把RDB,AOF全部关掉。
save "" # 禁用 rdb
appendonly no # 禁用 aof
注意:虽然rdb被禁用了,但是仍可以使用 save, bgsave 命令生成 rdb 文件。同样,也可以使用命令 bgrewriteaof 生成 aof 文件。
4. 事务
一组命令,要么都执行,要么都不执行。一个事务中所有命令都会序列化,按顺序的串行化执行而不会被其他命令插入。
开启:MULTI
开启一个事务
入队:将多个命令放入队列,这些命令不会立即执行,而是排队等待
执行:由 EXEC
命令触发事务
4.1 命令
命令 | 描述 |
---|---|
DISCARD | 取消事务,放弃执行事务块内的所有命令 |
EXEC | 执行所有事务块内的命令 |
MULTI | 标记一个事务块的开始 |
UNWATCH | 取消 WATCH 命令对所有 key 的监视 |
WATCH key [key...] | 监视一个(或多个)key,如果在事务执行之前这个(或这些)key 被其他命令所改动,那么事务将被打断 |
4.2 实例
4.2.1 正常执行
使用 MULTI
开启事务,中间加入要执行的命令,最后 EXEC
执行命令,这些命令都是被放在队列中的,最后会按序执行
4.2.2 放弃执行
当我们在开启事务后,中间遇到某种情况要放弃时,使用 DISCARD
命令,则该事务中所提到的所有命令都不会执行。
4.2.3 全体连坐
在 EXEC
命令之前,如果一系列命令出现语法错误,则 redis 会直接返回错误,所有的命令都不会执行
比如此处,set k2
语法错误,事务执行后会报错,由于之前的错误,事务执行中断。之后 get k1
得到的值是没有改变的值。里面所有的命令都不会执行
4.2.4 冤头债主
如果命令没有语法错误,而是执行事务的时候出现了错误,那么执行出错的命令不会影响其他命令的执行。
注意:事务无法回滚。
4.2.5 watch 监控
redis 使用 watch 来提供乐观锁定,类似于 CAS(check-and-set)
使用 WATCH
来监控一个键,如果在 EXEC
之前没有人修改过该键,那么事务成功执行,否则全部不执行,返回 nil
由于 WATCH
命令是一种乐观锁的实现,Redis 在修改的时候会检测数据是否被修改,如果更改了,则执行失败。由于第5步已经修改了 balance
,监控已经发现它被修改过了,所以事务执行失败
也可以使用 UNWATCH
来放弃监控
第二步发现监控过程中 balance
已经被人修改了,那就可以使用 UNWATCH
来取消监控,继续写事务,让事务能够重新执行。
一旦执行了
EXEC
,则之前加的监控锁都会被取消掉当客户端连接丢失的时候(比如退出链接),所有东西都会被取消监视
5. 管道
当我们 redis客户端向服务端发送命令时,比如三条 set 命令,那么需要经过一万次 发送-执行-返回(这里可以使用一条 mset 来解决)。这种频繁命令的往返会造成性能瓶颈。
redis 是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。一个请求会遵循一下步骤:
- 客户端向服务端发送命令分四步(发送命令->命令排队->命令执行->返回结果),并监听 Socket 返回,通常以阻塞模式等待服务端响应
- 服务端处理命令,并将结果返回给客户端
上述两步称为:Round Trip Time,简称RTT,数据包往返于两端的时间
Redis 客户端与服务端的交互模型如下
如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了RTT,而且还频繁调用系统IO,发送网络请求,同时需要redis调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好。
管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理完完毕后,通过一条响应一次性将结果返回,通过减少客户端与redis的通信次数来实现降低往返延时时间。pipeline 实现的原理就是队列,先进先出特性就保证数据的顺序性。
Pipeline 定义:它是为了解决RTT往返时间,仅仅是将命令打包一次性发送,对整个 redis 的执行不造成其它任何影响。属于批处理命令变种优化措施,类似 redis 的原生批处理命令(mget和mset)
5.1 操作
管道处理操作是在客户端外部使用命令进行操作的,首先使用 nano
将一批命令写入 cmd.txt 文件中。cat
命令会将文本文件的内容进行返回
cat cmd.txt | redis-cli -a redis --pipe
会将 cmd.txt 文件中的内容发送给 redis 客户端使用管道进行发送。可以看到 errors: 0, replies: 6
,表明 6 个命令返回 6 个结果,没有错误出现。
5.2 注意
原生批量命令与管道对比
- 原生批量命令是原子性(例如:mset,mget),pipeline 是非原子性
- 原生批量命令一次只能执行一种命令,pipeline 支持批量执行不同命令
- 原生批量命令是服务端实现,而 pipeline 需要服务端与客户端共同完成
事务与 pipeline
- 事务具有原子性,管道不具有原子性
- 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到 exec 命令后才执行,管道不会
- 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会
注意事项
- pipeline 缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
- 使用 pipeline 组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能太久,同时服务器端此时也被迫回复一个队列答复,占用很多内存。(假如整个十万条之类的,那太多了,可以 100 条一次之类的)
6. 发布订阅
了解即可,用不到,会用 kafka, rabbitMQ 等去做。此处就不介绍
7. 复制
7.1 介绍
实际运用中不可能只有一台 redis 服务器,是用多台的。
复制就是主从复制,master 以写为主,Slave 以读为主。当 master 数据变化的时候,自动将新的数据异步同步到其他 slave 数据库。
有什么好处呢?
- 读写分离
- 容灾恢复(由于写的数据会同步到其他 slave 数据库,所以当master坏掉的时候,也可以从slave中恢复)
- 数据备份(多台 redis)
- 水平扩容支持高并发(有多个 slave, 可以将用户请求读的命令进行分散,平均一下高并发的压力)
7.2 操作
只需要配置 slave 库,master 库不需要配置
当 slave 想要从 master 中拷贝数据时,我们需要指定主机是哪个并且需要主机同意。
master 如果配置了 requirepass 参数,需要密码登录
那么 slave 就要配置 masterauth 来设置校验密码,否则的话 master 会拒绝 slave 的访问请求。
masterauth redis
7.2.1 基本命令
命令 | 描述 |
---|---|
info replication | 可以查看复制节点的主从关系和配置信息 |
replicaof 主库IP 主库端口 | 从机要访问的主机IP及端口,一般写入 redis.conf 配置文件 |
slaveof 主库IP 主库端口 | 在运行期间修改 slave 节点的信息,如果该数据库已经是某个数据库的从数据库, 那么会停止和原主数据库的同步关系,转而和新的主数据库同步,重新拜老大 |
slaveof no one | 使当前数据库停止与其他数据库的同步,转成主数据库,自己一个人 |
7.2.2 架构
一个 master 两个 slave。准备3台虚拟机,每台都安装 redis。三个 redis 服务分别位于不同的 IP 和端口号。比如
master 192.168.153.129:6379
slave 192.168.153.130:6380
slave 192.168.153.131:6381
那如何实现 VMWare 中多个虚拟机互通呢?步骤如下
- 打开编辑中的虚拟网络编辑器,点击更改设置
- 将原来的VMnet8删除,并新添加一个 VMnet8,设置如下
- 注意DHCP设置中的起始IP,结束IP,你的虚拟机自定义IP地址时必须在这个范围内
- 对虚拟机的网络适配器进行设置
- 启动虚拟机后,对右上角的有线连接自定义IPv4地址(要在起始IP和结束IP之间),填入前面NAT设置中的,子网掩码,网关IP等。
之后在每个虚拟机上去 ping 另外两台虚拟机,确保能够 ping 通,比如在 192.168.153.130 这台机器上
ping 192.168.153.129
ping 192.168.153.131 # 两条命令都要可以执行成功
然后对每个配置文件都修改好文件名
- redis6379.conf
- redis6380.conf
- redis6381.conf
mv redis.conf redis6379.conf
修改配置文件中的 port 属性,修改使用的端口号
那么在启动客户端的时候就需要指定端口号了
redis-cli -a redis -p 6380
7.3 配置文件
不同虚拟机redis配置文件进行简单配置
daemonize yes
默认配置为 no,改为 yes,让 redis 作为守护进程,在后台运行
注释掉
bind 127.0.0.1
或者改为 bind 0.0.0.0
,这样 redis 就可以接受其他主机的连接请求,默认只能接受本机的连接请求
protected-mode no
保护模式设置为 no
port 6379
指定你的端口号
dir /home/liu/redis-7.4.0/bakfiles
指定当前工作目录
pid 文件名字,
pidfile
pidfile /var/run/redis_6379.pid
不需要修改,该文件记录了进程的pid和端口号,注意,不论主机从机这个都不要该,不要修改那个后缀
log 文件名字,
logfile
指定好文件路径及日志级别
requirepass redis
设置密码
dbfilename dump6379.rdb
--> 加端口号就可以
设置 rdb 文件名称
aof 文件,
appendfilename
appendonly yes
appendfilename "appendonly.aof"
appenddirname "appendonlydir"
这个可选
至此,前面 10 条是主机从机都要配置的,这一条只需要从机进行配置
配置
replicaof
和masterauth
配置主机的IP和端口号以及主机redis密码
7.4 常用
7.4.1 一主二仆
主机负责写操作,从机负责读操作
配置文件
一台主机,两台从机,注意区别端口号,dump文件名,主机IP端口及密码等
注意:先启动主机,再启动从机
主机日志:从机的连接
从机日志:连接主机
今后出了错就来查看即可
在主机上通过 info replication
查看信息,可以看到该 redis 为主机,有两个从机,并可以看到它们的id,port以及状态
在从机上通过 info replication
查看信息,可以看到从机的身份,主机的信息等
🐯 数据可以同步吗
答:可以
主机:
从机:
开始时,主机和从机都是没有数据的,主机 set k1 v1
后,主机从机都可以读取到数据
🐯 从机可以执行写命令吗
答:不可以
从机:
从机只可读不可写
🐯 从机切入点问题
slave 是从头开始复制还是从切入点(即开始连接时)开始复制?
案例:
- master 启动,写 k1, k2, k3
- salve1 和 master 同时启动,可以观察到能够读取到 k1, k2, k3
- slave2 在 master 写到 k3 后才启动,之前的数据是否可以读取到?
答:可以。即使从机中间宕机了,再次启动后仍能够同步主机的数据
🐯 主机shutdown后,从机会上位吗
主机 shutdown,我们查看从机状态
两台从机都仍是 slave,可以看到主机 down 了。主机宕机后,从机仍然可以读,等主机回归。
🐯 主机shutdown后,重启后主从关系还在吗,从机还能否顺利复制
还在,还能顺利复制
手动配置
从机中不配置主从关系,可以手动进行配置。比如此时三台机器都是 master。没有从属关系
那任意一台可以通过命令 slaveof 主库IP 主库端口
,config set masterauth
设置所属主机的信息,之后就作为了它的从机。但是当该台机器宕机重启后,它还是 master,从属关系就没了
所以使用配置文件的方式是持久稳定的,命令是临时的,当次生效。
当配置文件中已经配置了主从关系时,使用命令也是临时修改,当 redis 重启后,还是会读取配置文件中的
7.4.2 薪火相传
因为如果只有一个主机,多个从机,从机全部连接在一个主机上,主机的压力也是比较大的,因为它不仅要负责所有的写操作,还要对从机的数据进行同步,连接到主机的越多,master 的性能越差,会出现网络抖动和数据传输的延时。
上一个 slave 可以是下一个 slave 的 master,slave 同样可以接受其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master,可以有效减轻主 master 的写压力。
中途变更转向,会清除之前的数据,重新建立拷贝最新的。即 slaveof 新主库IP 新主库端口
,当然你改配置文件也可以。
7.4.3 反客为主
使用命令 slaveof no one
,自己就变为一个独立的 master。但是数据还在
7.5 复制流程
-
slave 启动,同步请求
- slave 启动成功连接到 master 后会发送一个 sync 同步命令
- slave 首次连接到 master,一次完全同步(全量复制)将被自动执行,slave 自身原有数据会被 master 数据覆盖清除
-
首次连接,全量复制
- master 节点收到 sync 同步命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master 节点执行RDB持久化完后,master 将 rdb 快照文件和所有缓存的命令发送到所有 slave,以完成一次完全同步
- 而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
-
保持通信
master 发出 ping 包的周期,保持联系,默认为10秒repl-ping-replica-period 10
-
进入平稳,增量复制
master 继续将新的所有收集到的修改命令自动依次传给 slave,完成同步 -
从机下线,重连续传
master 会检测 backlog 里面的 offset,master 和 slave 都会保存一个复制的 offset 还有一个 masterId,offset 是保存在 backlog 中的。master 只会把已经复制的 offset 后面的数据复制给 slave。类似于断点续传。
就是 master 会在每次同步时有一个 offset 记录在 backlog 中,slave 也会跟着记录。在重连时会把 slave 日志中 offset 后面缺失的补回来。offset 当作时间线来理解即可。
7.6 缺点
由于所有的写操作都是先在 master 上操作,然后同步更新到 slave 上,所以从 master 同步到 slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,slave 机器数量的增加也会使这个问题更加严重。
master 宕机后,就无法进行写操作了,所有从属机只能等主机回归
8. 哨兵
当不使用集群时哨兵会提供一个高可用redis
前面复制讲了复制机制中如果主机宕机了就无法进行写操作了。所以引入了哨兵
哨兵巡查监控后台master主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外服务。俗称无人值守运维
监听 redis 运行状态,包括 master 和 slave
8.1 作用
主从监控
监控主从 redis 库运行是否正常
消息通知
哨兵可以将故障转移的结果发送给客户端
故障转移
如果 master 异常,则会进行主从切换,将其中一个 slave 作为新的 master
配置中心
客户端通过连接哨兵来获得当前 redis 服务的主节点地址
8.2 实例
3 个哨兵,防止一个哨兵挂了就完了。自动监控和维护集群,不存放数据,只是吹哨人
1 主 2 从,用于数据读取和存放
3 个哨兵,应该是除了 1主2从 外新建 3 个虚拟机,作为 3 个哨兵,但是 16G 内存 3个虚拟机都有点不卡,所以将这 3 个哨兵放在主机上了
哨兵的配置是在 sentinel.conf 文件中,可以理解 redis 和 sentinel 是两个独立的东西,分别由不同的配置文件进行控制,可以打开看一下里面的内容。我们把三个哨兵的配置文件都放在主机中
8.2.1 配置文件说明
里面的配置文件和 redis.conf 差不多
bind
:服务监听地址,用于客户端连接,默认本机地址
daemonize
:是否以后台 daemon 方式运行
protected-mode
:安全保护模式
port
:端口
logfile
:日志文件路径
pidfile
:pid 文件路径
dir
:工作目录
sentinel monitor <master-name> <ip> <redis-port> <quorum>
:设置要监控的 master 服务器,quorum
表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数(即有 quorum 哨兵都认为 master 宕机了才进行后续操作)
因为网络是不可靠的,有时候一个 sentinel 会因为网络堵塞而误以为一个 master redis 已经宕机了,在 sentinel 集群环境下需要多个 sentinel 互相沟通来确认某个 master 是否真的宕机了,quorum 这个参数是进行客观下线的一个依据,意思是至少有 quorum 个 sentinel 认为这个 master 有故障,才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因,导致无法连接 master,而此时 master 并没有出现故障,所以,这就需要多个 sentinel 都一致认为该 master 有问题,才可以进行下一步操作,这就保证了公平性和高可用。
sentinel auth-pass <master-name> <password>
:master 设置了密码,连接 master 服务的密码
还有五个,第一个后面会再讲,后面四个通常用默认的
sentinel down-after-milliseconds <master-name> <milliseconds>
:指定多少毫秒后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线
sentinel parallel-syncs <master-name> <nums>
:表示允许并行同步的 slave 个数,当 master 挂了后,哨兵会选出新的 master,此时,剩余的 slave 会向新的 master 发起同步数据
sentinel failover-timeout <master-name> <milliseconds>
:故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败。即虽然选举成功了,但是故障转移超时了
sentinel notification-script <master-name> <script-path>
:配置当某一事件发生时所需要执行的脚本
sentinel client-reconfig-script <master-name> <script-path>
:客户端重新配置主节点参数脚本
将哨兵配置文件复制 3 份保存到主机目录下
然后编写配置文件中的内容
基本每个配置文件都如下,就端口号不一样,port,logfile,pidfile 三个不同,三个的 sentinel monitor 和 sentinel auth-pass
都一样
bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/home/liu/redis-7.4.0/sentinel26379.log"
pidfile "/var/run/redis-sentinel26379.pid"
dir /home/liu/redis-7.4.0
sentinel monitor mymaster 192.168.153.129 6379 2 # 主机IP+端口号
sentinel auth-pass mymaster redis # 主机密码
之后按序把咱们的一主二从三个虚拟机打开,注意,这里主机的配置文件要修改一下
是不是有疑问,为什么主机也要设置 masterauth,它不是作为主机连接从机时才需要吗?
是的,确实是这样,但是由于哨兵的存在,当主机宕机后,它可能会变为从机,如果不设置 masterauth
,它将无法连接到新的主机上,后续可能报错 master_link_status:down
8.2.2 开启哨兵
使用命令
redis-sentinel sentinel26379.conf --sentinel
启动三个哨兵配置文件
之后再测试主从复制,发现没有问题
查看端口使用情况,三个哨兵全部在运行
查看一下26379的哨兵的日志文件
有三个 Sentinel ID,表示三个哨兵,monitor 和两个 slave 表示了一主二从
那再看一下26380的日志问卷
🐯发现其实哨兵记录的内容都是一样的,为什么?
因为正常情况下,一个哨兵位于一台服务器上,三个哨兵就应该有3台,当一个哨兵意外挂了的时候,也不会出现太大问题,哨兵机制仍可以正常使用,这也要求任何一个哨兵记录的内容都要是完整的。
🐯然后我们注意一下日志中有一行 Sentinel new configuration saved on disk
,这是什么呢,这里修改的是哨兵的配置文件
sentinel26379.config
sentinel26380.config
这里的 known-replica
是对于哨兵监控的 master 中的从机信息
known-sentinel
记录了除自身的哨兵ID,其他的一块进行监控的所有哨兵ID。来知道哪些哨兵是一伙的
8.2.3 测试哨兵
我们手动 shutdown
关闭 6379 主机,模拟 master 宕机,可以发现它关机时间比较久,0.87s
然后我们在另外两台机器测试还能否读取数据
都是第一次不行,第二次才可以,而且出错的原因不一样。
解释:这两个错误本质上是同一个,是因为 master 宕机,它的读/写管道关闭了,客户端读取超时而关闭了连接,,,类似地,不懂,反正你记住出现这个问题是正常的,重新发送命令就可以了
🐯 master 更换
6380 变为主机,6381 变为从机
我们可以看一下 log 日志文件看一下
从中我们可以看出,6380变为新master,6379,6381变为从机。也就是说即使 6379 回归,也是从机,不再是主机了。
🐯 你把所有有关哨兵的配置文件、日志都删除后,发现即使所有的 redis 服务器重启,它们的主从关系还是哨兵修改后的
是因为它修改了 redis.conf 配置文件(加在配置文件末尾),我们先来看一下 redis6379 配置文件
拜了新的老大,再看 redis6380 配置文件,文件末尾是没有什么的
但是我们原来6380作为从机是配置了 replicaof
的
可以看到已经被删除了
结论
- 文件的内容,在运行期间会被 sentinel 动态进行更改
- master-slave 切换后,master_redis.conf、slave_redis.conf 和 sentinel.conf 的内容都会发生改变,master_redis.conf 会多一行 slaveof 的配置,sentinel.conf 的监控目标会随之调换
🐯 注意
- 哨兵只负责监控,业务压力不大
- 生产上都是一个哨兵放在一台服务器上,很少出现所有哨兵全部挂掉的情况
- 一套哨兵可以同时监控多个 master
只需要在配置文件中,把要监控的对象再加一份即可,注意换换名字,这里是 mymaster 和 resque 两个要监控的 master
8.3 运行流程
当一个主从配置中的 master 失效之后,sentinel 可以选举出一个新的 master 用于自动接替原 master 的工作,主从配置中的其他 redis 服务器自动指向新的 master 同步数据。一般建议 sentinel 采取奇数台,防止某一台 sentinel 无法连接到 master 导致误切换。
1. 三个哨兵监控一主二从,正常运行中
2. SDOWN:主观下线
-
是单个 sentinel 自己主观上检测到的关于 master 的状态。在 sentinel 的角度来看,如果发送了 PING 心跳包后,在一定时间内没有收到合法的回复,就达到了 SDOWN 的条件。那么这个 sentinel 单方面认为这个 master 不可用了
-
sentinel 配置文件中的
down-after-milliseconds
设置了判断主观下线的时间长度# 默认是 30s sentinel down-after-milliseconds mymaster 30000
3. ODOWN:客观下线
- ODOWN 需要一定数量的 sentinel,多个哨兵达成一致意见才能认为一个 master 客观上已经宕掉
quorum
就是客观下线的一个依据。意思是至少有quorum
个sentinel
认为这个 master 有故障才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因导致无法连接 master,而此时 master 并没有出现故障,所以这就需要多个 sentinel 都一致认为该 master 有问题,才可以进行下一步操作,这就保证了公平性和高可用
4. 选举出领导者哨兵
当主节点被判断客观下线后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点也即被选举出的兵王进行 failover (故障迁移) 即下面的 vote-for-leader
由一个 leader 去推动整个故障迁移即可。
❓ 怎么选出来的呢?
Raft 算法,这里简单介绍一下:监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是Raft算法,基本思路是先到先得,即在一轮选举中,哨兵A向B发送称为领导者的申请,如果B没有同意过其他哨兵,则会同意A称为领导者
5. 由兵王推动故障切换流程并选出一个新 master
🐯 选举新王
在剩余健康的 slave 节点中进行选举
- 优先级高的作为新 master,若优先级相同,进入第二步
即redis.conf
文件中,优先级slave-priority
/replica-priority
最高的从节点 (值越小优先级越高),默认值为 100。slave-priority
是 redis5.0之前用的,之后更名为replica-priority
- 复制偏移位置 offset 最大的从节点作为新的 master,若相同,进入下一步
因为偏移量越大,说明数据越新 - 最小的 Run ID 的从节点作为新的 master。字典顺序,ASCII 码
🐯 群臣俯首
执行 slaveof no one
命令让选出来的从节点成为新的主节点,并通过 slaveof
命令让其他节点成为其从节点
- Sentinel leader 会对选举出的新 master 执行
slaveof no one
操作,将其提升为 master 节点 - Sentinel leader 向其他 slave 发送命令,让剩余的 slave 成为新的 master 节点的 slave
🐯 旧主拜服
将之前已下线的老 master 设置为新选出的新 master 的从节点,当老 master 重新上线后,它会成为新 master 的从节点。Sentinel leader 会让原来的 master 降级为 slave 并恢复正常工作。
总结
上述的 failover 操作均由 sentinel 自己独自完成,完全不需要人工干预
8.4 使用建议
- 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
- 哨兵节点的数量应该是奇数
- 各个哨兵节点的配置应一致(内存、硬件配置之类的相同)
- 如果哨兵节点部署在 Docker 等容器里面,尤其要注意端口的正确映射
- 哨兵集群+主从复制,并不能保证数据零丢失
解释第5点,当 master 主机挂掉后,在选举新的 master 过程中,有大约 5~10s 是不能进行写入操作的,会有数据流失。所以推荐使用下一节的集群,redis 官方也这么说(经典白学)
9. 集群
9.1 定义
由于数据量过大,单个 master 复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只复制存储整个数据集的一部分,这就是 redis 的集群,其作用是提供在多个 redis 节点间共享数据的程序集。
redis 集群是一个提供在多个 redis 节点间共享数据的程序集
redis 集群可以支持多个 master
9.2 功能
- redis 集群支持多个 master,每个 master 又可以挂载多个 slave
- 由于 cluster 自带 sentinel 的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能
- 客户端与 redis 的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
- 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系
9.3 算法
9.3.1 槽位
集群的密钥空间被分为 16384 个槽,有效地设置了 16384 个主节点的集群大小上限(但是,建议的最大节点大小约为 1000 个节点)
redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分 hash 槽,举个例子,比如当前集群有3个节点,那么:
HASH_SLOT = CRC16(key) mod 16384
每个 key 都通过计算得到对应的 slot 槽位
这里的 redis 个数就是 Master1,Master2,Master3,看个数来划分 hash 槽。如算出来的slot值为 [0, 5460] 就去 Master1。
9.3.2 分片
什么是分片
使用 redis 集群时我们会将存储的数据分散到多台redis机器上,这称为分片。简言之,集群中的每个redis实例都被认为是整个数据的一个分片。
🐯 如何找到给定 key 的分片
对 key 进行CRC16(key)算法处理并通过对总分片数量取模。然后,使用确定性哈希函数,这意味着给定的key将多次始终映射到同一个分片,我们可以推断将来特定key的位置。
这张图就是3个分片,一个分片包含多个槽位。key相同时,读和写都是在同一个槽位。
9.3.3 优势
槽位+分片的优势:方便扩缩容和数据分派查找
这种结构很容易添加或者删除节点,比如如果我想在上面的ABC三个主节点再新添加一个节点D,那就需要从A,B,C中的部分槽分配到D上,如果想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态
9.3.4 槽位映射
slot 槽位映射,一般有3种解决方案
小厂:哈希取余分区
2亿条记录就是2亿个k,v。单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:hash(key)%N个机器台数
,计算出哈希值,用来决定数据映射到哪一个节点上。
优点: 简单粗暴,直接有效,只需要预估好数据,规划好节点数,例如3台、8台、10台,就能保证一段时间的数据支撑。使用Hash算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡+分而治之的作用。
缺点: 原来规划好的节点,进行扩容或者缩容就比较麻烦了,不管扩容还是缩容,映射关系需要重新进行计算,再服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化,hash(key)/3
会变为 hash(key) / ?
。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。
某个 redis 机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌,每个数据映射到的redis服务器都可能发生变化。
中厂:一致性哈希算法分区
该算法设计目的是为了解决分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,取机器真实台数就不行了,数据分配到的redis服务器和它数据真实存在的redis服务器可能不一致。该算法提出是为了当服务器个数发生变动时,尽量减少影响客户端到服务器的映射关系。
🐯 算法构建出一致性哈希环
前面那个是对redis服务器的数量进行取模。而一致性hash算法是对 2 32 2^{32} 232 取模,也就是将整个哈希值空间组织成一个虚拟的圆环,而原来的取模取决于redis服务器个数,是变化的,这个就是固定不变的了。大致如下
🐯 redis服务器IP节点映射
将集群中各个IP节点映射到环上的某一个位置,即 redis 服务器的IP值
将各个服务器使用 hash 进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。假如4个节点NodeA、B、C、D,经过IP地址的哈希函数计算 hash(ip),使用IP地址哈希后在环空间的位置如下,注意四个节点不太可能把环均分
🐯 key 落到服务器的落键规则
当需要存储一个k,v键值对时,首先计算key的hash值,hash(key),将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,从此位置沿环顺时针行走,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
如有Object A,Object B,Object C,Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性Hash算法,数据A会被定位到Node A上,B被定位到Node B上,C被定位到Node C上,D被定位到Node D上。
🐯 优点
容错性
假如Node C宕机,首先A,B,D不会受到影响。一般的,在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿逆时针方向行走遇到的第一台服务器,B-C)之间的数据,其它不会收到影响。也就是说,即使C宕机,收到影响的只是B,C之间的数据且这些数据会转移到D进行存储。
除B,C之间其他的数据还是原来的映射关系。
扩展性
数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那受到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致Hash取余全部数据重新洗牌
在节点数目发生改变时尽可能少的迁移数据
🐯 缺点
Hash环的数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。例如系统中只有两台服务器
大厂:哈希槽分区算法
哈希槽实质就是一个数组,数组 [0, 2^14 - 1]
形成 hash slot 空
❓ 能干什么
解决前面所提到的数据倾斜问题,让数据均匀分配。在数据和节点之间加入一层,这层成为哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里面放的是数据
槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用key的哈希值来计算所在的槽,便于数据分配。
❓ 多少个hash槽
一个集群只能有16384个槽,编号0-16383。这些槽会分配给集群中的所有主节点,分配策略没有要求。集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对key求哈希值,然后对16384取模,余数是多少就落入对应的槽中。就是下面这个哈。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
redis 集群中内置了16384个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要redis集群中放置一个key-value时,redis先对key使用CRC16算法算出一个结果然后用结果对16384求余数 CRC16(key)%16384
,这样每个key都会对应一个编号在 0-16384 之间的哈希槽,也就是映射到某个节点上。
❓ 为什么redis集群的最大槽数是16384个
再去看视频或者找吧,没看懂
redis 集群不保证强一致性,这意味着在特定的条件下,redis 集群可能会丢掉一些被系统收到的写入请求命令。
就比如这里,用户写入的数据存入了M1,但是还没来得及把数据同步给从机就宕机了,用户确实进行了写操作,但是读取不到了。
9.4 实例
9.4.1 环境搭建
如下,要搭建 3主3从 redis集群
找3台真实虚拟机,各自新建文件夹
mkdir -p /home/liu/redis-7.4.0/cluster
这里由于只能开3台虚拟机,内存不够用,所以每一台虚拟机实现一个一主一从
新建6个独立的redis实例服务
然后在 cluster 文件夹下新建两个文件并填入内容
nano redisCluster6381.conf
nano redisCluster6382.conf # 记得修改配置文件中的端口号,要对应
bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/home/liu/redis-7.4.0/cluster/cluster6381.log"
pidfile "/home/liu/redis-7.4.0/cluster/cluster6381.pid"
dir /home/liu/redis-7.4.0/cluster
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendonly6381.aof"
requirepass redis
masterauth redis
cluster-enabled yes
cluster-config-file nodes-6381.conf
cluster-node-timeout 5000
其他两台机器也如此,按照整体架构,注意端口号
然后通过 redis-server 命令启动,每台的两个都要启动
redis-server /home/liu/redis-7.4.0/cluster/redisCluster6381.conf
redis-server /home/liu/redis-7.4.0/cluster/redisCluster6382.conf
启动后我们可以看到,它们后面是有一个 cluster
集群的标志的
注意,其他两台也启动
通过redis-cli 命令为6台机器构建集群关系
redis-cli -a redis --cluster create --cluster-replicas 1 192.168.153.129:6381 192.168.153.129:6382 192.168.153.130:6383 192.168.153.130:6384 192.168.153.131:6385 192.168.153.131:6386
--cluster-replicas 1
表示为每个 master 创建一个slave节点
执行可以看到确实弄了个3主3从,但是它们的主从关系是自动分配的,和我们写的顺序无关,所以和我们的整体架构图是不一样的喔。靓仔
Master[0],Master[1],Master[2]槽分配也和预想的一样
确认执行,则配置完成
而 cluster
文件夹下也多了很多文件(在使用redis-server开启服务就有了)
我们使用 redis-cli 启动其中一个端口,注意指定端口号,不然默认就是6379了
可以看到6385作为主机,6382作为它的一个从机
我们还可以通过命令 CLUSTER NODES
查看集群节点信息
里面还说了每个主机对应的槽位号
也可以通过 CLUSTER info
查看当前节点的信息
9.4.2 测试
我们首先在端口号为6385的主机上进行写
发现写入 k2 的时候报错了,说移到了6381,那我们打开6381看一下
也没有存进去,数据就丢失了
这是因为每个主机负责的槽位是固定的,我们在存或者读取键的时候,必须要路由到位,即某个主机只能负责固定槽位的 读写。
那该怎么做?我们首先把所有的数据库内容清空,打开客户端时用如下命令
redis-cli -a redis -p 6385 -c # 多加一个 -c
然后我们再 set k2 v2 的时候它是重定向到了6381,让6381去执行,读的时候也是。这样在6385机器上也能读到6381槽位上的。请忽略中间的 get k2 v2
,写错了,,,
也能正确读取6385节点上操作的数据。为什么?注意一下前面的端口号,已经从
127.0.0.1:6381 变为了 192.168.153.131:6385
可以使用命令 CLUSTER KEYSLOT key
来计算键对应的槽值
OK,读写没问题了
9.4.3 主从容错切换
我们来测试一下宕机了会怎么样,首先清空数据,查看一下集群节点信息
6381主-6384从 [0-5460]
6383主-6386从 [5461-10922]
6385主-6382从 [10923-16383]
键 k1
对应的槽是12706,在 6383 节点上。我们在6385节点上存一下数据,然后关机
在其他节点上查看一下数据试试
没什么问题,我们再来看一下主从关系
6381主-6384从 [0-5460]
6383主-6386从 [5461-10922]
6382主 [10923-16383]
6385 宕机后,本来作为6385的从机的6382变为了主机。
我们再让6385回归,看一下结果
不行了,6385变为了6382的从机。如果你想恢复原来的主从关系,使用如下命令
CLUSTER FAILOVER
可以看到6385又变回为了主机,而6382为它的从机。
9.4.4 集群扩容
向集群中新增一个主机和从机,新创建两个配置文件,并使用 redis-server 启动
注意哦,此时6387和6388都没有加入集群呢,原来是通过 redis-cli -a redis --cluster create --cluster-replicas
命令来创建的集群。它俩还未加入
将新增的6387作为master节点加入原有的集群
redis-cli -a redis --cluster add-node 192.168.153.129:6387 192.168.153.129:6381
这里的IP地址根据所在IP设定。这里第一个是要新加入的节点,第二个是作为一个引路人的作用,它所在的集群就是要加入的集群。
添加成功。使用如下命令检查集群情况
redis-cli -a redis --cluster check 192.168.153.129:6381
可以看到虽然加进来了,但是没有任何作用,还未分配插槽,也没存储数据。
🐯 进行槽位的重新洗牌,让6387也干活
redis-cli -a redis --cluster reshard 192.168.153.129:6381
它问分配给6387多少个,16384/4 = 4096。就取这个吧,给谁呢?给6387,把它的ID取一下粘贴。由于要均分,所以我们选择 all。然后选择 yes 进行分派
完成后,查看集群信息
都是 4096 个。(被分配的槽位的数据也会移到新的master上)
这个时候新的 master 它的槽位区间不是连续的了。因为重新分配成本太高,在尽量不移动原来那些分配的槽位的基础上,选择一部分给新的 master,分配成本稍微低一些。
为主节点6387分配从节点6388
redis-cli -a redis --cluster add-node 192.168.153.129:6388 192.168.153.129:6387 --cluster-slave --cluster-master-id 345e43e90a6961cd7a9bf0ae3bb9e14af2ad160a # --cluster-master-id 是6387的ID,这里实测不需要这个,但是尚硅谷给的有,实际上不需要
加入成功,查看节点信息
已成功加入,且主从关系没问题。
9.4.5 集群缩容
将扩容的4主4从恢复到原来的3主3从,整体流程
- 清除节点 6388
- 清出来的槽号重新分配给6383
- 删除6387
- 恢复3主3从
🐯 清除节点 6388
首先使用命令查看 6388 的ID
redis-cli -a redis --cluster check 192.168.153.129:6388
然后使用如下命令进行移除节点
redis-cli -a redis --cluster del-node 192.168.153.129:6388 601b737434941cef91cb84574d576438cbd7a65b
删除的节点IP:端口号 + 节点ID
OK,成功删除,检查一下,确实没啦
🐯 清出来的槽号重新分配给6383
这里就是简单演示一下,因为生产上一般扩容后不会去缩容了。
然后我们查看一下结果
槽位变更完成,原来的6387变为了从节点
然后,删除6387,和第一步一样,就不演示了。
9.4.6 其他说明
不在同一个redis节点下的键值无法使用 mset、mget 等多键操作
(error) CROSSSLOT Keys in request don't hash to the same slot
。不同键位于不同的redis节点上,没办法从一个节点上进行读取,只从6382上读不到,也无法进行重定向,因为这三个键在多个redis节点上,没办法只重定向到一个上。
192.168.153.129:6382> mget k1 k2 k3
可以通过 {}
来定义一个组的概念,使 key 中 {}
相同内容的键值对放到一个redis节点上,对照下图类似 k1,k2,k3 都映射为x。
192.168.153.129:6385> mset k1{x} v1 k2{x} v2 k3{x} v3
OK
192.168.153.129:6385> mget k1{x} k2{x} k3{x}
1) "v1"
2) "v2"
3) "v3"
这里需要使用 {}
,CRC16算法计算哈希值时会去判断有没有 {}
的。
cluster-require-full-coverage
:集群是否完整才能对外提供服务
默认为 yes。现在集群架构是3主3从,每个master的小集群负责三分之一的slot,对应一部分服务。如果3个中,任意一个(一主一从都挂了,只挂一个主或一个从还没事)挂了,那么只能对外提供三分之二的数据了,整个集群是不完整的,默认是对外不提供。
集群并不是数据共享,每个人都备一份,而是每个人三分之一(假如有三个),当key不是我该负责的时候,会重定向到其他上面去写或读。每个人其实都只有三分之一,主机挂掉后,从机上位,从机也挂就没啦。
查看某个槽位是否被占用
CLUSTER COUNTKEYSINSLOT 槽位数字编号
10. 集成
Jedis -> lettuce -> RedisTemplate 是出现的顺序,现在着重学习 RedisTemplate
10.1 Jedis
Jedis Client 是 Redis 官网推荐的一个面向Java客户端,库文件实现了对各类API进行封装调用
10.1.1 集成
POM
文件
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
打开 linux redis客户端,使用一个单机的,6379端口,里面有两个数据。
连接并测试
import redis.clients.jedis.Jedis;
public class JedisDemo {
public static void main(String[] args) {
// 1. 指定IP和端口号
Jedis jedis = new Jedis("192.168.153.129", 6379);
// 2. 指定访问服务器的密码
jedis.auth("redis");
Set<String> keys = jedis.keys("*");
System.out.println(keys); // [k1, k2]
jedis.set("k3", "hello-jedis");
System.out.println(jedis.get("k3")); // hello-jedis
jedis.lpush("list", "11", "12", "13");
List<String> list = jedis.lrange("list", 0, -1);
list.forEach(System.out::println); // 13 12 11
}
}
可以看到在连接成功后,能够访问到里面的数据
10.2 lettuce
原来的Jedis存在高并发的问题,所以lettuce进行了优化,SpringBoot工程2.0后默认使用Lettuce这个客户端连接Redis服务器。
Jedis客户端连接Redis服务器的时候,每个线程都要拿自己创建的Jedis实例去连接Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建和关闭一个Jedis连接,而且也是线程不安全的,一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程。但是如果使用Lettuce就不会出现上述问题,Lettuce底层使用的是Netty,当有多个线程都需要连接Redis服务器的时候,可以保证只创建了一个Lettuce连接,使所有的线程共享这一个Lettuce连接,这样可以减少创建关闭一个Lettuce连接时候的开销。而且这种方式也是线程安全的,不会出现一个线程通过Lettuce更改Redis服务器中的数据之后而影响另一个线程的情况。
10.2.1 集成
POM
文件
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.3.2.RELEASE</version>
</dependency>
打开 linux redis客户端,使用一个单机的,6379端口
测试
package com.lh.jredis.demo;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import java.util.List;
public class LettuceDemo {
public static void main(String[] args) {
// 1 使用构建器链式编程来builder redisURI
RedisURI uri = RedisURI.builder()
.redis("192.168.153.129")
.withPort(6379)
.withAuthentication("default", "redis")
.build();
// 2 创建连接客户端
RedisClient redisClient = RedisClient.create(uri);
StatefulRedisConnection<String, String> conn = redisClient.connect();
// 3 通过 conn 创建操作的 command
RedisCommands<String, String> commands = conn.sync();
List<String> keys = commands.keys("*");
System.out.println(keys); // [k3, k1, list, k2]
commands.set("k4", "hello-lettuce");
System.out.println(commands.get("k4")); // hello-lettuce
// 4 关闭释放资源
conn.close();
redisClient.shutdown();
}
}
10.3 RedisTemplate
10.3.1 连接单机
SpringBoot 与 Redis 整合依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
可以看到 spring-boot-starter-data-redis
本身就整合了 lettuce
Swagger2,这是一个文档注释用的,此处不会用,后面再学
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
编写 yaml 配置文件
spring:
data:
redis:
database: 0 # redis 16个库选择哪一个
host: 192.168.153.129
port: 6379
password: redis
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
编写 RedisConfig
,这个是因为Java程序和Redis的序列化规则不一样。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 设置 key 的序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
编写 OrderService
@Service
@Slf4j
public class OrderService {
@Resource
private RedisTemplate redisTemplate;
public static final String ORDER_KEY = "ord:"; // 一般都加一个前缀
public void addOrder(){
int keyId = ThreadLocalRandom.current().nextInt(1000) + 1;
String serialNo = UUID.randomUUID().toString(); // 键和值随机产生
String key = ORDER_KEY + keyId;
String value = "京东订单" + serialNo;
redisTemplate.opsForValue().set(key, value); // opsFor** 代表对哪种类型的操作
log.info("***key:{}", key);
log.info("***value:{}", value);
}
public String getOrderById(Integer keyId){
return (String) redisTemplate.opsForValue().get(ORDER_KEY + keyId);
}
}
编写 Controller
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/order/add")
public void addOrder(){
orderService.addOrder();
}
@GetMapping("/order/{keyId}")
public void getOrderById(@PathVariable("keyId") Integer keyId){
orderService.getOrderById(keyId);
}
}
使用Postman 发送post请求,成功添加后,去Redis数据库查询
--raw
重新启动客户端,让客户端支持中文
我们再使用 Postman 发送 get 请求,也没什么问题
10.3.2 连接集群
只需要修改 yaml 文件即可
spring:
data:
redis:
password: redis
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
# 用来解决redis服务器宕机后,Java客户端不能动态感知到集群节点的变化
cluster:
refresh:
adaptive: true
period: 2000
cluster:
max-redirects: 3
nodes: 192.168.153.129:6381, 192.168.153.129:6382, 192.168.153.130:6383, 192.168.153.130:6384, 192.168.153.131:6385, 192.168.153.131:6386 # 端口号
由于内存太小,没法测试。
这里的 spring.data.redis.lettuce.cluster.refresh.**
配置是为了解决如下问题:
集群中如果某个主机宕机,比如6381宕机,那么在redis客户端上我们去操作是没有问题的,6381的从机会上位变为主机。但是在Java端,比如写入一个数据,它恰好要写到6381中,但是6381宕机了,按理来说Java应该会向6381的从机发送,但是实际上并没有,因为 lettuce 默认不更新节点拓扑图,它并没有感知到集群节点的变化,所以需要我们改写配置。
解决问题
1. 有线图标丢失
使用如下三个命令即可