Redis
1 Linux 上安装 Redis
1.1 什么是 Redis
Redis 是一个使用 ANSIC 编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。从2015年6月开始,Redis 的开发由 Redis Labs 赞助,而2013年5月至2015年6月期间,其开发由 Pivotal 赞助。在2013年5月之前,其开发由 VMware 赞助。根据月度排行网站 DB-Engines 的数据显示,Redis 是最流行的键值对存储数据库。
Redis 具有如下特点:
- Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,不会造成数据丢失。
-
Redis 支持五种不同的数据结构类型之间的映射,包括简单的 key/value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储。
- Redis 支持 master-slave 模式的数据备份。
Redis 具有如下功能:
-
内存存储和持久化:redis 支持异步将内存中的数据写到硬盘上,在持久化的同时不影响继续服务。
-
取最新 N 个数据的操作,如:可以将最新的 10 条评论的 ID 放在 Redis 的 List 集合里面。
- 数据可以设置过期时间。
- 自带发布、订阅消息系统。
- 定时器、计数器。
1.2 Redis 安装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 安装 ubuntu 24.04
multipass launch 24.04 --name redis1 --cpus 2 --memory 4G --disk 8G
# 登录虚拟机
multipass shell redis1
# 设置密码
sudo passwd
# 切换用户
su root
# 切换目录
cd ~
# 修改 ssh 配置
vi /etc/ssh/sshd_config
PermitRootLogin yes # 允许 root 用户登录
PasswordAuthentication yes # 允许使用密码登录
# 重启 ssh 服务
service ssh restart
# 本地生成密钥对
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "15235032479@163.com"
# 将公钥复制到虚拟机中,配置免密登录
scp ~/.ssh/id_ed25519.pub root@192.168.64.29:~/.ssh/
cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys
# 本地简化登录命令
vi ~/.zshrc
alias redis1='ssh root@192.168.64.29'
source ~/.zshrc
1
2
3
4
5
6
7
8
cd /usr/local/redis
apt update
apt install build-essential tcl
wget http://download.redis.io/releases/redis-6.2.9.tar.gz
tar -zxvf redis-6.2.9.tar.gz
cd redis-6.2.9
make
make install
在启动之前,修改配置文件 redis.conf
:
1
2
# 服务在后台启动
daemonize yes
1
2
3
4
5
./src/redis-server ./redis.conf
./src/redis-cli
ping
shutdown
exit
2 Redis 中的五种数据类型简介
2.1 五大数据类型介绍
Redis 中的数据都是以 key/value 的形式存储的,五大数据类型主要是指 value 的数据类型,包含如下五种:
STRING
STRING 是 Redis 中最基本的数据类型,Redis 中的 STRING 类型是二进制安全的,即它可以包含任何数据,比如一个序列化的对象甚至一个 jpg 图片,要注意的是 Redis 中的字符串大小上限是512M。
LIST
LIST 是一个简单的字符串列表,按照插入顺序进行排序,我们可以从 LIST 的头部(LEFT)或者尾部(RIGHT)插入一个元素,也可以从 LIST 的头部(LEFT)或者尾部(RIGHT) 弹出一个元素。
SET
SET 是 STRING 类型的无序集合,不同于 LIST,SET 中的元素不可以重复。
HASH
HASH 类似于 Java 中的 Map,是一个键值对集合,在 Redis 中可以用来存储对象。
ZSET
ZSET 和 SET 一样,也是 STRING 类型元素的集合,不同的是 ZSET 中的每个元素都会关联一个 double 类型的分数,ZSET 中的成员都是唯一的,但是所关联的分数可以重复。
2.2 相关命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 设置 key/value
set k1 v1
# 删除
del k1
# 序列化
dump k1
# 1:存在;0:不存在
exists k1
# -2:不存在或已过期;-1:存在但没有设置过期时间
ttl k1
# 设置有效期 30 秒
expire k1 30
# 移除过期时间
persist k1
# 改变有效期为 60 秒
persist k1 60
# pexpire 和 expire 功能一致,不同的是 pexpire 的单位是毫秒
pexpire k1 6000
# pttl 和 ttl 功能一致,不同的是 pttl 的单位是毫秒
pttl k1
# 根据模式获取所有的 key,* 代表所有,这里也可以写正则表达式
keys *
3 字符串 STRING
APPEND
使用 append 命令时,如果 key 已经存在,则直接在 value 后追加值,如果 key 不存在,则会先创建一个 value 为空字符串的 key,然后再追加:
1
2
3
4
5
6
7
8
127.0.0.1:6379> append k1 hello
(integer) 5
127.0.0.1:6379> get k1
"hello"
127.0.0.1:6379> append k1 world
(integer) 10
127.0.0.1:6379> get k1
"helloworld"
INCR
incr 操作可以对 value 执行加 1 操作,如果指定的 key 不存在,那么在加 1 操作之前,会先将 value 设置为 0,如果 value 不是数字,则会报错。如下:
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> set k1 9
OK
127.0.0.1:6379> incr k1
(integer) 10
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> set k1 z
OK
127.0.0.1:6379> incr k1
(error) ERR value is not an integer or out of range
INCRBY
incrby 和 incr 类似,不同的是 incrby 可以指定步长,如下:
1
2
3
4
127.0.0.1:6379> set k1 9
OK
127.0.0.1:6379> incrby k1 6
(integer) 15
INCRBYFLOAT
incrbyfloat 命令可以用来增长浮点数,如下:
1
2
3
4
5
6
127.0.0.1:6379> set k1 9.9
OK
127.0.0.1:6379> incrbyfloat k1 0.1
"10"
127.0.0.1:6379> incrbyfloat k1 -0.1
"9.9"
DECR
decr 命令可以实现对 value 的减 1 操作,如果 key 不存在,则 key 对应的初始值会被置 0,如果 value 不是数字,则会报错,如下:
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set k1 9
OK
127.0.0.1:6379> decr k1
(integer) 8
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> set k1 z
OK
127.0.0.1:6379> decr k1
(error) ERR value is not an integer or out of range
DECRBY
decrby 和 decr 类似,不同的是 decrby 可以指定步长,如下:
1
2
3
4
127.0.0.1:6379> set k1 9
OK
127.0.0.1:6379> decrby k1 6
(integer) 3
GET
get 命令是用来获取 value 的,如果 key 不存在则返回 nil,如下:
1
2
3
4
5
6
127.0.0.1:6379> get k1
"3"
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> get k1
(nil)
GETRANGE
getrange 用来返回 value 的子串,子串由 start 和 end 决定,从左往右计算,如果下标是负数,则从右往左计算,其中 -1 表示最后一个字符,-2 是倒数第二个,以此类推,如下:
1
2
3
4
5
6
127.0.0.1:6379> set k1 yueyazhui
OK
127.0.0.1:6379> getrange k1 0 2
"yue"
127.0.0.1:6379> getrange k1 -4 -1
"zhui"
GETSET
getset 命令可以用来获取 value,并对 key 进行重置,如下:
1
2
3
4
5
6
127.0.0.1:6379> set k1 yueyazhui
OK
127.0.0.1:6379> getset k1 yue
"yueyazhui"
127.0.0.1:6379> get k1
"yue"
MGET 与 MSET
mget 与 mset 分别用来批量设置值和批量获取值,如下:
1
2
3
4
5
6
127.0.0.1:6379> mset k1 yue k2 ya k3 zhui
OK
127.0.0.1:6379> mget k1 k2 k3
1) "yue"
2) "ya"
3) "zhui"
SETEX
setex 用来给 key 设置 value,同时设置过期时间,相当于先给 key 设置 value,再给 key 设置过期时间,如下:
1
2
3
4
5
6
127.0.0.1:6379> setex k1 30 yueyazhui
OK
127.0.0.1:6379> get k1
"yueyazhui"
127.0.0.1:6379> ttl k1
(integer) 15
PSETEX
psetex 的作用和 setex 类似,不同的是,setex 设置过期时间的单位是毫秒,如下:
1
2
3
4
5
6
127.0.0.1:6379> psetex k1 30000 yueyazhui
OK
127.0.0.1:6379> get k1
"yueyazhui"
127.0.0.1:6379> pttl k1
(integer) 15000
SETNX
setnx 是 SET if Not eXists 的简写,set 命令在执行时,如果 key 已经存在,则新值会覆盖掉旧值,而对于 setnx 命令,如果 key 已经存在,则不做任何操作,如果 key 不存在,则效果等同于 set 命令。如下:
1
2
3
4
5
6
127.0.0.1:6379> setnx k1 yueyazhui
(integer) 1
127.0.0.1:6379> setnx k1 yue
(integer) 0
127.0.0.1:6379> get k1
"yueyazhui"
MSETNX
msetnx 兼具了 setnx 和 mset 的特性,但是 msetnx 在执行时,如果有一个 key 存在,则所有的都不会执行,如下:
1
2
3
4
127.0.0.1:6379> get k1
"yueyazhui"
127.0.0.1:6379> msetnx k1 yue k2 ya k3 zhui
(integer) 0
因为 k1 已经存在,所以 k2、k3 没执行成功。
SETRANGE
setrange 用来覆盖 value,如下:
1
2
3
4
5
6
127.0.0.1:6379> set k1 yueyazhui
OK
127.0.0.1:6379> setrange k1 3 99
(integer) 9
127.0.0.1:6379> get k1
"yue99zhui"
但如果 value 长度小于 offset,则不足的地方用空格补齐,如下:
1
2
3
4
5
6
127.0.0.1:6379> set k1 yueyazhui
OK
127.0.0.1:6379> setrange k1 10 99
(integer) 12
127.0.0.1:6379> get k1
"yueyazhui\x0099"
STRLEN
strlen 用来计算 value 的长度,如下:
1
2
3
4
127.0.0.1:6379> set k1 yueyazhui
OK
127.0.0.1:6379> strlen k1
(integer) 9
4 字符串 STRING 中的 BIT 相关命令
在学习 BIT 命令之前,得先了解下 Redis 中字符串的存储方式,Redis 中的字符串都是以二进制的方式进行存储的,比如执行如下命令:
1
2
127.0.0.1:6379> set k1 a
OK
a 对应的 ASCII 码是 97,转换二进制数据是 01100001,BIT 相关命令都是对这个二进制数据进行操作。
GETBIT
getbit 命令可以返回 key 对应的 value 在 offset 处的 bit 值,以上文提到的 k1 为例,a 对应的二进制数据是 01100001,所以当 offset 为 0 时,对应的 bit 值为 0;offset 为 1 时,对应的 bit 值为 1;offset为 2 时,对应的 bit 值为 1;offset 为 3 时,对应的 bit 值为 0;依此类推….,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> getbit k1 0
(integer) 0
127.0.0.1:6379> getbit k1 1
(integer) 1
127.0.0.1:6379> getbit k1 2
(integer) 1
127.0.0.1:6379> getbit k1 3
(integer) 0
127.0.0.1:6379> getbit k1 4
(integer) 0
127.0.0.1:6379> getbit k1 5
(integer) 0
127.0.0.1:6379> getbit k1 6
(integer) 0
127.0.0.1:6379> getbit k1 7
(integer) 1
SETBIT
setbit 可以用来修改二进制数据,比如 a 对应的 ASCII 码为 97,c 对应的 ASCII 码为 99,97 转为二进制是 01100001,99 转为二进制是 01100011,两个的差异在于第六位一个是 0 一个是 1,通过 SETBIT 命令,可以将 k1 的第六位的 0 改为 1(第六位是从 0 开始算),如下:
1
2
3
4
127.0.0.1:6379> setbit k1 6 1
(integer) 0
127.0.0.1:6379> get k1
"c"
此时,k1 中存储的字符也就变为了 c。setbit 在执行时所返回的数字,表示该位上原本的 bit 值。
BITCOUNT
bitcount 可以用来统计这个二进制数据中 1 的个数,如下:
1
2
127.0.0.1:6379> bitcount k1
(integer) 4
关于 bitcount,Redis 官网上有一个非常有意思的案例:用户上线天数统计。节选部分原文如下:
举个例子,如果今天是网站上线的第 100 天,而用户 yueyazhui 在今天浏览过网站,那么执行命令 setbit yueyazhui 100 1;如果明天 yueyazhui 也继续浏览网站,那么执行命令 setbit yueyazhui 101 1,以此类推。当要统计 yueyazhui 总共以来的上线天数时,就使用 bitcount 命令,执行 bitcount yueyazhui,得出的结果就是 yueyazhui 上线的总天数。
这种统计方式最大的好处就是节省空间并且运算速度快。每天占用一个 bit,一年也就 365 个 bit,10 年也就 10*365 个 bit,也就是 456 个字节,对于这么大的数据,bit 的操作速度非常快。
BITOP
bitop 可以对一个或者多个二进制位串执行并(AND)、或(OR)、异或(XOR)以及非(NOT)运算,如下:a 对应的 ASCII 码转二进制是 01100001,c 对应的 ASCII 码转二进制是 01100011。对这两个二进制位串分别执行 AND 的结果如下:
与(AND)运算:只有当两个操作数都为1时,结果才为1,否则结果为0。
或(OR)运算:当两个操作数中至少有一个为1时,结果就为1,否则结果为0。
异或(XOR)运算:当两个操作数不同时,结果为1;相同时,结果为0。
非(NOT)运算:用于翻转操作数的状态。如果操作数为1,则结果为0;如果操作数为0,则结果为1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
a:01100001
c:01100011
and:01100001
or:01100011
xor:00000010
not:10011110
127.0.0.1:6379> set k1 a
OK
127.0.0.1:6379> set k2 c
OK
127.0.0.1:6379> bitop and k3 k1 k2
(integer) 1
127.0.0.1:6379> get k3
"a"
127.0.0.1:6379> bitop or k3 k1 k2
(integer) 1
127.0.0.1:6379> get k3
"c"
127.0.0.1:6379> bitop xor k3 k1 k2
(integer) 1
127.0.0.1:6379> get k3
"\x02"
127.0.0.1:6379> bitop not k4 k1
(integer) 1
127.0.0.1:6379> get k4
"\x9e"
BITPOS
bitpos 用来获取二进制位串中第一个 1 或者 0 的位置,如下:
1
2
3
4
5
6
7
8
a:01100001
127.0.0.1:6379> set k1 a
OK
127.0.0.1:6379> bitpos k1 1
(integer) 1
127.0.0.1:6379> bitpos k1 0
(integer) 0
也可以在后面设置一个范围,不过后面的范围是字节的范围,而不是二进制位串的范围。
5 Redis 列表与集合
5.1 列表
LPUSH
将一个或多个 value 值插入到列表 key 的表头,如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头,如下:
1
2
127.0.0.1:6379> lpush k1 v1 v2 v3
(integer) 3
LRANGE
返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定,下标(index)参数 start 和 stop 都以 0 为底,即 0 表示列表的第一个元素,1 表示列表的第二个元素,以此类推。也可以使用负数下标,以 -1 表示列表的最后一个元素,-2表示列表的倒数第二个元素,以此类推。如下:
1
2
3
4
127.0.0.1:6379> lrange k1 0 -1
1) "v3"
2) "v2"
3) "v1"
RPUSH
rpush 与 lpush 的功能基本一致,不同的是 rpush 的中的 value 值是按照从右到左的顺序依次插入,如下:
1
2
3
4
5
6
127.0.0.1:6379> rpush k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> lrange k1 0 -1
1) "v1"
2) "v2"
3) "v3"
RPOP
rpop 命令可以移除并返回列表 key 的尾元素。如下:
1
2
3
4
5
127.0.0.1:6379> rpop k1
"v3"
127.0.0.1:6379> lrange k1 0 -1
1) "v1"
2) "v2"
LPOP
lpop 和 rpop 类似,不同的是 lpop 移除并返回列表 key 的头元素,如下:
1
2
3
4
127.0.0.1:6379> lpop k1
"v1"
127.0.0.1:6379> lrange k1 0 -1
1) "v2"
LINDEX
lindex 命令可以返回列表 key 中,下标为 index 的元素,正数下标 0 表示第一个元素,也可以使用负数下标,-1 表示倒数第一个元素,如下:
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> rpush k1 v1 v2 v3
(integer) 3
127.0.0.1:6379> lrange k1 0 -1
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> lindex k1 0
"v1"
127.0.0.1:6379> lindex k1 -1
"v3"
LTRIM
ltrim 命令可以对一个列表进行修剪,即让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。下标与之前介绍的写法一致。如下:
1
2
3
4
5
6
7
8
127.0.0.1:6379> lrange k1 0 -1
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> ltrim k1 1 1
OK
127.0.0.1:6379> lrange k1 0 -1
1) "v2"
BLPOP
blpop 是阻塞式列表的弹出原语。它是命令 lpop 的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 blpop 命令阻塞。当给定多个 key 参数时,按参数 key 的先后顺序依次检查各个列表,弹出第一个非空列表的头元素。同时,在使用该命令时也需要指定阻塞的时长,时长单位为秒,在该时长内如果没有元素可供弹出,则阻塞结束。返回的结果是 key 和 value 的组合,如下:
1
2
3
4
5
6
127.0.0.1:6379> blpop k1 10
1) "k1"
2) "v2"
127.0.0.1:6379> blpop k1 10
(nil)
(10.06s)
量后,BRPOP、BPOPLPUSH、BRPOPLPUSH 都是相应命令的阻塞版本,这里就不赘述了。
5.2 集合
SADD
sadd 命令可以添加一个或多个指定的 member 元素到集合的 key 中,指定的一个或者多个元素 member 如果已经在集合 key 中存在则忽略,如果集合 key 中不存在,则添加 member 元素到集合 key 中。如下:
1
2
127.0.0.1:6379> sadd k1 v1 v2 v3 v1
(integer) 3
SREM
srem 命令可以在 key 集合中移除指定的元素,如果指定的元素不是 key 集合中的元素则忽略并返回 0。如果 key 集合不存在则被视一个空的集合,也返回 0。如下:
1
2
3
4
5
6
7
8
127.0.0.1:6379> srem k1 v3
(integer) 1
127.0.0.1:6379> srem k1 v3
(integer) 0
127.0.0.1:6379> keys *
1) "k1"
127.0.0.1:6379> srem k2 v3
(integer) 0
SISMEMBER
sismember 命令可以返回成员 member 是否是集合 key 的成员。如下:
1
2
3
4
127.0.0.1:6379> sismember k1 v3
(integer) 0
127.0.0.1:6379> sismember k1 v2
(integer) 1
SCARD
scard 命令可以返回集合元素的数量,如下:
1
2
127.0.0.1:6379> scard k1
(integer) 2
SMEMBERS
smembers 命令可以返回 key 集合所有的元素,如下:
1
2
3
127.0.0.1:6379> smembers k1
1) "v2"
2) "v1"
SRANDMEMBER
srandmember 仅需提供 key 参数,它就会随机返回 key 集合中的一个元素,从 Redis 2.6 开始,该命令也可以接受一个可选的 count 参数,如果 count 是整数且小于元素的个数,则返回 count 个随机元素,如果 count 是整数且大于集合中元素的个数时,则返回集合中的所有元素,当 count 是负数,则会返回一个 count 绝对值的个数元素的数组,如果 count 的绝对值大于元素的个数,则返回的结果集里会出现一个元素出现多次的情况。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> srandmember k1
"v2"
127.0.0.1:6379> srandmember k1 2
1) "v2"
2) "v1"
127.0.0.1:6379> srandmember k1 3
1) "v2"
2) "v1"
127.0.0.1:6379> srandmember k1 -2
1) "v2"
2) "v1"
127.0.0.1:6379> srandmember k1 -3
1) "v2"
2) "v1"
3) "v1"
SPOP
spop 命令的用法和 srandmember 类似,不同的是,spop 每次选择一个随机的元素之后,该元素会出栈,而 srandmember 则不会出栈,只是将该元素展示出来。
SMOVE
smove 命令可以将 member 从 source 集合移动到 destination 集合中,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> del k1
(integer) 0
127.0.0.1:6379> del k2
(integer) 0
127.0.0.1:6379> sadd k1 v1
(integer) 1
127.0.0.1:6379> sadd k2 v2
(integer) 1
127.0.0.1:6379> smove k1 k2 v1
(integer) 1
127.0.0.1:6379> smembers k1
(empty array)
127.0.0.1:6379> smembers k2
1) "v2"
2) "v1"
SDIFF
sdiff 可以用来返回一个集合与给定集合的差集,如下:
1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> del k1
(integer) 0
127.0.0.1:6379> del k2
(integer) 0
127.0.0.1:6379> sadd k1 v1
(integer) 1
127.0.0.1:6379> sadd k2 v2
(integer) 1
127.0.0.1:6379> sdiff k1 k2
1) "v1"
127.0.0.1:6379> sdiff k2 k1
1) "v2"
SDIFFSTORE
sdiffstore 命令与 sdiff 命令基本一致,不同的是 sdiffstore 命令会将结果保存在一个集合中,如下:
1
2
3
4
127.0.0.1:6379> sdiffstore k3 k1 k2
(integer) 1
127.0.0.1:6379> smembers k3
1) "v1"
SINTER
sinter 命令可以用来计算指定 key 之间元素的交集,如下:
1
2
3
4
5
6
127.0.0.1:6379> sadd k1 v1 v2
(integer) 2
127.0.0.1:6379> sadd k2 v2 v3
(integer) 2
127.0.0.1:6379> sinter k1 k2
1) "v2"
SINTERSTORE
sinterstore 命令和 sinter 命令类似,不同的是它会将结果保存到一个新的集合中,如下:
1
2
3
4
127.0.0.1:6379> sinterstore k3 k1 k2
(integer) 1
127.0.0.1:6379> smembers k3
1) "v2"
SUNION
sunion 可以用来计算两个集合的并集,如下:
1
2
3
4
127.0.0.1:6379> sunion k1 k2
1) "v2"
2) "v3"
3) "v1"
SUNIONSTORE
sunionstore 和 sunion 命令类似,不同的是它会将结果保存到一个新的集合中,如下:
1
2
3
4
5
6
127.0.0.1:6379> sunionstore k3 k1 k2
(integer) 3
127.0.0.1:6379> smembers k3
1) "v2"
2) "v3"
3) "v1"
6 Redis 散列与有序集合
6.1 散列
很多时候,散列就像一个微缩版的 Redis,在本文中,你会看到许多散列命令似曾相识。
HSET
hset 命令可以用来设置 key 指定的哈希集中指定字段的值,如下:
1
2
127.0.0.1:6379> hset k1 f1 v1
(integer) 1
HGET
hget 命令可以用来返回 key 指定的哈希集中该字段所关联的值,如下:
1
2
127.0.0.1:6379> hget k1 f1
"v1"
HMSET
hmset 命令可以批量设置 key 指定的哈希集中指定字段的值,如下:
1
2
127.0.0.1:6379> hmset k1 f1 v1 f2 v2 f3 v3
OK
HMGET
hmget 可以批量返回 key 指定的哈希集中指定字段的值,如下:
1
2
3
4
127.0.0.1:6379> hmget k1 f1 f2 f3
1) "v1"
2) "v2"
3) "v3"
HDEL
hdel 命令可以从 key 指定的哈希集中移除指定的字段,在哈希集中不存在的字段将被忽略,如下:
1
2
3
4
5
6
127.0.0.1:6379> hdel k1 f3
(integer) 1
127.0.0.1:6379> hmget k1 f1 f2 f3
1) "v1"
2) "v2"
3) (nil)
HSETNX
hsetnx 命令只在 key 指定的哈希集中不存在指定的字段时,设置字段的值,如果字段已存在,该操作无效。如下:
1
2
3
4
5
6
7
8
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> hsetnx k1 f1 v1
(integer) 1
127.0.0.1:6379> hsetnx k1 f1 v2
(integer) 0
127.0.0.1:6379> hget k1 f1
"v1"
HVALS
hvals 命令可以返回 key 指定的哈希集中所有字段的值,如下:
1
2
3
4
5
6
127.0.0.1:6379> hmset k1 f1 v1 f2 v2 f3 v3
OK
127.0.0.1:6379> hvals k1
1) "v1"
2) "v2"
3) "v3"
HKEYS
hkeys 命令可以返回 key 指定的哈希集中所有字段的名字,如下:
1
2
3
4
127.0.0.1:6379> hkeys k1
1) "f1"
2) "f2"
3) "f3"
HGETALL
hgetall 命令可以返回 key 指定的哈希集中所有的字段和值。返回值中,每个字段名的下一个是它的值,所以返回值的长度是哈希集大小的两倍,如下:
1
2
3
4
5
6
7
127.0.0.1:6379> hgetall k1
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
HEXISTS
hexists 命令可以返回哈希集中里面字段是否存在,如不:
1
2
3
4
127.0.0.1:6379> hexists k1 f1
(integer) 1
127.0.0.1:6379> hexists k1 f4
(integer) 0
HINCRBY
hincrby 可以增加 key 指定的哈希集中指定字段的数值。如果 key 不存在,会创建一个新的哈希集并与 key 关联。如果字段不存在,则字段的值在该操作执行前被设置为 0,hincrby 支持的值的范围限定在 64 位有符号整数,如下:
1
2
3
4
5
6
127.0.0.1:6379> del k1
(integer) 1
127.0.0.1:6379> hincrby k1 f1 9
(integer) 9
127.0.0.1:6379> hincrby k1 f1 1
(integer) 10
HINCRBYFLOAT
hincrbyfloat 与 hincrby 用法基本一致,只不过这里允许 float 类型的数据,就不赘述了。
HLEN
hlen 返回 key 指定的哈希集包含的字段的数量,如下:
1
2
3
4
127.0.0.1:6379> hset k1 f1 v1 f2 v2 f3 v3
(integer) 3
127.0.0.1:6379> hlen k1
(integer) 3
HSTRLEN
hstrlen 可以返回哈希集中指定字段的值的字符串长度,如果哈希集或者字段不存在,返回 0,如下:
1
2
3
4
127.0.0.1:6379> hstrlen k1 f1
(integer) 2
127.0.0.1:6379> hstrlen k1 f4
(integer) 0
6.2 有序集合
有序集合类似 Set,但是每个字符串元素都关联到一个叫 score 浮动数值。里面的元素总是通过 score 进行着排序,因此它是可以检索的一系列元素。
ZADD
zadd 命令可以将所有指定成员添加到键为 key 的有序集合里面。添加时可以指定多个分数/成员(score/member)对。如果指定添加的成员己经是有序集合里面的成员,则会更新该成员的分数(scrore)并更新到正确的排序位置。如下:
1
2
127.0.0.1:6379> zadd k1 20 v1
(integer) 1
ZSCORE
zscore 命令可以返回有序集 key 中成员 member 的 score 值。如下:
1
2
127.0.0.1:6379> zscore k1 v1
"20"
ZRANGE
zrange 命令可以根据 index 返回 member,该命令在执行时加上 withscores 参数可以连同 score 一起返回:
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> zadd k1 10 v2
(integer) 1
127.0.0.1:6379> zrange k1 0 -1
1) "v2"
2) "v1"
127.0.0.1:6379> zrange k1 0 -1 withscores
1) "v2"
2) "10"
3) "v1"
4) "20"
ZREVRANGE
zrevrange 和 zrange 功能基本一致,不同的是 zrevrange 是反着来的,如下:
1
2
3
4
5
6
7
8
127.0.0.1:6379> zrevrange k1 0 -1
1) "v1"
2) "v2"
127.0.0.1:6379> zrevrange k1 0 -1 withscores
1) "v1"
2) "20"
3) "v2"
4) "10"
ZCARD
zcard 命令可以返回 key 的有序集元素个数。如下:
1
2
127.0.0.1:6379> zcard k1
(integer) 2
ZCOUNT
zcount 命令可以返回有序集 key 中 score 的值在 min 和 max 之间(默认包括 score 值等于 min 或 max)的成员。如下:
1
2
3
4
127.0.0.1:6379> zadd k1 30 v3
(integer) 1
127.0.0.1:6379> zcount k1 10 30
(integer) 3
如果在统计时,不需要包含 60 和 100,则添加一个 ( 即可,如下:
1
2
127.0.0.1:6379> zcount k1 (10 (30
(integer) 1
ZRANGEBYSCORE
zrangebyscore 命令可以按照 score 范围元素,加上 withscores 可以连 score 一起返回。如下:
1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> zrangebyscore k1 10 30
1) "v2"
2) "v1"
3) "v3"
127.0.0.1:6379> zrangebyscore k1 10 30 withscores
1) "v2"
2) "10"
3) "v1"
4) "20"
5) "v3"
6) "30"
ZRANK
zrank 命令可以返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。排名以 0 为底,即 score 值最小的成员排名 0。如下:
1
2
3
4
5
6
127.0.0.1:6379> zrank k1 v1
(integer) 1
127.0.0.1:6379> zrank k1 v2
(integer) 0
127.0.0.1:6379> zrank k1 v3
(integer) 2
ZREVRANK
zrevrank 和 zrank 命令功能基本一致,不同的是,zrevrank 中的排序是从大到小:
1
2
3
4
5
6
127.0.0.1:6379> zrevrank k1 v1
(integer) 1
127.0.0.1:6379> zrevrank k1 v2
(integer) 2
127.0.0.1:6379> zrevrank k1 v3
(integer) 0
ZINCRBY
zincrby 命令可以为有序集 key 的成员 member 的 score 值加上增量 increment。如果 key 中不存在 member,就在 key 中添加一个 member,score 是 increment(就好像它之前的 score 是 0.0)。如果 key 不存在,就创建一个只含有指定 member 成员的有序集合:
1
2
3
4
5
127.0.0.1:6379> zincrby k1 5 v1
"25"
127.0.0.1:6379> zrange k1 1 1 withscores
1) "v1"
2) "25"
ZINTERSTORE
zinterstore 命令可以计算给定的 numkeys 个有序集合的交集,并且把结果放到 dedestination 中。在给定要计算的 key 和其它参数之前,必须先给定 key 个数(numkeys)。该命令也可以在执行的过程中给原 score 乘以 weights 后再求和,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
127.0.0.1:6379> zadd k1 10 v1
(integer) 1
127.0.0.1:6379> zadd k1 20 v2
(integer) 1
127.0.0.1:6379> zadd k1 30 v3
(integer) 1
127.0.0.1:6379> zadd k2 20 v2
(integer) 1
127.0.0.1:6379> zadd k2 30 v3
(integer) 1
127.0.0.1:6379> zadd k2 40 v4
(integer) 1
127.0.0.1:6379> zinterstore k3 2 k1 k2
(integer) 2
127.0.0.1:6379> zrange k3 0 -1 withscores
1) "v2"
2) "40"
3) "v3"
4) "60"
127.0.0.1:6379> zinterstore k4 2 k1 k2 weights 1 2
(integer) 2
127.0.0.1:6379> zrange k4 0 -1 withscores
1) "v2"
2) "60"
3) "v3"
4) "90"
ZREM
zrem 命令可以从集合中弹出一个或多个元素,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> zrange k1 0 -1 withscores
1) "v1"
2) "10"
3) "v2"
4) "20"
5) "v3"
6) "30"
127.0.0.1:6379> zrem k1 v3
(integer) 1
127.0.0.1:6379> zrange k1 0 -1 withscores
1) "v1"
2) "10"
3) "v2"
4) "20"
ZLEXCOUNT
ZLEXCOUNT 是 Redis 中用于计算有序集合(sorted set)中指定字典区间内成员数量的命令。以下是关于 ZLEXCOUNT 命令的详细解释:
1
zlexcount key min max
key
:有序集合的键名。min
:字典区间的起始值(包含或不包含取决于使用的符号)。max
:字典区间的结束值(包含或不包含取决于使用的符号)。
符号说明:
[
:表示区间包含该值。(
:表示区间不包含该值。-
:表示有序集合中得分最小值的成员。+
:表示有序集合中得分最大值的成员。
返回值:
返回有序集合中成员名称在 min
和 max
之间的成员数量。
示例:
假设我们有一个有序集合 k1
,并且已经添加了一些成员:
1
2
127.0.0.1:6379> zadd k1 0 a 0 b 0 c 0 d 0 e 0 f 0 g
(integer) 7
- 计算整个有序集合的成员数量:
1
2
127.0.0.1:6379> zlexcount k1 - +
(integer) 7
- 计算区间
[b, f]
(包含 b 和 f)内的成员数量:
1
2
127.0.0.1:6379> zlexcount k1 [b [f
(integer) 5
注意事项:
- ZLEXCOUNT 是根据成员的字典序来计算的,而不是根据成员的分数。
- 如果 key 不存在,ZLEXCOUNT 返回 0。
- 在使用 ZLEXCOUNT 时,需要注意区间符号的使用,以确保计算的是正确的成员数量。
ZLEXCOUNT 命令在处理需要按字典序查询有序集合中成员数量的情况时非常有用,例如,在实现分页显示、范围查询等功能时。
ZRANGEBYLEX
zrangebylex 返回指定成员区间内的成员,按成员字典序正序排序。如下:
1
2
3
4
5
6
127.0.0.1:6379> zrangebylex k1 [b [f
1) "b"
2) "c"
3) "d"
4) "e"
5) "f"
注意:min 和 max 参数的写法和 zlexcount 一致。
7 Redis 发布订阅和事务
7.1 发布订阅
Redis 的发布订阅系统有点类似于我们生活中的电台,电台可以在某一个频率上发送广播,而我们可以接收任何一个频率的广播,Android 中的 broadcast 也和这类似。
订阅消息的方式如下:
1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> subscribe c1 c2 c3
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1
1) "subscribe"
2) "c2"
3) (integer) 2
1) "subscribe"
2) "c3"
3) (integer) 3
这个表示接收 c1,c2,c3 三个频道传来的消息,发送消息的方式如下:
1
2
127.0.0.1:6379> publish c1 "hello redis!"
(integer) 1
当 c1 这个频道上有消息发出时,此时在消息订阅控制台可以看到如下输出:
1
2
3
1) "message"
2) "c1"
3) "hello redis!"
在 redis 中,我们也可以使用模式匹配订阅,如下:
1
2
3
4
5
127.0.0.1:6379> psubscribe c*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "c*"
3) (integer) 1
此时可以接收到所有以 c 开头的频道发来的消息。
tips
Redis 中的发布订阅系统在某些场景下还是非常好用的,但也有一些问题需要注意:由于网络在传输过程中可能会遭遇断线等意外情况,断线后需要进行重连,然而这会导致断线期间的数据丢失。
7.2 事务
既然 Redis 是一种 NoSQL 数据库,那它当然也有事务的功能,不过这里的事务和我们关系型数据库中的事务有一点点差异。
Redis 中事务的用法非常简单,我们通过 MULTI 命令开启一个事务,如下:
1
2
127.0.0.1:6379> multi
OK
在 MULTI 命令执行之后,我们可以连续发送命令去执行,此时的命令不会被立马执行,而是放在一个队列中,如下:
1
2
3
4
5
6
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
当所有的命令都输入完成后,我们可以通过 EXEC 命令发起执行,也可以通过 DISCARD 命令清空队列,如下:
1
2
3
4
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) OK
7.3 事务中的异常情况
Redis 中事务的异常情况总的来说分为两类:
- 进入队列之前就能发现的错误,比如命令输错;
- 执行 EXEC 之后才能发现的错误,比如给一个非数字字符加 1;
那么对于这两种不同的异常,Redis 中有不同的处理策略。对于第一种错误,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务(这个是 2.6.5 之后的版本做法,之前版本的做法可以参考官方文档)。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> del k1 k2 k3
(integer) 3
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2 2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR syntax error
3) OK
127.0.0.1:6379> keys *
1) "k3"
2) "k1"
而对于第二种情况,Redis 并没有对它们进行特别处理,即使事务中有某个/某些命令在执行时产生了错误,事务中的其他命令仍然会继续执行。如下:
1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> incr k1
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (error) ERR value is not an integer or out of range
127.0.0.1:6379> get k1
"v1"
不同于关系型数据库,Redis 中的事务出错时没有回滚,对此,官方的解释如下:Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
7.4 WATCH 命令
事务中的 WATCH 命令可以用来监控一个或多个 key,通过这种监控,我们可以为 Redis 事务提供 CAS(Compare and Swap 比较并交换)行为。如果至少有一个被 WATCH 监视的键在 EXEC 执行之前被修改了,那么整个事务都会被取消,EXEC 返回 nil 来表示事务已经失败。如下:
客户端 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> del k1 k2 k3
(integer) 0
127.0.0.1:6379> watch k1 k2 k3
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
客户端 2:
1
2
3
4
127.0.0.1:6379> set k1 v11
OK
127.0.0.1:6379> get k1
"v11"
通过 UNWATCH 命令,可以取消对所有 key 的监控,如下:
客户端 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> del k1 k2 k3
(integer) 1
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) OK
3) OK
127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
客户端 2:
1
2
3
4
127.0.0.1:6379> set k1 v11
OK
127.0.0.1:6379> get k1
"v11"
8 Redis 持久化
整体上来说,Redis 持久化有两种方式,快照持久化和 AOF,在项目中我们可以根据实际情况选择合适的持久化方式,也可以不用持久化,这关键看我们的 Redis 在项目中扮演了什么样的角色。
8.1 快照持久化
快照持久化,顾名思义,就是通过拍快照的方式实现数据的持久化,Redis 可以在某个时间点上对内存中的数据创建一个副本文件,副本文件中的数据在 Redis 重启时会被自动加载,我们也可以将副本文件拷贝到其他地方一样可以使用。
8.1.1 如何配置快照持久化
Redis 中的快照持化默认是开启的,redis.conf 中相关配置主要有如下几项:
1
2
3
4
5
6
7
save 3600 1
save 300 100
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
dbfilename dump.rdb
dir ./
前面三个 save 相关的选项表示备份的频率,分别表示 3600 秒内至少一个键被更改则进行快照,300 秒内至少 100 个键被更改则进行快照,60 秒内至少 10000 个键被更改则进行快照,stop-writes-on-bgsave-error 表示在快照创建出错后,是否继续执行写命令,rdbcompression 则表示是否对快照文件进行压缩,dbfilename 表示生成的快照文件的名字,dir 则表示生成的快照文件的位置,在 Redis 中,快照持久化默认就是开启的。我们可以通过如下步骤验证快照持久化的效果:
-
进入 Redis 安装目录,如果有 dump.rdb 文件,先将之删除。如下:
-
启动 redis,随便向 redis 中存储几个数据,然后关闭 redis 并退出,如下:
1 2 3 4 5 6 7 8 9 10
root@redis1:/usr/local/redis/redis-6.2.9# ./src/redis-server ./redis.conf root@redis1:/usr/local/redis/redis-6.2.9# ./src/redis-cli 127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> set k3 v3 OK 127.0.0.1:6379> shutdown not connected> exit
-
退出来后,我们发现刚刚删掉的 dump.rdb 文件又回来了,这就是生成的备份文 件。
-
此时再次启动 Redis 并进入,发现刚刚存储的数据都还在,这是因为 Redis 在启动时加载了dump.rdb 中的数据。再次关闭 Redis 并退出。
-
将 Redis 目录下的 dump.rdb 文件删除。
-
再次启动 Redis 并进入到控制台,所有的数据都不存在了。
8.1.2 快照持久化操作流程
通过上面的介绍,对快照持久化已经有一个大致的认识了,那么这个东西到底是怎么运行的?持久化的时机是什么?我们来仔细扒一扒。
1、在 Redis 运行过程中,我们可以向 Redis 发送一条 save 命令来创建一个快照,save 是一个阻塞命令,Redis 在接收到 save 命令之后,开始执行备份操作,在备份操作执行完毕之前,将不再处理其他请求,其他请求将被挂起,因此这个命令我们用的不多。save 命令执行如下:
1
2
127.0.0.1:6379> save
OK
2、在 Redis 运行过程中,我们也可以发送一条 bgsave 命令来创建一个快照,不同于 save 命令,bgsave 命令会 fork 一个子进程,然后这个子进程负责执行将快照写入硬盘,而父进程则继续处理客户端发来的请求,这样就不会导致客户端命令阻塞了。如下:
1
2
127.0.0.1:6379> bgsave
Background saving started
3、如果我们在 redis.conf 中配置了如下选项:
1
2
3
save 3600 1
save 300 100
save 60 10000
那么当条件满足时,比如 3600 秒内有一个 key 被操作了,那么 redis 就会自动触发 bgsave 命令进行备份。我们可以根据实际需求在 redis.conf 中配置多个这种触发规则。
4、还有一种情况也会触发 save 命令,那就是我们执行 shutdown 命令时,当我们用 shutdown 命令关闭 redis 时,此时也会执行一个 save 命令进行备份操作,并在备份操作完成后将服务器关闭。
5、还有一种特殊情况也会触发 bgsave 命令,就是在主从备份的时候。当从机连接上主机后,会发送一条 sync 命令来开始一次复制操作,此时主机会开始一次 bgsave 操作,并在 bgsave 操作结束后向从机发送快照数据实现数据同步。
8.1.3 快照持久化的缺点
快照持久化有一些缺点,比如 save 命令会发生阻塞,bgsave 虽然不会发生阻塞,但是 fork 一个子进程又要耗费资源,在一些极端情况下,fork 子进程的时间甚至超过数据备份的时间。定期的持久化也会让我们存在数据丢失的风险,最坏的情况我们可能丢失掉最近一次备份到当下的数据,具体丢失多久的数据,要看我们项目的承受能力,我们可以根据项目的承受能力配置 save 参数。
8.2 AOF 持久化
8.2.1 AOF(append-only file)
与快照持久化不同,AOF 持久化是将被执行的命令写到 aof 文件末尾,在恢复时只需要从头到尾执行一遍写命令即可恢复数据,AOF 在 Redis 中默认是没有开启的,需要我们手动开启,开启方式如下:
打升 redis.conf 配置文件,修改 appendonly 属性值为 yes,如下:
1
appendonly yes
另外几个和 AOF 相关的属性如下:
1
2
3
4
5
6
7
8
9
10
11
# 表示生成 AOF 备份文件的文件名
appendfilename "appendonly.aof"
# 表示备份的时机
# appendfsync always #表示每执行一个命令就备份一次
appendfsync everysec # 表示每秒备份一次
# appendfsync no # 表示将备份时机交给操作系统
# 表示在对 AOF 文件进行压缩时,是否执行同步操作
no-appendfsync-on-rewrite no
# 表示 AOF 文件的压缩时机
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
同时为了避免快照备份的影响,需要将快照备份关闭,关闭方式如下:
1
save ""
此时,当我们在 Redis 中进行数据操作时,就会自动生成 AOF 的配置文件 appendonly.aof,如下:
注意此时没有 dump.rdb 文件,这时我们将 Redis 关闭并重启,会发现之前的数据都还在,这就是 AOF 备份的结果。
8.2.2 AOF 备份的几个关键点
1、通过上面的介绍,了解到 appendfsync 的取值一共有三种,我们在项目中首选 everysec,always 选项会严重降低 Redis 性能。
2、使用 everysec,最坏的情况下我们可能丢失1秒的数据。
8.2.3 AOF 文件的重写与压缩
AOF 备份有很多明显的优势,当然也有劣势,那就是文件大小。随着系统的运行,AOF 的文件会越来越大,甚至把整个电脑的硬盘填满,AOF 文件的重写与压缩机制可以在一定程度上缓解这个问题。
当 AOF 的备份文件过大时,我们可以向 Redis 发送一条 bgrewriteaof 命令进行文件重写,如下:
1
2
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
bgrewriteaof 的执行原理和上文说的 bgsave 的原理一致,这里就不再赘述了,因此 bgsave 执行过程中存在的问题在这里也一样存在。
bgrewriteaof 也可以自动执行,自动执行时间则依赖于 auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 配置,auto-aof-rewrite-percentage 100 表示当 aof 文件大小超过上一次重写时的 aof 文件大小的百分之多少时会再次进行重写,如果之前没有重写,则以启动时的 aof 文件大小为依据,同时还要求 aof 文件的大小至少要大于 64M(auto-aof-rewrite-min-size 64mb)。
8.2.4 最佳实践
1、如果 Redis 只做缓存服务器,那么可以不使用任何持久化方式。
2、同时开启两种持久化方式,在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整;RDB 的数据不完整时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?作者建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),快速重启,而且不会有 AOF 可能潜在的 bug,留着作为一个万一的手段。
3、因为 RDB 文件只用作后备用途,建议只在 slave 上持久化 RDB 文件,而且只要 15 分钟备份一次就够了,只保留 save 3600 1 这条规则。
4、如果启用 AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只加载自己的 AOF 文件就可以了。代价一是带来了持续的 IO,二是 AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少 AOF rewrite 的频率,AOF 重写的基础大小默认值 64M 太小了,可以设到 5G 以上。默认超过原大小 100% 时重写可以改到适当的数值。
5、如果不启用 AOF,仅靠 Master-Slave Replication 实现高可用性也可以。能省掉一大笔 IO,也减少了 rewrite 时带来的系统波动。代价是如果 Master/Slave 同时挂掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB 文件,载入较新的那个。
9 Redis 主从复制
9.1 主从复制
主从复制可以在一定程度上扩展 Redis 性能,Redis 的主从复制和关系型数据库的主从复制类似,从机能够精确的复制主机上的内容。实现主从复制后,一方面能够实现数据的读写分离,降低 master 的压力,另一方面也能实现数据的备份。
9.2 配置方式
假设有三个 Redis 实例,地址分别如下:
192.168.248.128:6379 192.168.248.128:6380 192.168.248.128:6381
即同一台服务器上三个实例,配置方式如下:
1、将 redis.conf 文件更名为 redis6379.conf,以便区分,然后把 redis6379.conf 再复制两份,分别为 redis6380.conf 和 redis6381.conf。如下:
2、打开 redis6379.conf,将如下配置均加上 6379,如下:
1
2
3
4
5
port 6379
pidfile /var/run/redis_6379.pid
logfile "6379.1og"
dbfilename dump6379.rdb
appendfilename "appendonly6379.aof"
3、同理,分别打开 redis6380.conf 和 redis6381.conf 两个配置文件,将第二步涉及到 6379 的分别改为 6380 和 6381。
4、输入如下命令,启动三个 redis 实例:
1
2
3
4
cd /usr/local/redis/redis-6.2.9
./src/redis-server ./redis6379.conf
./src/redis-server ./redis6380.conf
./src/redis-server ./redis6381.conf
5、输入如下命令,分别进入三个实例的控制台:
1
2
3
4
5
6
./src/redis-cli -p 6379
127.0.0.1:6379>
./src/redis-cli -p 6380
127.0.0.1:6380>
./src/redis-cli -p 6381
127.0.0.1:6381>
此时就成功配置了三个 redis 实例了。
6、假设在这三个实例中,6379 是主机,即 master,6380 和 6381 是从机,即 slave,那么如何配置这种实例关系呢,很简单,分别在 6380 和 6381 上执行如下命令:
1
2
3
4
127.0.0.1:6380> slaveof 127.0.0.1 6379
OK
127.0.0.1:6381> slaveof 127.0.0.1 6379
OK
这一步也可以通过在两个从机的 redis.conf 中添加如下配置来解决:
1
slaveof 127.0.0.1 6379
OK,主从关系搭建好后,可以通过如下命令可以查看每个实例当前的状态,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=224,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=224,lag=1
master_failover_state:no-failover
master_replid:b98f40cef119494e9940739bbb5fa21cd2a3ee5b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:224
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:224
可以看到 6379 是一个主机,上面挂了两个从机,两个从机的地址、端口等信息都展现出来了。如果在 6380 上执行 info replication,显示信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:336
slave_repl_offset:336
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:b98f40cef119494e9940739bbb5fa21cd2a3ee5b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:336
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:336
可以看到 6380 是一个从机,从机的信息以及它的主机的信息都展示出来了。
7、此时,在主机中存储一条数据,在从机中就可以 get 到这条数据了。
1
2
3
4
5
6
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6380> keys *
1) "k1"
127.0.0.1:6380> get k1
"v1"
9.3 主从复制注意点
1、如果主机已经运行了一段时间了,并且已经存储了一些数据了,此时从机连上来,那么从机会将主机上所有的数据进行备份,而不是从连接的那个时间点开始备份。
2、配置了主从复制之后,主机上可读可写,但是从机只能读取不能写入(可以通过修改 redis.conf 中 slave-read-only 的值让从机也可以执行写操作)。
1
2
slave-read-only yes # 设置为只读模式
# slave-read-only no # 设置为读写模式(不推荐在线上环境中使用)
3、在整个主从结构运行过程中,如果主机不幸挂掉,重启之后,他依然是主机,主从复制操作也能够继续进行。
9.4 复制原理
- 初始连接与全量同步
- 当一个新的从服务器第一次连接到主服务器时,或者由于某种原因(如主服务器重启)导致之前的复制状态无效时,从服务器会请求进行全量同步。
- 在全量同步过程中,主服务器会先保存当前的数据到一个快照文件中(这个过程可能会稍微影响性能),然后把这个快照文件发送给从服务器。
- 从服务器接收并加载这个快照文件,这样它的数据集就与主服务器当前的数据集一致了。
- 接着,主服务器还会把快照之后发生的所有写操作(比如新增、修改、删除数据的命令)发送给从服务器,确保从服务器完全跟上主服务器的步伐。
- 增量同步
- 在全量同步之后,或者如果从服务器之前已经成功复制过数据,并且断开连接的时间不长,那么再次连接时可能会进行增量同步。
- 增量同步意味着主服务器只需要发送从服务器断开连接后错过的那些写操作给从服务器,而不是整个数据集。
- 为了支持增量同步,主服务器会保留一个最近的写操作记录(叫做命令积压缓冲区)。
- 如果从服务器提供的断开连接时的复制位置(一个数字,表示接收到的字节数)在命令积压缓冲区中能找到,那么就从那个位置开始继续同步。
- 复制过程中的标记
- 每个主服务器都有一个唯一的复制 ID 和一个不断增长的复制偏移量。
- 复制 ID 就像主服务器的身份证号码,每次主服务器重启时都会改变。
- 复制偏移量就像主服务器发送数据的进度条,它告诉从服务器已经接收了多少数据。
- 重新连接与全量同步的触发
- 如果从服务器与主服务器的连接断开太久,或者由于某种原因(比如网络问题、主服务器重启等)导致之前的复制状态无法恢复,那么重新连接时通常会再次进行全量同步。
- 这是因为主服务器可能已经发生了很多变化,无法仅通过增量同步来确保从服务器与主服务器的数据一致性。
9.5 一场接力赛
在上面的文章中,搭建的主从复制模式是下面这样的:
graph LR
subgraph 1主2从
direction LR
master(master)
slave1(slave1)
slave2(slave2)
master --> slave1
master --> slave2
end
style master fill:#ccf,stroke:#f66,stroke-width:2px
style slave1 fill:#ccf,stroke:#f66,stroke-width:2px
style slave2 fill:#ccf,stroke:#f66,stroke-width:2px
实际上,一主二仆的主从复制,也可以搭建成下面这种结构:
graph LR
subgraph 1主1主从1从
direction LR
master(master)
slave1(slave1)
slave2(slave2)
master --> slave1
slave1 --> slave2
end
style master fill:#ccf,stroke:#f66,stroke-width:2px
style slave1 fill:#ccf,stroke:#f66,stroke-width:2px
style slave2 fill:#ccf,stroke:#f66,stroke-width:2px
搭建方式很简单,在前文基础上,只需要修改 6381 的 master 即可,在 6381 实例上执行如下命令,让 6381 从 6380 实例上复制数据,如下:
1
2
127.0.0.1:6381> slaveof 127.0.0.1 6380
OK
此时,再看6379的 slave,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=3510,lag=0
master_failover_state:no-failover
master_replid:b98f40cef119494e9940739bbb5fa21cd2a3ee5b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3510
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:3510
只有一个 slave,就 6380,再看 6380 的信息,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_read_repl_offset:3566
slave_repl_offset:3566
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=3566,lag=1
master_failover_state:no-failover
master_replid:b98f40cef119494e9940739bbb5fa21cd2a3ee5b
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:3566
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:3566
6380 此时的角色是一个从机,它的主机是 6379,但是 6380 自己也有一个从机,那就是 6381,此时的主从结构如下图:
graph LR
subgraph 1主1主从1从
direction LR
master(master 6379)
slave1(slave1 6380)
slave2(slave2 6381)
master --> slave1
slave1 --> slave2
end
style master fill:#ccf,stroke:#f66,stroke-width:2px
style slave1 fill:#ccf,stroke:#f66,stroke-width:2px
style slave2 fill:#ccf,stroke:#f66,stroke-width:2px
9.6 哨兵模式
结合上篇文章,一共介绍了两种主从模式了,但是这两种,不管是哪一种,都会存在这样一个问题,那就是当主机宕机时,就会发生群龙无首的情况,如果在主机宕机时,能够从从机中选出一个来充当主机,那么就不用每次去手动重启主机了,这就涉及到一个新的话题,那就是哨兵模式。
所谓的哨兵模式,其实并不复杂,还是在前面的基础上来搭建哨兵模式。
假设现在 master 是 6379,两个从机分别是 6380 和 6381,两个从机都是从 6379 上复制数据。先按照上文的步骤,配置好一主二仆,然后在 redis 目录下打升开 sentinel.conf 文件,做如下配置:
1
sentinel monitor mymaster 127.0.0.1 6379 1
-
sentinel monitor
:这是 Sentinel 用于开始监控一个主节点的命令。 mymaster
:这是你给监控的主节点起的名字,可以是任意名字,用于在 Sentinel 的配置和日志中标识这个主节点。127.0.0.1
:这是被监控的主节点的IP地址。6379
:这是被监控的主节点的端口号。1
:这个最后的数字表示进行故障转移(failover)所需要的 Sentinel 数量。也就是说,当有多少个 Sentinel 认为主节点不可用时,才会触发故障转移。在这个例子中,设置为1
意味着只要有一个 Sentinel 认为主节点不可用,就会触发故障转移。(这里只启动一个哨兵,所以设置为 1)
配置完成后,输入如下命令启动哨兵:
1
./src/redis-sentinel sentinel.conf
然后启动一主二仆架构,启动成功后,关闭 master,观察哨兵窗口输出的日志,如下:
可以看到,6379 挂掉之后,redis 内部重新选举,6380 重新上位。此时,如果 6379 重启,也不再是 master 了,只能屈身做一个 slave 了。
9.7 注意问题
由于所有的写操作都是先在 Master 上操作,然后同步更新到 Slave 上,所以从 Master 同步到 Slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave 机器数量的增加也会使这个问题更加严重。因此需要集群来进一步提升 redis 性能。
10 Redis 集群搭建
10.1 集群原理
- 所有的 Redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。
- 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。
- 客户端与 Redis 节点直连,不需要中间 proxy 层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
- Redis-cluster 把所有的物理节点映射到 [0-16383] slot 上,cluster(簇)负责维护 node<->slot<->value。Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,Redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
怎么样投票
投票过程是集群中所有 master 参与,如果半数以上 master 节点与 master 节点通信超过 cluster-node-timeout 设置的时间,认为当前 master 节点挂掉。
怎么样判定节点不可用
- 如果集群任意 master 挂掉,且当前 master 没有 slave,集群进入 fail 状态,也可以理解成集群的 slot 映射 [0-16383] 不完整时进入 fail 状态。
- 如果集群超过半数以上 master 挂掉,无论是否有 slave,集群进入 fail 状态,当集群不可用时,所有对集群的操作做都不可用,收到 ((error) CLUSTERDOWN The cluster is down) 错误。
10.2 Ruby 环境
Redis 集群管理工具 redis-trib.rb 依赖 ruby 环境,首先需要安装 ruby 环境:
1
2
apt install ruby
apt install rubygems
10.3 集群搭建
首先对集群做一个简单规划,假设集群中一共有三个节点,每个节点一个主机一个从机,这样一共需要 6 个 Redis 实例。首先创建 redis-cluster 文件夹,在该文件夹下分别创建 7001、7002、7003、7004、7005、7006 文件夹,用来存放 Redis 配置文件。
将 Redis 也在 redis-cluster 目录下安装一份,然后将 redis.conf 文件向 7001-7006 这6个文件夹中分别拷贝一份,拷贝完成后,分别修改如下参数:
1
2
3
4
5
6
7
8
9
cd /usr/local/redis
mkdir redis-cluster
mkdir 7001 7002 7003 7004 7005 7006
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7001
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7002
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7003
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7004
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7005
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7006
1
2
3
4
5
6
port 7001
# bind 127.0.0.1
cluster-enabled yes
cluster-config-file nodes-7001.conf
protected-mode no
daemonize yes
1
2
make
make install
这是 7001 目录下的配置,其他的文件夹将 7001 改为对应的数字即可。修改完成后,进入到 redis 安装目录中,分别启动各个 redis,使用刚刚修改过的配置文件。
启动成功后,可以查看 redis 进程,如下:
1
2
3
4
5
6
7
./7001/redis-6.2.9/src/redis-server ./7001/redis-6.2.9/redis.conf
./7002/redis-6.2.9/src/redis-server ./7002/redis-6.2.9/redis.conf
./7003/redis-6.2.9/src/redis-server ./7003/redis-6.2.9/redis.conf
./7004/redis-6.2.9/src/redis-server ./7004/redis-6.2.9/redis.conf
./7005/redis-6.2.9/src/redis-server ./7005/redis-6.2.9/redis.conf
./7006/redis-6.2.9/src/redis-server ./7006/redis-6.2.9/redis.conf
ps -ef | grep redis
这个表示各个节点都启动成功了。接下来就可以进行集群的创建了,在 redis-cluster 目录下执行如下命令:
1
./7001/redis-6.2.9/src/redis-cli --cluster create 192.168.64.29:7001 192.168.64.29:7002 192.168.64.29:7003 192.168.64.29:7004 192.168.64.29:7005 192.168.64.29:7006 --cluster-replicas 1
注意,–cluster-replicas 后面的 1 表示每个主机都带有 1 个从机。
注意创建过程的日志,每个 redis 都获得了一个编号,同时日志也说明了哪些实例做主机,哪些实例做从机,每个从机的主机是谁,每个主机所分配到的 hash 槽范围等等。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
root@redis1:/usr/local/redis/redis-cluster# ./7001/redis-6.2.9/src/redis-cli --cluster create 192.168.64.29:7001 192.168.64.29:7002 192.168.64.29:7003 192.168.64.29:7004 192.168.64.29:7005 192.168.64.29:7006 --cluster-replicas 1
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.64.29:7005 to 192.168.64.29:7001
Adding replica 192.168.64.29:7006 to 192.168.64.29:7002
Adding replica 192.168.64.29:7004 to 192.168.64.29:7003
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 192.168.64.29:7001
slots:[0-5460] (5461 slots) master
M: 71f355c9bc847167d45861bfc6c0eb41483cc889 192.168.64.29:7002
slots:[5461-10922] (5462 slots) master
M: ca43abd94b9ff64bc7d3318861a00d0df848cc8e 192.168.64.29:7003
slots:[10923-16383] (5461 slots) master
S: 56ae8a94a1f0acf0babd602f92eaedbd4c3cbe8e 192.168.64.29:7004
replicates ca43abd94b9ff64bc7d3318861a00d0df848cc8e
S: 7347a004e19beefe3487006b60eff261741e8591 192.168.64.29:7005
replicates d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c
S: 552188d6036ee5d010c846cd82b6a72f2d43902c 192.168.64.29:7006
replicates 71f355c9bc847167d45861bfc6c0eb41483cc889
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
>>> Performing Cluster Check (using node 192.168.64.29:7001)
M: d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 192.168.64.29:7001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 71f355c9bc847167d45861bfc6c0eb41483cc889 192.168.64.29:7002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: ca43abd94b9ff64bc7d3318861a00d0df848cc8e 192.168.64.29:7003
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 552188d6036ee5d010c846cd82b6a72f2d43902c 192.168.64.29:7006
slots: (0 slots) slave
replicates 71f355c9bc847167d45861bfc6c0eb41483cc889
S: 56ae8a94a1f0acf0babd602f92eaedbd4c3cbe8e 192.168.64.29:7004
slots: (0 slots) slave
replicates ca43abd94b9ff64bc7d3318861a00d0df848cc8e
S: 7347a004e19beefe3487006b60eff261741e8591 192.168.64.29:7005
slots: (0 slots) slave
replicates d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
查询集群信息
集群创建成功后,可以登录到 Redis 控制台查看集群信息,注意登录时要添加 -c 参数,表示以集群方式连接,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
./7001/redis-6.2.9/src/redis-cli -p 7001 -c
127.0.0.1:7001> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:25
cluster_stats_messages_pong_sent:32
cluster_stats_messages_sent:57
cluster_stats_messages_ping_received:27
cluster_stats_messages_pong_received:25
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:57
添加节点
首先准备一个端口为 7007 的主节点并启动,准备方式和前面步骤一样,启动成功后,通过如下命令添加主节点:
1
2
3
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7007
./7007/redis-6.2.9/src/redis-server ./7007/redis-6.2.9/redis.conf
./7001/redis-6.2.9/src/redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001
主节点添加之后,可以通过 cluster nodes 命令查看主节点是否添加成功,此时发现新添加的节点没有分配到 slot,如下:
1
2
3
4
5
6
7
8
9
root@redis1:/usr/local/redis/redis-cluster# ./7001/redis-6.2.9/src/redis-cli -p 7001 -c
127.0.0.1:7001> cluster nodes
cd3f96a72f46894260fcaa139ff94b9a87ef09b0 127.0.0.1:7007@17007 master - 0 1733648275000 0 connected
71f355c9bc847167d45861bfc6c0eb41483cc889 192.168.64.29:7002@17002 master - 0 1733648275278 2 connected 5461-10922
d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 127.0.0.1:7001@17001 myself,master - 0 1733648272000 1 connected 0-5460
ca43abd94b9ff64bc7d3318861a00d0df848cc8e 192.168.64.29:7003@17003 master - 0 1733648272000 3 connected 10923-16383
552188d6036ee5d010c846cd82b6a72f2d43902c 192.168.64.29:7006@17006 slave 71f355c9bc847167d45861bfc6c0eb41483cc889 0 1733648274000 2 connected
56ae8a94a1f0acf0babd602f92eaedbd4c3cbe8e 192.168.64.29:7004@17004 slave ca43abd94b9ff64bc7d3318861a00d0df848cc8e 0 1733648273000 3 connected
7347a004e19beefe3487006b60eff261741e8591 192.168.64.29:7005@17005 slave d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 0 1733648272000 1 connected
没有分配到 slot 将不能存储数据,此时需要手动分配 slot,分配命令如下:
1
./7001/redis-6.2.9/src/redis-cli --cluster reshard 127.0.0.1:7001
后面的地址为任意一个节点地址,在分配的过程中,一共要输入如下几个参数:
-
一共要划分多少个 hash 槽出来?就是总共要给新添加的节点分多少 hash 槽,这个参数依实际情况而定,如下:
1
How many slots do you want to move (from 1 to 16384)? 4096
-
这些划分出来的槽要给谁,这里输入 7007 节点的编号,如下:
1
What is the receiving node ID? cd3f96a72f46894260fcaa139ff94b9a87ef09b0
-
要让谁出血?因为 hash 槽目前已经全部分配完毕,要重新从已经分好的节点中拿出来一部分给 7007,必然要让另外三个节点把吃进去的吐出来,这里可以输入多个节点的编号,每次输完一个点击回车,输完所有的输入 done 表示输入完成,这样就让这几个节点让出部分 slot,如果要让所有具有 slot 的节点都参与到此次 slot 重新分配的活动中,那么这里直接输入 all 即可,如下:
1 2 3 4
Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1: all
主要就是这几个参数,输完之后进入到 slot 重新分配环节,分配完成后,通过 cluster nodes 命令,可以发现 7007 已经具有 slot 了,如下:
1
2
3
4
5
6
7
8
9
root@redis1:/usr/local/redis/redis-cluster# ./7001/redis-6.2.9/src/redis-cli -p 7001 -c
127.0.0.1:7001> cluster nodes
cd3f96a72f46894260fcaa139ff94b9a87ef09b0 127.0.0.1:7007@17007 master - 0 1733649239498 7 connected 0-1364 5461-6826 10923-12287
71f355c9bc847167d45861bfc6c0eb41483cc889 192.168.64.29:7002@17002 master - 0 1733649240789 2 connected 6827-10922
d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 127.0.0.1:7001@17001 myself,master - 0 1733649237000 1 connected 1365-5460
ca43abd94b9ff64bc7d3318861a00d0df848cc8e 192.168.64.29:7003@17003 master - 0 1733649240000 3 connected 12288-16383
552188d6036ee5d010c846cd82b6a72f2d43902c 192.168.64.29:7006@17006 slave 71f355c9bc847167d45861bfc6c0eb41483cc889 0 1733649239000 2 connected
56ae8a94a1f0acf0babd602f92eaedbd4c3cbe8e 192.168.64.29:7004@17004 slave ca43abd94b9ff64bc7d3318861a00d0df848cc8e 0 1733649237000 3 connected
7347a004e19beefe3487006b60eff261741e8591 192.168.64.29:7005@17005 slave d5a02d07edbce89d7f74cd41bb76e7c0ce5f029c 0 1733649238000 1 connected
刚刚是添加主节点,也可以添加从节点,比如要把 7008 作 7007 的从节点,添加方式如下:
1
2
3
4
5
6
7
8
tar -zxvf redis-6.2.9.tar.gz -C ./redis-cluster/7008
./7008/redis-6.2.9/src/redis-server ./7008/redis-6.2.9/redis.conf
./7001/redis-6.2.9/src/redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7001
./7001/redis-6.2.9/src/redis-cli -p 7008 -c
cluster replicate cd3f96a72f46894260fcaa139ff94b9a87ef09b0
其中 cd3f96a72f46894260fcaa139ff94b9a87ef09b0 是 7007 的编号。
删除节点
删除节点也比较简单,如下:
1
./7001/redis-6.2.9/src/redis-cli --cluster del-node 127.0.0.1:7001 ca43abd94b9ff64bc7d3318861a00d0df848cc8e
注意 cd3f96a72f46894260fcaa139ff94b9a87ef09b0 是要删除节点的编号。
再注意:删除已经占有 hash 槽的结点会失败,报错如下:
1
[ERR] Node 192.168.64.29:7003 is not empty! Reshard data away and try again.
需要将该节点占用的 hash 槽分配出去才可删除(分配方式与上文一致,就不再赘述了)。
11 Java Redis客户端库
比较常用的 Java Redis 客户端库:Jedis、Lettuce。
11.1 选择建议
- 如果你的项目对 Redis 操作的性能要求不是特别高,或者更偏向于使用简单易用的API,那么 Jedis 可能是一个不错的选择。
- 如果你的项目需要处理大量的 Redis 操作,或者对性能有较高的要求,那么 Lettuce 可能更适合你,因为它提供了异步和响应式编程模型,以及出色的性能表现。
11.2 Jedis
Jedis 是一个 Java 语言的 Redis 客户端库,它提供了丰富的 API 来操作 Redis 数据库。以下是 Jedis 的详细操作说明:
11.2.1 连接 Redis 服务器
在使用 Jedis 之前,首先需要连接到 Redis 服务器。可以通过创建 Jedis 对象并指定 Redis 服务器的地址和端口号来实现连接。例如:
1
Jedis jedis = new Jedis("localhost", 6379);
11.2.2 操作 Redis 数据类型
Jedis 支持 Redis 支持的各种数据类型,包括字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等。以下是对这些数据类型的操作示例:
-
字符串(String)
- 设置键值对:
jedis.set("key", "value");
- 获取键的值:
String value = jedis.get("key");
- 追加值到已有键:
jedis.append("key", "appendValue");
- 设置键值对:
-
哈希(Hash)
- 设置哈希字段的值:
jedis.hset("hashKey", "field", "value");
- 获取哈希字段的值:
String fieldValue = jedis.hget("hashKey", "field");
- 获取哈希中所有字段和值:
Map<String, String> hashMap = jedis.hgetAll("hashKey");
- 设置哈希字段的值:
-
列表(List)
- 在列表头部插入元素:
jedis.lpush("listKey", "value");
- 在列表尾部插入元素:
jedis.rpush("listKey", "value");
- 获取列表中的元素:
List<String> list = jedis.lrange("listKey", 0, -1);
- 在列表头部插入元素:
-
集合(Set)
- 添加元素到集合:
jedis.sadd("setKey", "value");
- 获取集合中的所有元素:
Set<String> set = jedis.smembers("setKey");
- 添加元素到集合:
-
有序集合(Sorted Set)
- 添加元素到有序集合,并设置分数:
jedis.zadd("sortedSetKey", score, "value");
- 获取有序集合中的元素,按分数排序:
Set<Tuple> sortedSet = jedis.zrangeWithScores("sortedSetKey", 0, -1);
- 添加元素到有序集合,并设置分数:
11.2.3 连接池管理
Jedis 提供了连接池的支持,可以通过连接池来管理与 Redis 服务器的连接,提高并发性能。使用连接池时,需要先创建 JedisPool 对象,并通过它来获取 Jedis 连接。例如:
1
2
3
4
5
6
7
8
9
10
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(128); // 设置连接池的最大连接数
poolConfig.setMaxIdle(16); // 设置连接池的最大空闲连接数
poolConfig.setMinIdle(4); // 设置连接池的最小空闲连接数
JedisPool jedisPool = new JedisPool(poolConfig, "localhost", 6379);
try (Jedis jedis = jedisPool.getResource()) {
// 使用 jedis 进行 Redis 操作
}
11.2.4 事务和管道
-
事务
Jedis 提供了事务的支持,可以通过 multi() 方法开启一个事务,然后通过 exec() 方法提交事务。在事务执行期间,所有的命令都会按照顺序被发送到 Redis 服务器,并且不会立即执行,直到提交事务时才会一起执行。例如:
1 2 3 4 5 6 7
try (Jedis jedis = new Jedis("localhost", 6379)) { Transaction tx = jedis.multi(); tx.set("txKey1", "value1"); tx.set("txKey2", "value2"); List<Object> results = tx.exec(); // 处理事务结果 }
-
管道
管道(Pipeline)是一种可以将多个命令打包发送到 Redis 服务器,并一次性获取所有命令执行结果的技术。使用管道可以减少网络往返次数,提高性能。例如:
1 2 3 4 5 6 7
try (Jedis jedis = new Jedis("localhost", 6379)) { Pipeline pipeline = jedis.pipelined(); pipeline.set("pipeKey1", "value1"); pipeline.set("pipeKey2", "value2"); List<Object> results = pipeline.syncAndReturnAll(); // 处理命令结果 }
11.2.5 其他操作
Jedis 还提供了许多其他操作,如发布订阅、Lua 脚本执行、服务器信息等。这些操作可以通过 Jedis 提供的相应 API 来实现。例如:
- 发布消息:
jedis.publish("channel", "message");
- 订阅消息:可以使用 Jedis 的 PubSub 类来实现订阅功能。
- 执行 Lua 脚本:
Object result = jedis.eval(script, keys, args);
其中 script 是 Lua 脚本的字符串表示,keys 和 args 分别是传递给 Lua 脚本的键和参数列表。 - 获取服务器信息:
String info = jedis.info();
可以获取 Redis 服务器的详细信息。
以上是 Jedis 的详细操作说明,希望对你有所帮助。
11.3 Lettuce
11.3.1 引入 Lettuce 依赖
在 Java 项目中,可以使用 Maven 或者 Gradle 来引入 Lettuce 的依赖。
- 使用 Maven 引入依赖:
1
2
3
4
5
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>最新版本号</version>
</dependency>
- 使用 Gradle 引入依赖:
1
2
3
dependencies {
implementation 'io.lettuce:lettuce-core:最新版本号'
}
11.3.2 连接到 Redis 服务器
创建一个RedisClient实例,并使用该实例打开一个新的连接。可以通过连接获取同步、异步或反应式的Redis命令接口。
1
2
3
RedisClient redisClient = RedisClient.create("redis://localhost:6379/");
StatefulRedisConnection<String, String> connection = redisClient.connect();
RedisCommands<String, String> syncCommands = connection.sync();
11.3.3 执行 Redis 命令
Lettuce 提供了丰富的 Redis 命令操作,包括基本的增删改查、事务、管道、发布订阅和脚本执行等。
- 基本操作:
1
2
3
4
5
6
// 设置键值对
syncCommands.set("key", "value");
// 获取键值对
String value = syncCommands.get("key");
System.out.println("Retrieved value: " + value);
- 事务操作:
Lettuce 使用 Transaction 对象来实现 Redis 的事务操作。事务可以确保在一次请求中执行多个命令,并保证这些命令的原子性。
1
2
3
4
5
RedisTransaction transaction = syncCommands.multi();
transaction.set("key1", "value1");
transaction.set("key2", "value2");
List<Object> result = transaction.exec();
// 处理事务结果
- 管道操作:
Pipeline 是一种批量执行多个 Redis 命令的机制,通过减少网络往返的次数,提高了操作的效率。
1
2
3
4
RedisPipeline pipeline = syncCommands.pipelined();
pipeline.set("key1", "value1");
pipeline.set("key2", "value2");
pipeline.sync(); // 执行管道中的所有命令
- 发布订阅操作:
Lettuce 支持 Redis 的发布/订阅模式,可以用于实现消息传递和通知机制。
1
2
3
4
5
6
7
RedisPubSubListener<String, String> listener = new RedisPubSubListener<String, String>() {
@Override
public void message(String channel, String message) {
System.out.println("Received message: " + message);
}
};
syncCommands.subscribe(listener, "channel");
- 脚本执行:
Lettuce 支持使用 Lua 脚本执行 Redis 命令,可以实现更复杂的操作和逻辑。
1
2
3
String script = "return redis.call('set', KEYS[1], ARGV[1])";
Object result = syncCommands.eval(script, 1, "key", "value");
System.out.println("Script result: " + result);
11.3.4 关闭连接和客户端
在完成Redis操作后,需要关闭连接和客户端以释放资源。
1
2
connection.close();
redisClient.shutdown();
11.3.5 连接池的使用
虽然 Lettuce 本身并没有传统意义上的连接池,但在使用 Spring Data Redis 的情况下,可以通过配置 LettuceConnectionFactory 来实现连接池的效果。
1
2
3
4
5
6
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(new GenericObjectPoolConfig<>().setMaxTotal(10).setMaxIdle(5).setMinIdle(2))
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration("localhost", 6379);
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(serverConfig, clientConfig);
在这个配置中,GenericObjectPoolConfig
允许设置一些连接池的参数,如maxTotal
(最大连接数)、maxIdle
(最多的空闲连接数)、minIdle
(最少的空闲连接数)等。
11.3.6 错误处理和重试
Lettuce 提供了自动重连功能,当 Redis 服务器重新启动或断开连接时,Lettuce 可以自动恢复连接。此外,还可以使用错误处理和重试机制来增强应用程序的健壮性。
12 Spring Data Redis
Spring Data Redis是Spring Data模块的一部分,专门用于支持在Spring管理项目中对Redis的操作。以下是Spring Data Redis的详细使用方法:
12.1 引入依赖
在 Spring Boot 项目中,可以通过在 pom.xml
文件中添加依赖来引入 Spring Data Redis:
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
12.2 配置 Redis 数据源
在 Spring Boot 应用的主配置文件(如 application.properties
或 application.yml
)中配置 Redis 服务器的基本信息,包括主机地址、端口号、密码等参数。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=yourpassword
# 连接池配置
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.min-idle=2
spring.redis.lettuce.pool.max-wait=-1ms
12.3 使用 RedisTemplate 或 StringRedisTemplate
Spring Data Redis 提供了 RedisTemplate
和 StringRedisTemplate
两个模板类,用于实现 Redis 的 CRUD 操作。
- RedisTemplate:
RedisTemplate
的 key 和 value 的泛型都是Object
,它采用 JDK 的序列化策略(JdkSerializationRedisSerializer
)来保存数据。因此,当存储对象时,会先将对象序列化为字节数组,再存入 Redis 数据库。取出时,再将字节数组反序列化为对象。由于这种序列化方式可能导致存储的数据在 Redis 中显示为乱码,因此通常用于存储复杂的 Java 对象。 - StringRedisTemplate:
StringRedisTemplate
的 key 和 value 的泛型都是String
,它采用 String 的序列化策略(StringRedisSerializer
)来保存数据。因此,它只能存储字符串类型的数据。当尝试存储对象时,会报错。StringRedisTemplate
更加轻量级,通常用于存储和读取简单的字符串数据。
12.4 操作 Redis 数据类型
Spring Data Redis 支持 Redis 中的五种数据类型(string、list、set、zset、hash)的操作。可以通过 RedisTemplate
或 StringRedisTemplate
的 opsFor...
方法来获取对应数据类型的操作接口。
- 操作字符串(String):
1
2
3
4
ValueOperations<String, String> valueOps = redisTemplate.opsForValue();
valueOps.set("name", "张三");
String name = valueOps.get("name");
System.out.println(name);
- 操作列表(List):
1
2
3
4
5
6
ListOperations<String, String> listOps = redisTemplate.opsForList();
listOps.rightPush("mylist", "元素1", "元素2");
List<String> mylist = listOps.range("mylist", 0, -1);
for (String element : mylist) {
System.out.println(element);
}
- 操作集合(Set):
1
2
3
4
5
6
SetOperations<String, String> setOps = redisTemplate.opsForSet();
setOps.add("myset", "元素A", "元素B");
Set<String> myset = setOps.members("myset");
for (String element : myset) {
System.out.println(element);
}
- 操作有序集合(ZSet):
1
2
3
4
5
6
7
ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
zSetOps.add("myzset", "元素1", 1.0);
zSetOps.add("myzset", "元素2", 2.0);
Set<String> myzset = zSetOps.range("myzset", 0, -1);
for (String element : myzset) {
System.out.println(element);
}
- 操作哈希(Hash):
1
2
3
4
5
6
7
HashOperations<String, String, String> hashOps = redisTemplate.opsForHash();
hashOps.put("myhash", "field1", "value1");
hashOps.put("myhash", "field2", "value2");
Map<String, String> myhash = hashOps.entries("myhash");
for (Map.Entry<String, String> entry : myhash.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
12.5 序列化与反序列化
Spring Data Redis 提供了多种序列化策略,可以通过自定义 RedisTemplate
的序列化器来优化数据存取性能。例如,可以使用 StringRedisSerializer
作为键的序列化器,以确保键名保持字符串形式;使用 GenericJackson2JsonRedisSerializer
作为值的序列化器,以将对象序列化为 JSON 格式存储在 Redis 中。
12.6 事务管理与消息订阅/发布
Spring Data Redis 还支持 Redis 的事务管理和消息订阅/发布机制。可以通过 RedisTemplate.executePipelined()
或 execute()
方法来开启事务,并在其中执行多条命令。同时,可以利用 Lettuce
或 ReactiveRedisConnectionFactory
来实现频道(channel)的订阅与消息的发布。
12.7 持久化与缓存策略
为了防止因意外断电或系统崩溃导致的数据丢失,Spring Data Redis 提供了多种持久化方案供开发者选择,如 RDB(Redis Database Backup)快照和AOF(Append Only File)。此外,还支持配置缓存策略,如 TTL(Time To Live)机制,通过设置键的有效期来自动清理过期数据,以减轻内存压力并提高系统整体性能。
综上所述,Spring Data Redis 为开发者提供了丰富且易用的 API 来操作 Redis 数据库。通过合理配置和使用这些 API,可以充分发挥 Redis 的强大功能并提升应用的性能。