Redis
一、什么是 Redis?
1.1、简介
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 写的 key-value 存储系统,是跨平台的非关系型数据库。
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的 API。
Redis 通常被称为数据结构服务器,因为值(value)可以是字符串(String)、哈希(Hash)、列表(list)、集合(sets)和有序集合(sorted sets)等类型。
1.2、特点
Redis 是完全开源的,遵守 BSD 协议(可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布),是一个高性能的 key-value 数据库。
Redis 与其他 key - value 缓存产品有以下三个特点:
- Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
- Redis支持数据的备份,即 master-slave 模式的数据备份。
1.3、优势
- 性能极高 – Redis 能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis 支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis 的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。
- 丰富的特性 – Redis 还支持 publish/subscribe, 通知, key 过期等等特性。
1.4、与其它 key-value 存储有什么不同?
- Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。在内存数据库方面的另一个优点是,相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。
二、Redis 安装
2.1、安装
先下载 Redis 的压缩包,Redis 6.2.6 is the latest stable version.
1 | tar -zxvf redis-6.2.6.tar.gz -C /usr # 解压文件 |
执行完 make 命令后,redis-6.2.6 的 src 目录下会出现编译后的 redis 服务程序 redis-server,还有用于测试的客户端程序 redis-cli:
下面启动 redis 服务:
1 | cd src |
注意这种方式启动 redis 使用的是默认配置。也可以通过启动参数告诉 redis 使用指定配置文件使用下面命令启动(源文件就当备份了,不要乱改)。
1 | ./redis-server MyRedis.conf |
默认配置文件是前台执行,需要手动配置成后台执行。
之后启动就是在后台启动了
来尝试连接一下 Redis 数据库
1 | [root@deflory src]# ./redis-cli -p 6379 |
2.2、redis-benchmark 测试性能
benchmark 可选参数如下所示:
序号 | 选项 | 描述 | 默认值 |
---|---|---|---|
1 | -h | 指定服务器主机名 | 127.0.0.1 |
2 | -p | 指定服务器端口 | 6379 |
3 | -s | 指定服务器 socket | |
4 | -c | 指定并发连接数 | 50 |
5 | -n | 指定请求数 | 10000 |
6 | -d | 以字节的形式指定 SET/GET 值的数据大小 | 2 |
7 | -k | 1=keep alive 0=reconnect | 1 |
8 | -r | SET/GET/INCR 使用随机 key, SADD 使用随机值 | |
9 | -P | 通过管道传输 |
1 |
10 | -q | 强制退出 redis。仅显示 query/sec 值 | |
11 | –csv | 以 CSV 格式输出 | |
12 | ***-l*(L 的小写字母)** | 生成循环,永久执行测试 | |
13 | -t | 仅运行以逗号分隔的测试命令列表。 | |
14 | ***-I*(i 的大写字母)** | Idle 模式。仅打开 N 个 idle 连接并等待。 |
1 | [root@deflory src]# ./redis-benchmark -h 127.0.0.1 -p 6379 -n 100000 -c 100 -q |
三、Redis 基本知识
3.1、数据库相关
Redis 默认有16个数据库,默认使用的是0号数据库。
select index 切换数据库
1 | 127.0.0.1:6379> select 1 # 切换1号数据库 |
dbsize 查看当前数据库大小
1 | 127.0.0.1:6379> dbsize # 查看数据库大小 |
set 添加键值对,get 获取键值对
1 | 127.0.0.1:6379> select 3 |
keys * 获取所有的键
1 | 127.0.0.1:6379[3]> keys * # 获取所有的键 |
flushdb 清空当前数据库
1 | 127.0.0.1:6379[3]> flushdb # 清空数据库,flushall清空所有数据库 |
flushall 清空所有数据库
1 | 127.0.0.1:6379[3]> set hello world |
3.2、Redis-Key
命令很多,这里只介绍常用的命令,如果之后碰到新的命令可以到官网查看。
del key 删除 key
1 | 127.0.0.1:6379> set name yqx |
exists key 是否存在该键值对,存在则返回1,不存在则返回0
1 | 127.0.0.1:6379> exists name # 是否存在键为name的键值对 |
expire key time 为该键值对设置有效时间,单位为秒
1 | 127.0.0.1:6379> expire name 5 # 设置name的有效时间为5秒 |
type key 查看键值对的存储类型
1 | 127.0.0.1:6379> set hello world |
rename key newkey 重命名键
1 | 127.0.0.1:6379> rename hello hi |
persist key 移除该键值对的过期时间,改为持久保存
1 | 127.0.0.1:6379> set name yqx |
四、五大基本类型
4.1、字符串(String)
getrange key start end 获取子串
1 | 127.0.0.1:6379> set key1 "hello,my name is LiHua!" |
getset key value 先获取值,再设置值
1 | 127.0.0.1:6379> getset key1 "it's new string" # 先获取,再设置值 |
strlen 获取字符串长度
1 | 127.0.0.1:6379> strlen key1 # 获取字符串长度 |
mset key1 value1 key2 value2… 批量设置键值对
1 | 127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 批量设置键值对 |
mget key1 key2…
1 | 127.0.0.1:6379> mget k1 k2 k3 # 批量获取键值对 |
setex key time value 如果存在该键值对,则修改值;不存在则添加该键值对。最后设置有效时间
1 | 127.0.0.1:6379> setex key1 10 value1 # set expire |
setnx key value 如果不存在该键值对,则添加;存在则不操作,不会修改值!
1 | 127.0.0.1:6379> setnx key2 value2 # set if not exists |
incr key 自增加1,必须是整数或是可以转换为整数的字符串!
1 | 127.0.0.1:6379> set num 1 |
incrby key increment 设置增量
1 | 127.0.0.1:6379> INCRBY num 10 |
decr key 自减1,必须是整数!
1 | 127.0.0.1:6379> decr num |
decrby key increment 设置减量
1 | 127.0.0.1:6379> decrby num 5 |
append key value 追加值到该键值对中
1 | 127.0.0.1:6379> set name yqx |
4.2、哈希(Hash)
Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
Redis 中每个 hash 可以存储 $2^{32}$ - 1 键值对(40多亿)。
hash 大部分的命令都是以 h 开头,表示这是 hash 的命令
hset hash k1 v1 k2 v2 同时设置一个或多个键值对到 hash中(hmset 和它的功能一模一样)
1 | # user:1:{ |
hget hash key 获取该 hash 中指定键的值
1 | 127.0.0.1:6379> hget user:1 name # 获取name值 |
hgetall key 获取该 hash 中所有的键值对
1 | 127.0.0.1:6379> hgetall user:1 |
hexists hash key 返回该 hash 中是否存在以 key 为键的键值对
1 | 127.0.0.1:6379> hexists user:1 name # 是否存在name键 |
hkeys hash 获取该 hash 中的所有键
1 | 127.0.0.1:6379> hkeys user:1 # 获取所有键 |
hvals hash 获取该 hash 中的所有值
1 | 127.0.0.1:6379> hvals user:1 # 获取所有值 |
hincrby hash key increment 为 hash 中键为 key 的值(整型)加一个增量 increment
1 | 127.0.0.1:6379> hincrby user:1 age 10 # 年龄+10 |
hincrby hash key increment 为 hash 中键为 key 的值加一个增量 increment
1 | 127.0.0.1:6379> hgetall user:1 |
hlen hash 获取该 hash 中的键值对数量
1 | 127.0.0.1:6379> hkeys user:1 |
hmget hash k1 k2… 获取所有给定字段的值
1 | 127.0.0.1:6379> hmget user:1 name weight |
hsetnx hash key value 只有在字段 key 不存在时,才会设置值
1 | 127.0.0.1:6379> hsetnx user:1 name deflory # set if not exist |
hscan hash cursor [MATCH pattern] [COUNT count] 迭代 hash 的键值对并进行匹配(count 默认为10)
1 | 127.0.0.1:6379> hscan user:1 0 match "*gh*" # 从下标0开始匹配键中包含gh的项 |
4.3、列表(List)
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
一个列表最多可以包含 $2^{32}$ - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
list 大部分的命令都是以 L 开头,表示这是 list 的命令
lpush list value 将数组从左边插入到 list 中,没有该列表,则会先创建再插入
1 | 127.0.0.1:6379> lpush list redis # 插入redis |
lpushx list value 将数组从左边插入到一个存在
的 list 中
1 | 127.0.0.1:6379> lpushx list2 yqx # pushx 到一个不存在的列表中 |
lrange list start end 查看 list 指定范围的数据(左闭右闭)
1 | 127.0.0.1:6379> lrange list 0 -1 # push和栈一样,最后放进来的在最上面 |
lindex list index 获取指定下标的元素
1 | 127.0.0.1:6379> lindex list 0 |
linsert list before|after pivot value 在列表元素的前面或后面插入元素
1 | 127.0.0.1:6379> linsert list before redis oracle # 在list列表中redis前面插入oracle,如果有多个redis,默认插入在第一个redis前 |
llen list 获取列表长度
1 | 127.0.0.1:6379> llen list # 查看list的长度 |
lpop list 弹出列表第一个元素
1 | 127.0.0.1:6379> lrange list 0 -1 |
lrem list count value 移出列表元素
1 | 127.0.0.1:6379> lrange list 0 -1 |
lset list index value 通过索引设置列表元素的值
1 | 127.0.0.1:6379> lset list 0 mybatis # 将第一个元素改为mybatis |
ltrim list start stop 只保留 list 在区间[start, stop]上的元素
1 | 127.0.0.1:6379> lrange list 0 -1 |
rpush list value 在列表末尾插入元素,同 lpush
1 | 127.0.0.1:6379> lrange list 0 -1 |
rpushx list value 在已存在列表末尾插入元素,同 rpushx
1 | 127.0.0.1:6379> rpushx name deflory |
rpop list 弹出列表最后一个元素
1 | 127.0.0.1:6379> lrange list 0 -1 |
rpoplpush source destination 将 source 中最后一个元素插入到 destination 最前面
1 | # 原列表 |
小结:
list 列表实际上是一个链表
如果移除了链表中的所有值,那么该链表不存在
1
2
3
4
5
6
7
8
9
10
11
12127.0.0.1:6379> lpush list v1 v2 # 创建测试列表
(integer) 2
127.0.0.1:6379> lrem list 1 v1 # 移除所有元素
(integer) 1
127.0.0.1:6379> lrem list 1 v2
(integer) 1
127.0.0.1:6379> lrange list 0 -1 # 查看数组内容
(empty array)
127.0.0.1:6379> lpushx list v3 # 无法给空数组lpushx元素!
(integer) 0
127.0.0.1:6379> lrange list 0 -1 # 还是空数组!
(empty array)在两遍插入或修改值时,效率最高!反之,操作中间元素时,效率较低!
如果只使用 lpush,rpop,即消息队列;只使用 lpush、lpop,即栈。
4.4、集合(Set)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
集合对象的编码可以是 intset 或者 hashtable。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 $2^{32} - 1$ (4294967295, 每个集合可存储40多亿个成员)。
set 大部分的命令都是以 s 开头,表示这是 set 的命令
sadd set value 添加 value 到 set 中
1 | 127.0.0.1:6379> sadd set redis mongodb # 可以同时添加多个元素 |
smembers set 查看该集合中的元素
1 | 127.0.0.1:6379> sadd set redis |
scard set 查看该集合数量
1 | 127.0.0.1:6379> scard set |
sdiff set1 set2 求差集(set1 - set2)
1 | 127.0.0.1:6379> smembers set |
sdiffstore destination set1 set2 将 set1 和 set2 的差集存储在 destination 中
1 | 127.0.0.1:6379> sdiffstore result set set2 # 将结果存储在result中 |
sinter set1 set2 求 set1 和 set2 的交集
1 | 127.0.0.1:6379> sinter set set2 |
sinnerstore destination set1 set2 将 set1 和 set2 的交集存储在 destination 中
1 | 127.0.0.1:6379> sinterstore result set set2 # 会将原先result中的内容覆盖 |
sunion set1 set2 求 set1 和 set2 的并集
1 | 127.0.0.1:6379> smembers set |
sunionstore destination set1 set2 将 set1 和 set2 的并集存储在 destination 中
1 | 127.0.0.1:6379> sunionstore result set set2 |
sismember set member 判断 member 是否存在于 set 中
1 | 127.0.0.1:6379> smembers result |
smove source destination member 将 member 从 source 中转移到destination 中
1 | 127.0.0.1:6379> smembers set |
spop set 随机弹出 set 中的某个元素
1 | 127.0.0.1:6379> smembers set2 |
srandmember set [count] 随即返回集合中的元素,默认count=1(不移除!)
1 | 127.0.0.1:6379> smembers set |
srem set value1 value2… 移除集合中的元素
1 | 127.0.0.1:6379> smembers set |
可用于微博,共同关注,共同爱好,推荐好友等……
4.5、有序集合(sorted set)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 $2^{32} - 1$ (4294967295, 每个集合可存储40多亿个成员)。
zadd zset score1 member1 score2 member2…. 向有序集合添加一个或多个成员,或者更新成员分数
1 | 127.0.0.1:6379> zadd salary 500 yqx 1000 sk 3000 deflory |
zcard zset 获取有序集合的元素个数
1 | 127.0.0.1:6379> zcard salary |
zrange zset min max byscore 根据分数从小到大返回数据
1 | 127.0.0.1:6379> zrange salary -inf +inf byscore |
zcount zset min max 查找指定范围内的元素个数
1 | 127.0.0.1:6379> zcount salary 10 1000 |
zscore zset member 获取指定某个成员的分数
1 | 127.0.0.1:6379> zscore salary yqx |
zincrby zset increment member 为指定成员添加增量
1 | 127.0.0.1:6379> zincrby salary 20 nojob |
zrank zset member 获取指定成员的索引
1 | 127.0.0.1:6379> zrank salary yqx |
zrangebyscore zset -inf +inf [withscores] 根据分数从小到大返回数据,可以选择是否连同分数一起返回
1 | 127.0.0.1:6379> zrangebyscore salary -inf +inf withscores |
zrevrange zset start end [withscores] 根据分数从大到小且索引在[start, end]中的数据,可以选择是否连同分数一起返回
1 | 127.0.0.1:6379> zrevrange salary 0 10 withscores |
zrevrangebyscore zset max min [withscores] 根据分数从大到小,范围在[min, max]返回数据,可以选择是否连同分数一起返回
1 | 127.0.0.1:6379> zrevrangebyscore salary +inf -inf withscores |
zrem zset member1 member2… 删除指定多个成员
1 | 127.0.0.1:6379> zrem salary nojob |
五、三种特殊数据类型
5.1、Geo
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
Redis GEO 操作方法有:
- geoadd:添加地理位置的坐标。
- geopos:获取地理位置的坐标。
- geodist:计算两个位置之间的距离。
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
- geohash:返回一个或多个位置对象的 geohash 值。
geoadd geo longitude1 latitude1 member1… 将多个经纬度和位置名称的信息添加到 geo 中
1 | 127.0.0.1:6379> zrem salary nojob |
geopos geo member1 member2 获取多个成员的地理位置
1 | 127.0.0.1:6379> geopos geo Palermo # 获取Palermo的地理位置 |
geodist geo member1 member2 [m|km|ft|mi] 获取两个成员间的距离
1 | 127.0.0.1:6379> geodist geo Palermo Catania # 计算Palermo与Catania的距离 |
geodius geo longitude latitude radius m|km [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key] 获取 geo 在以给定经纬度为圆心,radius 为半径的圆中的成员集合
- m :米,默认单位。
- km :千米。
- mi :英里。
- ft :英尺。
- WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
- WITHCOORD: 将位置元素的经度和维度也一并返回。
- WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
- COUNT 限定返回的记录数。
- ASC: 查找结果根据距离从近到远排序。
- DESC: 查找结果根据从远到近排序。
1 | 127.0.0.1:6379> georadius geo 15 37 200 km |
georadiusbymember 语法同上,但是以成员为圆心
1 | 127.0.0.1:6379> geoadd geo 13.583333 37.316667 "Agrigento" 13.361389 38.115556 "Palermo" 15.087269 37.502669 "Catania" |
geohash member1 member2 获取多个成员的 geohash 值
1 | 127.0.0.1:6379> geohash geo Palermo Catania |
因为 geo 底层是使用 zset 实现的,因此我们可以使用 zset 的方法在 geo 上!!!
1 | 127.0.0.1:6379> zrange geo 0 -1 # 使用zrange获取geo中所有的成员 |
5.2、HyperLogLog
Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
那么什么是基数呢?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。
pfadd pf v1 v2 v3… 添加多个值到 HyperLogLog 中
1 | 127.0.0.1:6379> pfadd pf1 a b c d e f g |
pfcount pf1 [pf2…] 返回给定 HyperLogLog 的基数估算值
1 | 127.0.0.1:6379> pfcount pf1 # pf1基数为7 |
pfmerge destkey sourcekey [sourcekey…]
1 | 127.0.0.1:6379> pfmerge pf1 pf2 # 将pf1和pf2合并的结果赋给pf1 |
5.3、Bitmaps
Bitmaps 位图,是一种数据结构,通过操作二进制位来进行记录,只有0和1两个状态。
如果要统计一年的打卡情况,365天 = 365bit,约等于46B。
setbit bitmap index value 设值
1 | 127.0.0.1:6379> setbit bitmap 0 2 # 位图只有0和1两种状态 |
bitcount bitmap 统计值
1 | 127.0.0.1:6379> bitcount bitmap |
getbit 获取指定下标的值
1 | 127.0.0.1:6379> getbit bitmap 1 |
六、事务
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
6.1、开启事务
以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令:
1 | 127.0.0.1:6379> multi # 开启事务 |
当事务中出现编译错误时,这其中所有的操作都不会被执行
1 | 127.0.0.1:6379> multi |
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
1 | 127.0.0.1:6379> multi |
discard 取消事务
1 | 127.0.0.1:6379> multi |
6.2、watch 监听
悲观锁:
- 很悲观,认为什么时候都会出问题,无论做什么都会加锁!
乐观锁:
- 很乐观,认为什么时候都不会出问题,所以不会上锁!更新数据的时候会进行判断,在此前是否有人修改过这个数据。
watch key1 key2… 监听一个或多个 key ,如果在事务执行之前,这个 key 被其它命令所改动,那么事务将被打断,由此实现乐观锁的效果。
1 | 127.0.0.1:6379> set money 100 |
在启动事务的同时,我们新连接一个 redis 客户端,修改 money 的值。
1 | 127.0.0.1:6379> set money 1000 |
此时再回到第一个客户端,完善事务。
1 | 127.0.0.1:6379> multi |
与 watch 相对的,有一个命令叫做 unwatch,用于取消对所有 key 的监视
1 | 127.0.0.1:6379> watch money |
七、Jedis
7.1、配置
如果想要在 Java 中连接 Redis,就需要使用 Jedis 这个 jar 包,以下是它的 Maven 依赖。
1 | <dependency> |
当然,在连接之前还需要修改一下阿里云中 Redis 的配置文件,如下。
注释 bind 127.0.0.1
protect-mode 改为 no
之后重启 Redis 即可。
停止所有客户端
1
./redis-cli shutdown
启动 Redis
1
redis-server ../MyRedis.conf
测试连接,成功输出 PONG
!
7.2、Jedis 使用
测试键
1 |
|
测试 String
1 | @Test |
测试 Set
1 |
|
太多了,后面的就不写了,和 Redis 的使用差不多!
事务
1 |
|
1 |
|
八、Redis 配置
Redis 单位不区分大小写
1 | # it in the usual form of 1k 5GB 4M and so forth: |
主要配置字段说明
序号 | 配置项 | 说明 |
---|---|---|
1 | daemonize no |
Redis 默认不是以守护进程的方式运行,可以通过该配置项修改,使用 yes 启用守护进程(Windows 不支持守护线程的配置为 no ) |
2 | pidfile /var/run/redis.pid |
当 Redis 以守护进程方式运行时,Redis 默认会把 pid 写入 /var/run/redis.pid 文件,可以通过 pidfile 指定 |
3 | port 6379 |
指定 Redis 监听端口,默认端口为 6379,作者在自己的一篇博文中解释了为什么选用 6379 作为默认端口,因为 6379 在手机按键上 MERZ 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字 |
4 | bind 127.0.0.1 |
绑定的主机地址 |
5 | timeout 300 |
当客户端闲置多长秒后关闭连接,如果指定为 0 ,表示关闭该功能 |
6 | loglevel notice |
指定日志记录级别,Redis 总共支持四个级别:debug、verbose、notice、warning,默认为 notice |
7 | logfile stdout |
日志记录方式,默认为标准输出,如果配置 Redis 为守护进程方式运行,而这里又配置为日志记录方式为标准输出,则日志将会发送给 /dev/null |
8 | databases 16 |
设置数据库的数量,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id |
9 | save <seconds> <changes> Redis 默认配置文件中提供了三个条件:save 900 1save 300 10save 60 10000分别表示 900 秒(15 分钟)内有 1 个更改,300 秒(5 分钟)内有 10 个更改以及 60 秒内有 10000 个更改。 |
指定在多长时间内,有多少次更新操作,就将数据同步到数据文件,可以多个条件配合 |
10 | rdbcompression yes |
指定存储至本地数据库时是否压缩数据,默认为 yes,Redis 采用 LZF 压缩,如果为了节省 CPU 时间,可以关闭该选项,但会导致数据库文件变的巨大 |
11 | dbfilename dump.rdb |
指定本地数据库文件名,默认值为 dump.rdb |
12 | dir ./ |
指定本地数据库存放目录 |
13 | slaveof <masterip> <masterport> |
设置当本机为 slave 服务时,设置 master 服务的 IP 地址及端口,在 Redis 启动时,它会自动从 master 进行数据同步 |
14 | masterauth <master-password> |
当 master 服务设置了密码保护时,slav 服务连接 master 的密码 |
15 | requirepass foobared |
设置 Redis 连接密码,如果配置了连接密码,客户端在连接 Redis 时需要通过 AUTH |
16 | maxclients 128 |
设置同一时间最大客户端连接数,默认无限制,Redis 可以同时打开的客户端连接数为 Redis 进程可以打开的最大文件描述符数,如果设置 maxclients 0,表示不作限制。当客户端连接数到达限制时,Redis 会关闭新的连接并向客户端返回 max number of clients reached 错误信息 |
17 | maxmemory <bytes> |
指定 Redis 最大内存限制,Redis 在启动时会把数据加载到内存中,达到最大内存后,Redis 会先尝试清除已到期或即将到期的 Key,当此方法处理 后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis 新的 vm 机制,会把 Key 存放内存,Value 会存放在 swap 区 |
18 | appendonly no |
指定是否在每次更新操作后进行日志记录,Redis 在默认情况下是异步的把数据写入磁盘,如果不开启,可能会在断电时导致一段时间内的数据丢失。因为 redis 本身同步数据文件是按上面 save 条件来同步的,所以有的数据会在一段时间内只存在于内存中。默认为 no |
19 | appendfilename appendonly.aof |
指定更新日志文件名,默认为 appendonly.aof |
20 | appendfsync everysec |
指定更新日志条件,共有 3 个可选值:no:表示等操作系统进行数据缓存同步到磁盘(快)always:表示每次更新操作后手动调用 fsync() 将数据写到磁盘(慢,安全)everysec:表示每秒同步一次(折中,默认值) |
21 | vm-enabled no |
指定是否启用虚拟内存机制,默认值为 no,简单的介绍一下,VM 机制将数据分页存放,由 Redis 将访问量较少的页即冷数据 swap 到磁盘上,访问多的页面由磁盘自动换出到内存中(在后面的文章我会仔细分析 Redis 的 VM 机制) |
22 | vm-swap-file /tmp/redis.swap |
虚拟内存文件路径,默认值为 /tmp/redis.swap,不可多个 Redis 实例共享 |
23 | vm-max-memory 0 |
将所有大于 vm-max-memory 的数据存入虚拟内存,无论 vm-max-memory 设置多小,所有索引数据都是内存存储的(Redis 的索引数据 就是 keys),也就是说,当 vm-max-memory 设置为 0 的时候,其实是所有 value 都存在于磁盘。默认值为 0 |
24 | vm-page-size 32 |
Redis swap 文件分成了很多的 page,一个对象可以保存在多个 page 上面,但一个 page 上不能被多个对象共享,vm-page-size 是要根据存储的 数据大小来设定的,作者建议如果存储很多小对象,page 大小最好设置为 32 或者 64bytes;如果存储很大大对象,则可以使用更大的 page,如果不确定,就使用默认值 |
25 | vm-pages 134217728 |
设置 swap 文件中的 page 数量,由于页表(一种表示页面空闲或使用的 bitmap)是在放在内存中的,,在磁盘上每 8 个 pages 将消耗 1byte 的内存。 |
26 | vm-max-threads 4 |
设置访问swap文件的线程数,最好不要超过机器的核数,如果设置为0,那么所有对swap文件的操作都是串行的,可能会造成比较长时间的延迟。默认值为4 |
27 | glueoutputbuf yes |
设置在向客户端应答时,是否把较小的包合并为一个包发送,默认为开启 |
28 | hash-max-zipmap-entries 64 hash-max-zipmap-value 512 |
指定在超过一定的数量或者最大的元素超过某一临界值时,采用一种特殊的哈希算法 |
29 | activerehashing yes |
指定是否激活重置哈希,默认为开启(后面在介绍 Redis 的哈希算法时具体介绍) |
30 | include /path/to/local.conf |
指定包含其它的配置文件,可以在同一主机上多个Redis实例之间使用同一份配置文件,而同时各个实例又拥有自己的特定配置文件 |
九、持久化
由于 Redis 的数据都是保存在内存中的,因此如果哪儿天服务器宕机了,那么所有的数据就都消失了,这是绝对不会被允许的!!!因此需要持久化到硬盘中。
9.1、RDB 持久化
RDB 持久化就是每过一个周期,就将内存中的数据全都遍历一遍并写入到文件中,而且为了节省空间,定义了一个二进制的格式进行存储,将数据一条一条连在一起,生成了一个 RDB 文件。
那么该如何设置保存周期呢?在 Redis 的 config 文件中,有着如下三行内容,可以按需进行修改。多个条件可以组合使用,只要下面其中一个条件满足,Redis 就会去进行备份。
1 | save 900 1 # 900秒(15分钟)内有1个写入 |
那么如果在保存数据库文件的时候,又有新的请求该怎么办呢?很自然而然地,就能想到可以 fork 出一个子线程来做备份数据库的事情。
但是啊,周期性地备份文件有一个致命性的问题,那就是周期时间——竟然是分钟级别!!!如果出现停机等问题,还是会损失这一分钟内的数据。诚然,你也可以设置成每秒钟备份一次,那效率就太低下了。
由此,我们引出了新的概念—— AOF。
9.2、AOF 持久化
AOF 全称 Append Only File(只追加文件),类似于 mysql 的二进制日志 binlog。其原理就是记录数据更改的操作(不包括查询),而不是记录数据本身。毕竟只要记录数据在这一段时间内是如何修改的,那之后也能通过执行这个 AOF 文件还原回去。
那么 AOF 应该多久写一次文件呢?
肯定不能每执行一条修改命令就记录到文件中,IO 操作十分昂贵,这回严重拖垮数据库性能!因此需要准备一个缓冲区,将要记录的命令先临时保存在这里,再择机写入文件,我把这个临时缓冲区叫做 aof_buf。
具体如何设置刷新周期可以在 Redis 的配置文件中进行修改。
1 | appendfsync always|everysec|no |
AOF 重写
但是正如其名,只追加文件只会追加数据,这就会导致随着时间的推移,这个 AOF 备份文件会越来越大!!!不仅非常占硬盘空间,复制移动,加载分析等操作都异常麻烦耗时。
这就需要对 AOF 文件进行压缩
,也就是 AOF 重写。
观察以下命令,是不是非常冗余!
1 | set name yqx |
将每次修改操作都记录下来导致文件过大,而且数据改来改去,很多中间状态根本没有用,只需要记录最终的状态即可,这就是 AOF 重写的思想。
因此上述命令只需要一行即可记录完毕。
1 | set name DreamFragrance |
同 RDB 一样,IO 操作总是代价昂贵的,因此要 fork 一个子进程来重写文件。
但是,这样就真的没有问题了吗?
如果在重写 AOF 文件的时候,新的数据被修改了,那岂不是会出现数据库数据和重写内容不一致的情况吗?
于是,继之前的 aof_buf,Redis 又准备了一个新的缓冲区—— AOF 重写缓冲区。
从创建重写 AOF 子进程的那个一刻起,Redis 将后面来的写入命令都复制到这个重写缓冲区中,等到子进程重写 AOF 文件结束以后,再将这个缓冲区的文件写入到新的 AOF 文件中。
最后再将这个崭新的 AOF 文件替换掉原先那个冗余的 AOF 文件,整个重写过程就算大功告成了!
9.3、详解 RDB 和 AOF
Redis 持久化:
Redis 提供了多种不同级别的持久化方式:一种是 RDB,另一种是 AOF。
RDB 持久化可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot)。
AOF 持久化记录服务器执行的所有写操作命令,并在服务器启动时,通过重新执行这些命令来还原数据集。 AOF 文件中的命令全部以 Redis 协议的格式来保存,新命令会被追加到文件的末尾。 Redis 还可以在后台对 AOF 文件进行重写(rewrite),使得 AOF 文件的体积不会超出保存数据集状态所需的实际大小。Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时, 它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。你甚至可以关闭持久化功能,让数据只在服务器运行时存在。
RDB 的优点:
RDB 是一个非常紧凑(compact)的文件,它保存了 Redis 在某个时间点上的数据集。 这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。 这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心,或者亚马逊 S3 中。RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB 的缺点:
如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。每次保存 RDB 的时候,Redis 都要 fork 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
AOF 的优点:
使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题,至于是如何修复的嘛,只是把有问题的命令删去而已。既然解决不了问题,那就解决掉提出问题的人。
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF 的缺点:
对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
RDB 和 AOF 该如何选择?
一般来说,如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能。如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。有很多用户都只使用 AOF 持久化, 但我们并不推荐这种方式: 因为定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快, 除此之外, 使用 RDB 还可以避免之前提到的 AOF 程序的 bug 。因为以上提到的种种原因, 未来我们可能会将 AOF 和 RDB 整合成单个持久化模型。 (这是一个长期计划。)
RDB 快照:
在默认情况下, Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE 或者 BGSAVE , 手动让 Redis 进行数据集保存操作。比如说, 以下设置会让 Redis 在满足“ 60 秒内有至少有 1000 个键被改动”这一条件时, 自动保存一次数据集:
1 | save 60 1000 |
这种持久化方式被称为快照(snapshot)。
快照的运作方式:
当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
Redis 调用 fork() ,同时拥有父进程和子进程。
子进程将数据集写入到一个临时 RDB 文件中。
当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。
只进行追加操作的文件(append-only file,AOF)
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。尽管对于某些程序来说, 数据的耐久性并不是最重要的考虑因素, 但是对于那些追求完全耐久能力(full durability)的程序来说, 快照功能就不太适用了。
从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
你可以通过修改配置文件来打开 AOF 功能:
appendonly yes
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
快照的运作方式:
当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
Redis 调用 fork() ,同时拥有父进程和子进程。
子进程将数据集写入到一个临时 RDB 文件中。
当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。
只进行追加操作的文件(append-only file,AOF)
快照功能并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。尽管对于某些程序来说, 数据的耐久性并不是最重要的考虑因素, 但是对于那些追求完全耐久能力(full durability)的程序来说, 快照功能就不太适用了。
从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。
你可以通过修改配置文件来打开 AOF 功能:
appendonly yes
从现在开始, 每当 Redis 执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到 AOF 文件的末尾。
这样的话, 当 Redis 重新启时, 程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的。
AOF 重写:
因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
如果 AOF 出错了:
服务器可能在程序正在对 AOF 文件进行写入时停机, 如果停机造成了 AOF 文件出错(corrupt), 那么 Redis 在重启时会拒绝载入这个 AOF 文件, 从而确保数据的一致性不会被破坏。
当发生这种情况时, 可以用以下方法来修复出错的 AOF 文件:
为现有的 AOF 文件创建一个备份。
使用 Redis 附带的 redis-check-aof 程序,对原来的 AOF 文件进行修复。
1
redis-check-aof --fix
使用 diff -u 对比修复后的 AOF 文件和原始 AOF 文件的备份,查看两个文件之间的不同之处(可选)。
重启 Redis 服务器,等待服务器载入修复后的 AOF 文件,并进行数据恢复。
AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制。
AOF 重写的执行步骤:
Redis 执行 fork() ,现在同时拥有父进程和子进程。
子进程开始将新 AOF 文件的内容写入到临时文件。对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
RDB 和 AOF 之间的相互作用:
在版本号大于等于 2.4 的 Redis 中, BGSAVE 执行的过程中, 不可以执行 BGREWRITEAOF 。 反过来说, 在 BGREWRITEAOF 执行的过程中, 也不可以执行 BGSAVE 。
这可以防止两个 Redis 后台进程同时对磁盘进行大量的 I/O 操作。
如果 BGSAVE 正在执行, 并且用户显示地调用 BGREWRITEAOF 命令, 那么服务器将向用户回复一个 OK 状态, 并告知用户, BGREWRITEAOF 已经被预定执行: 一旦 BGSAVE 执行完毕, BGREWRITEAOF 就会正式开始。当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。
备份 Redis 数据:
Redis 对于数据备份是非常友好的, 因为你可以在服务器运行的时候对 RDB 文件进行复制: RDB 文件一旦被创建, 就不会进行任何修改。 当服务器要创建一个新的 RDB 文件时, 它先将文件的内容保存在一个临时文件里面, 当临时文件写入完毕时, 程序才使用 原子地用临时文件替换原来的 RDB 文件。这也就是说, 无论何时, 复制 RDB 文件都是绝对安全的。
十、发布订阅
10.1、什么是发布订阅?
Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
要想在 Redis 中模拟发布订阅的过程,需要开启两个 redis-cli 客户端。
1 | # 订阅channel1频道 |
1 | # 频道channel1发布消息,“hello,it's channel1” |
1 | # 用户收到来自channel1的消息 |
10.2、发布订阅常用命令
序号 | 命令 | 描述 |
---|---|---|
1 | PSUBSCRIBE pattern [pattern …] | 订阅一个或多个符合给定模式的频道。 |
2 | PUBSUB subcommand [argument [argument …]] | 查看订阅与发布系统状态。 |
3 | PUBLISH channel message | 将信息发送到指定的频道。 |
4 | PUNSUBSCRIBE [pattern [pattern …]] | 退订所有给定模式的频道。 |
5 | SUBSCRIBE channel [channel …] | 订阅给定的一个或多个频道的信息。 |
6 | UNSUBSCRIBE [channel [channel …]] | 指退订给定的频道。 |
10.3、原理
Redis 是使用 C 实现的,通过分析 Redis 源码里的 public.c 文件,了解发布和订阅机制的底层实现,借此加深对 Redis 的理解。
Redis 通过 PUBLISH、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 channel,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。
通过 PUBLISH 命令向订阅者发送信息,redis-server 会使用给定的频道作为键,在它所维护的 channel 字典中查找订阅了这个频道所有的客户端链表,借此将消息发布给所有的订阅者。
Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subcribe),在Redis中,你可以设定对某一个 key 进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会受到相应的信息。这一功能最明显的用法就是用作实时消息系统,例如群聊等。
十一、主从复制
11.1、简介
主从复制是为了达成高可用
- 为了避免单点Redis服务器故障,准备多台服务器,互相连通。将数据复制多个副本保存在不同的服
务器上,连接在一起,并保证数据是同步的。 - 即使有其中一台服务器宕机,其他服务器依然可以继续提供服务,实现Redis的高可用,同时实现数据冗余备份。
- 提供数据方:master
- 写数据
- 执行写操作时,将出现变化的数据自动同步到slave
- 读数据(可忽略)
- 一个master可以拥有多个slave
- 接收数据方:slave
- 读数据
- 写数据(禁止)
- 一个slave只对应一个master
- 需要解决的问题
- 数据同步
- 核心工作
- master的数据复制到slave中
11.2、作用
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master/leader),后者称为从节点(slave/follower) ; 数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。
默认情况下,每台Redis服务器都是主节点 ;
且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
其作用有以下五点:
- 读写分离:主节点写,从节点读,提高服务器的读写负载能力
- 数据冗余︰主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复︰当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复 ; 实际上是一种服务的冗余。
- 负载均衡︰在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载 ; 尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用(集群)基石︰除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
11.3、环境搭建
这里我们实现的是一主二从
,先准备三份配置文件,因为是根据配置文件创建服务的。
1 | [root@deflory redis-6.2.6]# mkdir master-slaver-config |
每个配置文件都需要修改一下四个地方。
1 | port 6379 # 端口号 |
开启三个服务器端。
1 | [root@deflory src]# ./redis-server ../master-slaver-config/redis79.config |
查看后台进程。
1 | [root@deflory src]# ps -ef | grep redis |
11.4、主从复制实战
连接服务器端,使用 info replication
来查看当前主从情况。
由于服务器默认都是以主机启动的,因此目前“三台机器”都是主机。
1 | 127.0.0.1:6379> info replication |
通过 slaveof host port
来连接主机,如果主机有设置密码,则会要求进行 auth 验证。
1 | 127.0.0.1:6380> slaveof 127.0.0.1 6379 |
主机的主从信息。
1 | 127.0.0.1:6379> info replication |
运行测试
在主机写入name:deflory后,从机也能读取到相应内容。
1 | # 主机写入 |
但是,从机只能用来读取数据,而不能修改数据,只读!
1 | 127.0.0.1:6380> set age 10 |
那么如果此时,我们的主机宕机了会发生什么呢?
1 | # 主机宕机 |
接下来我们换个角度,从机宕机呢?
1 | # 从机1宕机 |
注:配置文件主从复制设置
在5.0.0之后,slaveof 逐渐被 replicaof 所取代,因此在配置文件中也不叫 slaveof 了,找了我好久!!!
1 | # 设置主机ip和端口 |
不过,就正常而言,如果主机宕机了,剩下的从机是无法进行写入操作的,因此一个比较好的解决方案,就是从剩下的从机中,再选出一个主机。
11.5、链式主从模式
即如下设计结构,中间的服务器既是前面的从服务器,也是后面的主服务器。
1 | # 服务器1,角色:master,有一个slaver |
测试,主服务器写入数据,后面两个从服务器都能读取数据。
1 | 127.0.0.1:6379> set name yqx |
但当主服务器宕机后,第二台服务器依旧不能写入数据。
1 | 127.0.0.1:6379> shutdown |
需要使用 replicaof no one
,来将自己提升为主服务器。
1 | 127.0.0.1:6380> replicaof no one |
但是这种类似链式的主从复制模型,有着三个非常大的问题!!!
- 如果中间的某台服务器宕机了,那么后面的服务器就无法再接受到更新的数据了!
- 由于数据是像链式一样进行更新,处于末尾的从服务器会和前面的服务器的更新时间差距很大!
- 每次都要手动的选择主服务器,效率非常低!
11.6、主从复制运行原理
Slave 启动成功连接到 master 后会发送一个 sync 同步指令
Master 接到指令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave 并完成一次完全同步。
全量复制:slave 在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令一次传给 slave ,完成同步。
只要是重新连接到 Master,就会进行一次完全同步(全量复制 + 增量复制)。
增量复制实现
- 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
- 主服务器的复制积压缓冲区(replication backlog)
- 服务器运行ID(run ID)
复制偏移量
执行复制的双方——主服务器和从服务器分别维护着一个对应的复制偏移量。
- 主服务器每次向从服务器传播 N 个字节的数据时,就将自己的复制偏移量的值加上 N。
- 从服务器每次收到主服务器传来的 N 个字节的数据时,也将自己的复制偏移量的值加上 N。
通过对比主从服务器的复制偏移量,程序很容易就能知道主从服务器是否处于同一个状态。
- 如果主从服务器处于一致的状态,那么他们的复制偏移量总是相同的。
- 相反,如果主从服务器的复制偏移量不相同,那么他们就并未处于一致的状态
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size)先进先出的队列,默认大小为1MB。固定长度的先入先出队列不能随着元素的增加和减少而动态的调整长度,固定长度先进先出队列的长度是固定的,当入队元素的数量大于队列长度时,最先入队的元素会被弹出,而新元素会被放入队列。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会降所有写命令入队到复制积压缓冲区里面,如图所示:
重连同步流程:
- 当从服务器A(上图为例)断线重连后,向主服务器发送PSYNC命令,报告自己的偏移量为10086。
- 主服务器收到命令后,检查偏移量10086之后的数据是否存在于复制积压缓冲区里。存在,向服务器发送+CONTINUE回复,表示数据同步将以部分重同步模式进行。(不存在,主服务器将对从服务器执行完整重同步操作)。
- 主服务器将复制积压缓冲区10086之后的所有数据发送给从服务器。
- 从服务器接收缺失数据,回到与主服务器一致的状态。
因此,主服务器的复制积压缓冲区会存放近期传播过的写命令,并且复制积压缓冲区会为队列中的每个字节记录响应的复制偏移量。
当从服务器重新连接上主服务器时,从服务器会通过 PSYNC 命令将自己的复制偏移量 offset 发送给主服务器,主服务器会根据从服务器的复制偏移量来决定对从服务器执行何种同步操作。
- 如果偏移量 offset 之后的数据仍然处于复制积压缓冲区里,那么主服务器将会对从服务器执行部分同步操作(增量复制)。
- 如果偏移量 offset 之后的数据已经不存在于复制积压缓冲区中,那么主服务器将会对从服务器执行完整同步操作(全量复制)。
服务器运行 ID
除了复制偏移量和复制积压缓冲区之外,是部分重同步还需要用到服务器 ID:
- 每个 Redis 服务器,不论是主服务器还是从服务器,都会有自己的运行 ID
- 运行 ID 在服务器启动时自动生成,由40个随机的十六进制地府组成
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行 ID 传递给从服务器,从服务器则会将这个运行 ID 保存起来。当从服务器断线并重新连接到主服务器时,从服务器就会比对之前连接的主服务器的运行 ID:
- 如果从服务器保存的运行 ID 和当前连接的主服务器的运行 ID 一致,说明从服务器现在与之前连接的主服务器都是一个服务器,主服务器可以继续尝试执行部分重同步的操作。
- 如果从服务器保存的运行 ID 和当前连接的主服务器的运行 ID 不一致,说明在从服务器断连的这一段时间里,已经更换了一个主服务器,现在的主服务器需要对从服务器执行完整的重同步操作。
PSYNC 命令的实现
PSYNC 命令的调用方式有两种:
- 如果从服务器之前从来没有复制过任何主服务器,或者之前没有执行过
slaveof no one
命令,那么从服务器则会向主服务器发送 PSYNC? -1命令,主动请求服务器进行完整重同步 - 相反的,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时,将向主服务器发送 PSYNC runid offset 命令:其中 runid 是上一次复制的主服务器的运行 ID,而 offset 则是从服务器当前的复制偏移量,接收到这个命令的主服务器会根据这两个参数来判断对该从服务器执行哪儿中同步操作。
- 如果主服务器返回
+FULLRESYNC <runid> <offset>
,那么则表示主服务器将与从服务器进行完整重同步: 其中 runid 是当前主服务器的运行 ID,从服务器需将这个 ID 保存起来,再下一次发送 PASYNC 时使用;而 offset 则是当前主服务器的复制偏移量,而从服务器则会将这个值作为自己的初始化复制偏移量。 - 如果主服务器返回
+Continue
,那么表示主服务器将与从服务器进行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来即可! - 如果主服务器返回
-ERR
,表示当前的主服务器版本低于2.8,它识别不了 PSYNC 命令,从服务器将向主服务器发送 SYNC 命令,并且与主服务器执行完全重同步操作。
- 如果主服务器返回
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率。像主服务器发送命令:REPLCONF ACK <offset>
,其中 offset
是从服务器当前的复制偏移量。
作用:
检测 主从服务器的网络连接状态
辅助实现 min-slaves 选项,min-slaves-to-write 和 min-slaves-max-lag 都是为了防止主服务器在不安全的情况下执行写命令。
1
2min-slaves-to-write 3
min-slaves-max-lag 10在从服务器数量小于3个,或者三个从服务器的延迟值(最后一次心跳检测距离现在过了多少秒)都大于等于十秒时,主服务器将拒绝执行写命令。
检测命令丢失。偏移量不一致时,会进行补发缺失数据的操作。
11.7、哨兵模式(Sentinel)【推荐】
上述主从切换技术的方法,主要是通过在主服务器宕机的时候,人工手动地将某台从服务器切换成主服务器,再将其余的从服务器都配置成它的 slaver。这整一个过程需要人工干预,费时又费力,还会造成一段时间内的服务不可用。不推荐!
因此就有了哨兵模式。
什么是哨兵模式?
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
这里哨兵有三个作用:
- 监控(Monitoring):Sentinel 会不断检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification):当被监控的某个 Redis 服务器出现问题时,Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automotic failover):当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其它从服务器也改为新的主服务器的从服务器。而当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址,使得集群可以使用新主服务器来代替失效的服务器。
诶,可是如果这个哨兵进程宕机了,整个系统不就崩盘了吗?
为此,我们可以使用多个哨兵进行监控。各个哨兵之间也会互相监控,这样就形成了多哨兵模式。
这里简单用文字描述一下故障切换(failover)的过程。
假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵1主观地认为主服务器不可用,这个现象被称为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就行进行一次投票,投票的结果由一个哨兵发起,进行 failover 操作。切换成功后,就会通过发布订阅的方式,让各个哨兵把自己监控的从服务器实现切换主机,在个过程成为客观下线,这样对于客户端而言,一切都是透明的。
每个 Sentinel 都需要定期执行的任务
- 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。 一个有效回复可以是: +PONG 、 -LOADING 或者 -MASTERDOWN 。
- 如果一个主服务器被标记为主观下线, 那么正在监视这个主服务器的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
- 如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
- 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有主服务器和从服务器发送 INFO 命令。 当一个主服务器被 Sentinel 标记为客观下线时, Sentinel 向下线主服务器的所有从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
- 当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除。
自动发现 Sentinel 和从服务器
一个 Sentinel 可以与其他多个 Sentinel 进行连接, 各个 Sentinel 之间可以互相检查对方的可用性, 并进行信息交换。
你无须为运行的每个 Sentinel 分别设置其他 Sentinel 的地址, 因为 Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel , 这一功能是通过向频道 sentinel:hello 发送信息来实现的。
与此类似, 你也不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过询问主服务器来获得所有从服务器的信息。
- 每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 sentinel:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。
- 每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 sentinel:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。
- Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
- 在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。
故障转移
一次故障转移操作由以下步骤组成:
- 发现主服务器已经进入客观下线状态。
- 对我们的当前纪元进行自增(详情请参考 Raft leader election ), 并尝试在这个纪元中当选。
- 如果当选失败, 那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选。 如果当选成功, 那么执行以下步骤。
- 选出一个从服务器,并将它升级为主服务器。
- 向被选中的从服务器发送
SLAVEOF NO ONE
命令,让它转变为主服务器。 - 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
- 向已下线主服务器的从服务器发送 SLAVEOF 命令, 让它们去跟随新的主服务器。
- 当所有从服务器都已经开始跟随新的主服务器时, 领头 Sentinel 终止这次故障迁移操作。
每当一个 Redis 实例被重新配置(reconfigured) —— 无论是被设置成主服务器、从服务器、又或者被设置成其他主服务器的从服务器 —— Sentinel 都会向被重新配置的实例发送一个 CONFIG REWRITE 命令, 从而确保这些配置会持久化在硬盘里。
Sentinel 使用以下规则来选择新的主服务器:
- 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
- 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
- 在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。
Sentinel 自动故障迁移的一致性特质
Sentinel 自动故障迁移使用 Raft 算法来选举领头(leader) Sentinel , 从而确保在一个给定的纪元(epoch)里, 只有一个领头产生。
这表示在同一个纪元中, 不会有两个 Sentinel 同时被选中为领头, 并且各个 Sentinel 在同一个纪元中只会对一个领头进行投票。
更高的配置纪元总是优于较低的纪元, 因此每个 Sentinel 都会主动使用更新的纪元来代替自己的配置。
简单来说, 我们可以将 Sentinel 配置看作是一个带有版本号的状态。 一个状态会以最后写入者胜出(last-write-wins)的方式(也即是,最新的配置总是胜出)传播至所有其他 Sentinel 。
举个例子, 当出现网络分割(network partitions)时, 一个 Sentinel 可能会包含了较旧的配置, 而当这个 Sentinel 接到其他 Sentinel 发来的版本更新的配置时, Sentinel 就会对自己的配置进行更新。
如果要在网络分割出现的情况下仍然保持一致性, 那么应该使用 min-slaves-to-write 选项, 让主服务器在连接的从实例少于给定数量时停止执行写操作, 与此同时, 应该在每个运行 Redis 主服务器或从服务器的机器上运行 Redis Sentinel 进程。
Sentinel 状态的持久化
Sentinel 的状态会被持久化在 Sentinel 配置文件里面。
每当 Sentinel 接收到一个新的配置, 或者当领头 Sentinel 为主服务器创建一个新的配置时, 这个配置会与配置纪元一起被保存到磁盘里面。
这意味着停止和重启 Sentinel 进程都是安全的。
Sentinel 在非故障迁移的情况下对实例进行重新配置
即使没有自动故障迁移操作在进行, Sentinel 总会尝试将当前的配置设置到被监视的实例上面。 特别是:
- 根据当前的配置, 如果一个从服务器被宣告为主服务器, 那么它会代替原有的主服务器, 成为新的主服务器, 并且成为原有主服务器的所有从服务器的复制对象。 那些连接了错误主服务器的从服务器会被重新配置, 使得这些从服务器会去复制正确的主服务器。
不过, 在以上这些条件满足之后, Sentinel 在对实例进行重新配置之前仍然会等待一段足够长的时间, 确保可以接收到其他 Sentinel 发来的配置更新, 从而避免自身因为保存了过期的配置而对实例进行了不必要的重新配置。
TILT 模式
Redis Sentinel 严重依赖计算机的时间功能: 比如说, 为了判断一个实例是否可用, Sentinel 会记录这个实例最后一次相应 PING 命令的时间, 并将这个时间和当前时间进行对比, 从而知道这个实例有多长时间没有和 Sentinel 进行任何成功通讯。
不过, 一旦计算机的时间功能出现故障, 或者计算机非常忙碌, 又或者进程因为某些原因而被阻塞时, Sentinel 可能也会跟着出现故障。
TILT 模式是一种特殊的保护模式: 当 Sentinel 发现系统有些不对劲时, Sentinel 就会进入 TILT 模式。
因为 Sentinel 的时间中断器默认每秒执行 10 次, 所以我们预期时间中断器的两次执行之间的间隔为 100 毫秒左右。 Sentinel 的做法是, 记录上一次时间中断器执行时的时间, 并将它和这一次时间中断器执行的时间进行对比:
- 如果两次调用时间之间的差距为负值, 或者非常大(超过 2 秒钟), 那么 Sentinel 进入 TILT 模式。
- 如果 Sentinel 已经进入 TILT 模式, 那么 Sentinel 延迟退出 TILT 模式的时间。
当 Sentinel 进入 TILT 模式时, 它仍然会继续监视所有目标, 但是:
- 它不再执行任何操作,比如故障转移。
- 当有实例向这个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令时, Sentinel 返回负值: 因为这个 Sentinel 所进行的下线判断已经不再准确。
如果 TILT 可以正常维持 30 秒钟, 那么 Sentinel 退出 TILT 模式。
处理 -BUSY 状态
当 Lua 脚本的运行时间超过指定时限时, Redis 就会返回 -BUSY 错误。
当出现这种情况时, Sentinel 在尝试执行故障转移操作之前, 会先向服务器发送一个 SCRIPT KILL 命令, 如果服务器正在执行的是一个只读脚本的话, 那么这个脚本就会被杀死, 服务器就会回到正常状态。
11.8、哨兵模式实战
创建配置文件 sentinel.conf
1 | # 哨兵实例运行端口 |
哨兵模式配置参数
配置项 | 参数类型 | 作用 |
---|---|---|
port | 整数 | 启动哨兵进程端口 |
dir | 文件夹目录 | 哨兵进程服务临时文件夹,默认为/tmp,要保证有可写入的权限 |
sentinel down-after-milliseconds | <服务名称><毫秒数(整数)> | 指定哨兵在监控Redis服务时,当Redis服务在一个默认毫秒数内都无法回答时,单个哨兵认为的主观下线时间,默认为30000(30秒) |
sentinel parallel-syncs | <服务名称><服务器数(整数)> | 指定可以有多少个Redis服务同步新的主机,一般而言,这个数字越小同步时间越长,而越大,则对网络资源要求越高 |
sentinel failover-timeout | <服务名称><毫秒数(整数)> | 指定故障切换允许的毫秒数,超过这个时间,就认为故障切换失败,默认为3分钟 |
sentinel notification-script | <服务名称><脚本路径> | 指定sentinel检测到该监控的redis实例指向的实例异常时,调用的报警脚本。该配置项可选,比较常用 |
运行哨兵进程
1 | [root@deflory src]# ./redis-sentinel ../master-slaver-config/sentinel.conf |
查看哨兵进程
1 | [root@deflory src]# ps -ef|grep sentinel |
接下来配置主从复制,这里我们仍然选择一主二从的模式。
1 | 127.0.0.1:6379> info replication |
关闭主服务器(端口6379),哨兵进程就开始工作了,将主服务器的身份移给了从服务器(端口6380)。
1 | 32507:X 28 Nov 2021 21:06:51.180 # +failover-end master mymaster 127.0.0.1 6379 |
1 | 127.0.0.1:6380> info replication |
紧接着我再把当前新的主服务器关闭,现在只剩一台服务器,理所当然就成为了主服务器。
1 | 32507:X 28 Nov 2021 21:09:07.218 # +failover-end master mymaster 127.0.0.1 6380 |
1 | 127.0.0.1:6381> info replication |
此时再将原来的主服务器(端口6379)重新连接,直接就变成了现主服务器的从服务器。
1 | 32507:X 28 Nov 2021 21:11:08.150 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6381 |
1 | 127.0.0.1:6379> info replication |
原来的从服务器(端口6380)就更是如此了。
1 | 32507:X 28 Nov 2021 21:14:49.324 * +convert-to-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6381 |
1 | 127.0.0.1:6380> info replication |
十二、缓存问题
11.1、缓存雪崩
当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。
分析:
造成缓存雪崩的关键在于在同一时间大规模的key失效。为什么会出现这个问题呢,有几种可能,第一种可能是 Redis 宕机,第二种可能是采用了相同的过期时间(购物节零点秒杀,大量商品同时失效)。搞清楚原因之后,那么有什么解决方案呢?
解决方案:
- 在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。
- 使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
- 提高数据库的容灾能力,可以使用分库分表,读写分离的策略。
- 为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。
11.2、缓存击穿
其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。
分析:
关键在于某个热点的key失效了,导致大并发集中打在数据库上。所以要从两个方面解决,第一是否可以考虑热点key不设置过期时间,第二是否可以考虑降低打在数据库上的请求数量。
解决方案:
- 如果业务允许的话,对于热点的 key 可以设置永不过期。
- 使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。
11.3、缓存穿透
我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。
分析:
关键在于在Redis查不到key值,这和缓存击穿有根本的区别,区别在于缓存穿透的情况是传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。
解决方案:
- 把无效的Key存进Redis中。如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value=”null”,当下次再通过这个Key查询时就不需要再查询数据库。这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。
- 使用布隆过滤器。布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。
11.4、小结
这三个问题在使用Redis的时候是肯定会遇到的,而且是非常致命性的问题,所以在日常开发中一定要注意,每次使用Redis时,都要对其保持严谨的态度。还有一个需要注意的是要做好熔断,一旦出现缓存雪崩,击穿,穿透这种情况,至少还有熔断机制保护数据库不会被打死。