说明
唉,写得太长了,CSDN编辑器不允许我在一篇文章上继续发挥了。
这是上一篇文章 【大厂面试】面试官看了赞不绝口的Redis笔记(二)
目录:
这是下一篇文章【大厂面试】面试官看了赞不绝口的Redis笔记(三)分布式篇
目录:
四、Redis的其他功能
(一)慢查询
慢查询简介 慢查询顾名思义是将redis执行命令较慢的命令记录下来。
一条命令的生命周期
- client通过网络向Redis发送一条命令
- 由于Redis是单线程应用,可以把Redis想像成一个队列,client执行的所有命令都在排队等着server端执行
- Redis服务端按顺序执行命令
- server端把命令结果通过网络返回给client
两点说明
(1)慢查询发生在第3阶段
(2)客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素
慢查询是一个先进先出的队列,如果一条命令在执行过程中被列入慢查询范围内,就会被放入一个队列,这个队列是基于Redis的列表来实现,而且这个队列是固定长度的,当队列的长度达到固定长度时,最先被放入队列就会被pop出去。慢查询队列保存在内存之中,不会做持久化,当Redis重启之后就会消失。
结合上面图示这里涉及到两个配置和三个慢查询命令。
先看两个配置
(1) slowing-max-len
(2)slowing-log- slower-than
slowlog-max-len 慢查询队列的长度
slowlog-log-slower-than 慢查询阈值(单位:微秒),执行时间超过阀值的命令会被加入慢查询命令
如果设置为0,则会记录所有命令,通常在需要记录每条命令的执行时间时使用
如果设置为小于0,则不记录任何命令
slowlog list 慢查询记录
慢查询配置方法
1.修改配置文件重启
修改/etc/redis.conf配置文件,配置慢查询
修改配置方式应该在第一次配置Redis中时配置完成,生产后不建议修改配置文件
2.动态配置
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "128"
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
127.0.0.1:6379> config set slowlog-max-len 1000
OK
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "1000"
127.0.0.1:6379> config set slowlog-log-slower-than 1000
OK
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "1000"
与配置对应的是三个慢查询命令
- slowlog get [n]:获取慢查询队列
- slowlog len:获取慢查询队列长度
- slowlog reset:清空慢查询队列
值得注意的是:
- slowing-max-len不要设置过大,默认10ms,通常设置1ms
- slowing-log- slower-than不要设置过小,通常设置1000左右。
- 理解命令生命周期。
- 定期持久化慢查询。
(二)pipeline
pipeline的中文意思是管道。
下面通过图示,我们看看认清楚什么是流水线:
批量网络命令通信模型:
n次时间=n次网络时间+n次命令时间
Pipeline模型:
pipeline就是把一批命令进行打包,然后传输给server端进行批量计算,然后按顺序将执行结果返回给client端
使用Pipeline模型进行n次网络通信需要的时间:
1次pipeline(n条命令) = 1次网络时间 + n次命令时间
为了更具体,我们可以测试一下时间:(python实现)
import redis
import time
client = redis.StrictRedis(host='192.168.81.100',port=6379)
start_time = time.time()
for i in range(10000):
client.hset('hashkey','field%d' % i,'value%d' % i)
ctime = time.time()
print(client.hlen('hashkey'))
print(ctime - start_time)
程序执行结果:
10000
2.0011684894561768
在上面的例子里,直接向Redis中写入10000条hash记录,需要的时间大约为2.00秒
使用pipeline的方式向Redis中写入1万条hash记录
import redis
import time
client = redis.StrictRedis(host='192.168.81.100',port=6379)
start_time = time.time()
for i in range(100):
pipeline = client.pipeline()
j = i * 100
while j < (i+ 1) * 100:
pipeline.hset('hashkey1','field%d' % j * 100,'value%d' % i)
j += 1
pipeline.execute()
ctime = time.time()
print(client.hlen('hashkey1'))
print(ctime - start_time)
程序执行结果:
10000
0.3175079822540283
可以看到使用Pipeline方式每次向Redis服务端发送100条命令,发送100次所需要的时间仅为0.31秒,可以看到使用Pipeline可以节省网络传输时间
值得注意的是
- 每次pipeline携带数据量不能太大
- pipeline可以提高Redis批量处理的并发的能力,但是并不能无节制的使用
- 如果批量执行的命令数量过大,则很容易对网络及客户端造成很大影响,此时可以把命令分割,每次发送少量的命令到服务端执行
- pipeline每次只能作用在一个Redis节点上
还有,记得pipeline命令不是原子命令(要么全部一下子执行,要么不执行),pipeline中命令以子命令的形式穿插在Redis执行的其他命令当中
(三)发布订阅
我们在字符类型那部分已经探讨过 Redis简易的消息队列(点对点,后面有解释和对比)。这里则是高级点的实现。
对于有接触过发布订阅模型(生产者消费者模型)的消息队列的朋友来说,这部分是So easy的。
发布订阅模型分成三个角色:
- 发布者( publisher)
- 订阅者( subscriber)
- 频道( channel)
它们的关系如下:
- 每个订阅者可以订阅多个频道
- 发布者发布消息后,订阅者就可以收到不同频道的消息
- 订阅者不可以接收未订阅频道的消息
- 订阅者订阅某个频道后,Redis无法做消息的堆积,不能接收频道被订阅之前发布的消息
Redis server就相当于频道
发布者是一个redis-cli,通过redis server发布消息
订阅者也是于一个redis-cli, 如果订阅了这个频道,就可以通过redis server获取消息
发布订阅的命令
publish channel message 发布消息
subscribe [channel] 订阅频道
unsubscribe [channel] 取消订阅
psubscribe [pattern...] 订阅指定模式的频道
punsubscribe [pattern...] 退订指定模式的频道
pubsub channels 列出至少有一个订阅者的频道
pubsub numsub [channel...] 列表给定频道的订阅者数量
pubsub numpat 列表被订阅模式的数量
打开一个终端1
127.0.0.1:6379> subscribe sohu_tv # 订阅sohu_tv频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sohu_tv"
3) (integer) 1
打开一个终端2
127.0.0.1:6379> publish sohu_tv 'hello python' # sohu_tv频道发布消息
(integer) 1
127.0.0.1:6379> publish sohu_tv 'hello world' # sohu_tv频道发布消息
(integer) 3
可以看到终端1中已经接收到sohu_tv发布的消息
127.0.0.1:6379> subscribe sohu_tv
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sohu_tv"
3) (integer) 1
1) "message"
2) "sohu_tv"
3) "hello python"
1) "message"
2) "sohu_tv"
3) "hello world"
打开终端3,取消订阅sohu_tc频道
127.0.0.1:6379> unsubscribe sohu_tv
1) "unsubscribe"
2) "sohu_tv"
3) (integer) 0
消息队列点对点与发布订阅区别
1.点对点
消息生产者消息发送到queue中,然后消费者从queue中取。
注意:消息被消费以后,队列中不再有存储 。客户端和客户端之间是 抢 的关系。生产者发送一条消息到 queue,只有一个消费者能收到。
2.发布/订阅
生产者将消息发送到topic中,同时多个消费者消费这个消息。 和点对点不同,发布到topic的消息会被所有订阅者消费。
(四)Bitmap
在我们平时开发过程中,会有⼀些 布尔型数据需要存取,⽐如CSDN APP⽤户⼀年的签到记录(我快签到100天了,还是比较活跃的,欢迎与我交流),签了是 1,没签是 0,要记录 365 天。如果使⽤普通的 key/value,每个⽤户要记录 365 个,⽤户上千万的时候,需要的存储空间是比较大的。
为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据⼀个位,365 天就是 365 个位,46 个字节 (⼀个稍⻓⼀点的字符串) 就可以完全容纳下,这就⼤⼤节约了存储空间。
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是byte 数组。我们可以使⽤普通的 get/set 直接获取和设置整个位图的内容,也可以使⽤位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
首先来看一个例子,字符串big,
字母b的ASCII码为98,转换成二进制为 01100010
字母i的ASCII码为105,转换成二进制为 01101001
字母g的ASCII码为103,转换成二进制为 01100111
如果在Redis中,设置一个key,其值为big,此时可以get到big这个值,也可以获取到 big的ASCII码每一个位对应的值,也就是0或1
127.0.0.1:6379> set hello big
OK
127.0.0.1:6379> getbit hello 0 # b的二进制形式的第1位,即为0
(integer) 0
127.0.0.1:6379> getbit hello 1 # b的二进制形式的第2位,即为1
(integer) 1
我们看一下它常用的API
1.setbit
SETBIT key offset value
时间复杂度: O(1)
对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。位的设置或清除取决于 value 参数,可以是 0 也可以是 1 。当 key 不存在时,自动生成一个新的字符串值。字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。
offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。
redis> SETBIT bit 10086 1
(integer) 0
redis> GETBIT bit 10086
(integer) 1
redis> GETBIT bit 100 # bit 默认被初始化为 0
(integer) 0
setbit
偏移量不要太大,向上面的 SETBIT bit 10086 1 0-10085都要初始化成0
2.getbit
GETBIT key offset
时间复杂度: O(1)
对 key 所储存的字符串值,获取指定偏移量上的位(bit)。当 offset 比字符串值的长度大,或者 key 不存在时,返回 0 。
# 对不存在的 key 或者不存在的 offset 进行 GETBIT, 返回 0
redis> EXISTS bit
(integer) 0
redis> GETBIT bit 10086
(integer) 0
# 对已存在的 offset 进行 GETBIT
redis> SETBIT bit 10086 1
(integer) 0
redis> GETBIT bit 10086
(integer) 1
3.bitcount
时间复杂度: O(N)
计算给定字符串中,被设置为 1 的比特位的数量。
一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。
start 和 end 参数的设置和 GETRANGE key start end 命令类似,都可以使用负数值: 比如 -1 表示最后一个字节, -2 表示倒数第二个字节,以此类推。
不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
redis> BITCOUNT bits
(integer) 0
redis> SETBIT bits 0 1 # 0001
(integer) 0
redis> BITCOUNT bits
(integer) 1
redis> SETBIT bits 3 1 # 1001
(integer) 0
redis> BITCOUNT bits
(integer) 2
对了哦,前面提到的CSDN APP签到的应用,就是用这个命令实现的。
4.bitop
BITOP operation destkey key [key …]
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。返回保存到 destkey 的字符串的长度,和输入 key 中最长的字符串长度相等。
operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种:
- BITOP AND destkey key [key …] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
- BITOP OR destkey key [key …] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
- BITOP XOR destkey key [key …] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
- BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey 。
除了 NOT 操作之外,其他操作都可以接受一个或多个 key 作为输入。
处理不同长度的字符串
当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0 。
空的 key 也被看作是包含 0 的字符串序列。
redis> SETBIT bits-1 0 1 # bits-1 = 1001
(integer) 0
redis> SETBIT bits-1 3 1
(integer) 0
redis> SETBIT bits-2 0 1 # bits-2 = 1011
(integer) 0
redis> SETBIT bits-2 1 1
(integer) 0
redis> SETBIT bits-2 3 1
(integer) 0
redis> BITOP AND and-result bits-1 bits-2
(integer) 1
redis> GETBIT and-result 0 # and-result = 1001
(integer) 1
redis> GETBIT and-result 1
(integer) 0
redis> GETBIT and-result 2
(integer) 0
redis> GETBIT and-result 3
(integer) 1
5.bitpos
BITPOS key bit [start] [end]
时间复杂度: O(N),其中 N 为位图包含的二进制位数量
返回位图中第一个值为 bit 的二进制位的位置。在默认情况下, 命令将检测整个位图, 但用户也可以通过可选的 start 参数和 end 参数指定要检测的范围。
127.0.0.1:6379> SETBIT bits 3 1 # 1000
(integer) 0
127.0.0.1:6379> BITPOS bits 0
(integer) 0
127.0.0.1:6379> BITPOS bits 1
(integer) 3
下面我们再说一个应用
如果一个网站有1亿用户,假如user_id用的是整型,长度为32位,每天有5千万独立用户访问,如何判断是哪5千万用户访问了网站
方式一:用set来保存
使用set来保存数据运行一天需要占用的内存为
32bit * 50000000 = (4 * 50000000) / 1024 /1024 MB,约为200MB
运行一个月需要占用的内存为6G,运行一年占用的内存为72G
30 * 200 = 6G
方式二:使用bitmap的方式
如果user_id访问网站,则在user_id的索引上设置为1,没有访问网站的user_id,其索引设置为0,此种方式运行一天占用的内存为
1 * 100000000 = 100000000 / 1014 /1024/ 8MB,约为12.5MB
运行一个月占用的内存为375MB,一年占用的内存容量为4.5G
由此可见,使用bitmap可以节省大量的内存资源
值得注意的是
- bitmap是string类型,单个值最大可以使用的内存容量为512MB
- setbit时是设置每个value的偏移量,可以有较大耗时(偏移量不要太大)
- bitmap不是绝对好,用在合适的场景最好
(五)HyperLogLog
基于HyperLogLog算法,极小空间完成独立数量统计,本质还是字符串。算法描述参考维基百科介绍,实现起来50行代码左右的样子。
HyperLogLog 提供了两个指令 pfadd 和 pfcount,⼀个是增加计数,⼀个是获取计数。pfadd ⽤法和 set集合的 sadd 是⼀样的,pfcount 和 scard ⽤法是⼀样的,直接获取计数值。
PFADD key element [element …]
将任意数量的元素添加到指定的 HyperLogLog 里面。
PFCOUNT key [key …]
计算hyperloglog的独立总数
prmerge destkey sourcekey [sourcekey…]
合并多个hyperloglog
127.0.0.1:6379> pfadd unique_ids1 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_4' # 向unique_ids1中添加4个元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids1 # 查看unique_ids1中元素的个数
(integer) 4
127.0.0.1:6379> pfadd unique_ids1 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_10' # 再次向unique_ids1中添加4个元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids1 # 由于两次添加的value有重复,所以unique_ids1中只有5个元素
(integer) 5
127.0.0.1:6379> pfadd unique_ids2 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_4' # 向unique_ids2中添加4个元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids2 # 查看unique_ids2中元素的个数
(integer) 4
127.0.0.1:6379> pfadd unique_ids2 'uuid_4' 'uuid_5' 'uuid_6' 'uuid_7' # 再次向unique_ids2中添加4个元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids2 # 再次查看unique_ids2中元素的个数,由于两次添加的元素中有一个重复,所以有7个元素
(integer) 7
127.0.0.1:6379> pfmerge unique_ids1 unique_ids2 # 合并unique_ids1和unique_ids2
OK
127.0.0.1:6379> pfcount unique_ids1 # unique_ids1和unique_ids2中有重复元素,所以合并后的hyperloglog中只有8个元素
(integer) 8
hyperloglog也有非常明显的局限性:
- hyperloglog有一定的错误率,在使用hyperloglog进行数据统计的过程中,hyperloglog给出的数据不一定是对的
按照维基百科的说法,使用hyperloglog处理10亿条数据,占用1.5Kb内存时,错误率为2% - 没法从hyperloglog中取出单条数据,这很容易理解,使用16KB的内存保存100万条数据,此时还想把100万条数据取出来,显然是不可能的
所以具体的应用还需要考量实际的场景。
(六)GEO
GEO即地址信息定位,可以用来存储经纬度,计算两地距离,范围计算等。这意味着我们可以使⽤ Redis 来实现美团和饿了么「附近的餐馆」,微信摇一摇等功能了。
我们看一下它的常用API
geoadd key longitude latitude member [longitude latitude member…] 增加地理位置信息
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing # 添加北京的经纬度
(integer) 1
127.0.0.1:6379> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang # 添加天津和石家庄的经纬度
(integer) 2
127.0.0.1:6379> geoadd cities:locations 118.01 39.38 tangshan 115.29 38.51 baoding # 添加唐山和保定的经纬度
(integer) 2
geopos key member [member…] 获取地理位置信息
27.0.0.1:6379> geopos cities:locations tianjin # 获取天津的地址位置信息
1) 1) "117.12000042200088501"
2) "39.0800000535766543"
geodist key member1 member2 [unit] 获取两个地理位置的距离,unit:m(米),km(千米),mi(英里),ft(尺)
127.0.0.1:6379> geodist cities:locations tianjin beijing km
"89.2061"
127.0.0.1:6379> geodist cities:locations tianjin baoding km
"170.8360"
georedius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
获取指定位置范围内的地理位置信息集合
- withcoord:返回结果中包含经纬度
- withdist:返回结果中包含距离中心节点位置
- withhash:返回结果中包含geohash
- COUNT count:指定返回结果的数量
- asc|desc:返回结果按照距离中心节点的距离做升序或者降序
- store key:将返回结果的地理位置信息保存到指定键
- storedist key:将返回结果距离中心节点的距离保存到指定键
127.0.0.1:6379> georadiusbymember cities:locations beijing 150 km # 获取距离北京150km范围内的城市
1) "beijing"
2) "tianjin"
3) "tangshan"
4) "baoding"
最后还需要补充
- Redis的GEO功能是从3.2版本添加
- geo功能基于zset实现
- geo没有删除命令
五、Redis持久化的取舍和选择
Redis 的数据全部在内存⾥,如果突然宕机,数据就会全部丢失,因此必须有⼀种机制来保证 Redis 的数据不会因为故障⽽丢失,这种机制就是 Redis 的持久化机制。
Redis的持久化就是将储存在内存里面的数据以文件形式保存硬盘里面,这样即使Redis服务端被关闭,已经同步到硬盘里面的数据也不会丢失,除此之外,持久化也可以使Redis服务器重启时,通过载入同步的持久文件来还原之前的数据,或者使用持久化文件来进行数据备份和数据迁移等工作
Redis 的持久化机制有两种,一种是RDB、一种是AOF。
(一)RDB
RDB持久化功能可以将Redis中所有数据生成快照,快照是内存数据的⼆进制序列化形式,在存储上⾮常紧凑,将其保存在硬盘里,文件名为.RDB文件
在Redis启动时载入RDB文件,Redis读取RDB文件内容,还原服务器原有的数据库数据
触发Redis服务端创建RDB文件,有三种方式:
(1)使用SAVE命令手动同步创建RDB文件
客户端向Redis服务端发送SAVE命令,服务端把当前所有的数据同步保存为一个RDB文件。通过向服务器发送SAVE命令,Redis会创建一个新的RDB文件。
由于Redis单线程的特点,在执行SAVE命令的过程中(也就是即时创建RDB文件的过程中),Redis服务端将被阻塞,无法处理客户端发送的其他命令请求。只有在SAVE命令执行完毕之后(也就时RDB文件创建完成之后), 服务器才会重新开始处理客户端发送的命令请求。如果已经存在RDB文件,那么服务器将自动使用新的RDB文件去代替旧的RDB文件。
演示
1、修改Redis的配置文件/etc/redis.conf,把下面三行注释掉(后面会解释原因)
#save 900 1
#save 300 10
#save 60 10000
2、执行下面三条命令
127.0.0.1:6379> flushall # 清空Redis中所有的键值对
OK
127.0.0.1:6379> dbsize # 查看Redis中键值对数量
(integer) 0
127.0.0.1:6379> info memory # 查看Redis占用的内存数为834.26K
# Memory
used_memory:854280
used_memory_human:834.26K
used_memory_rss:5931008
used_memory_rss_human:5.66M
used_memory_peak:854280
used_memory_peak_human:834.26K
total_system_memory:2080903168
total_system_memory_human:1.94G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:6.94
mem_allocator:jemalloc-3.6.0
3、从Redis的配置文件可以知道,Redis的RDB文件保存在/var/lib/redis/目录中
[root@mysql redis]# pwd
/var/lib/redis
[root@mysql redis]# ll # 查看Redis的RDB目录下的文件
total 0
4、在客户端执行程序,向Redis中插入500万条数据
5、向Redis中写入500万条数据完成后,执行SAVE命令
127.0.0.1:6379> save # 执行SAVE命令,花费5.72秒
OK
(5.72s)
6.切换另一个Redis-cli窗口执行命令
127.0.0.1:6379> spop key1 # 执行spop命令弹出'key1'的值,因为SAVE命令在执行的原因,spop命令会阻塞直到save命令执行完成,执行spop命令共花费4.36秒
"value1"
(4.36s)
7、查看Redis占用的内存数
127.0.0.1:6379> info memory # 向Redis中写入500万条数据后,Redis占用1.26G内存容量
# Memory
used_memory:1347976664
used_memory_human:1.26G
used_memory_rss:1381294080
used_memory_rss_human:1.29G
used_memory_peak:1347976664
used_memory_peak_human:1.26G
total_system_memory:2080903168
total_system_memory_human:1.94G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:1.02
mem_allocator:jemalloc-3.6.0
127.0.0.1:6379> dbsize # 查看Redis中数据总数
(integer) 4999999
8、在系统命令提示符中查看生成的RDB文件
[root@mysql redis]# ls -lah # Redis的RDB文件经过压缩后的大小为122MB
total 122M
drwxr-x--- 2 redis redis 22 Oct 13 15:31 .
drwxr-xr-x. 64 root root 4.0K Oct 13 13:38 ..
-rw-r--r-- 1 redis redis 122M Oct 13 15:31 dump.rdb
(2)使用BGSAVE命令异步创建RDB文件
执行BGSAVE命令也会创建一个新的RDB文件,BGSAVE不会造成redis服务器阻塞:在执行BGSAVE命令的过程中,Redis服务端仍然可以正常的处理其他的命令请求。
BGSAVE命令执行步骤:
- Redis服务端接受到BGSAVE命令
- Redis服务端通过fork()来生成一个名叫redis-rdb-bgsave的进程,由redis-rdb-bgsave子进程来创建RDB文件,而Redis主进程则继续处理客户端的命令请求
- 当redis-rdb-bgsave子进程创建完成RDB文件,会向Redis主进程发送一个信号,告知Redis主进程RDB文件已经创建完毕,然后redis-rdb-bgsave子进程退出
- Redis服务器(父进程)接手子进程创建的RDB文件,BGSAVE命令执行完毕
Redis主进程因为创建子进程,会消耗额外的内存。不过,如果在Redis主进程fork子进程的过程中花费的时间过多,Redis仍然可能会阻塞
SAVE命令与BGSAVE命令的区别
命令 | save | bgsave |
---|---|---|
IO类型 | 同步 | 异步 |
是否阻塞 | 是 | 是(阻塞发生在fork) |
时间复杂度 | O(n) | O(n) |
优点 | 不会消耗额外内存) | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fork消耗内存。 |
总结:
SAVE创建RDB文件的速度会比BGSAVE快,SAVE可以集中资源来创建RDB文件。如果数据库正在上线当中,就要使用BGSAVE
;如果数据库需要维护,可以使用SAVE命令。
(3)自动生成RDB
打开Redis的配置文件/etc/redis.conf,可以看到我们刚才注释的内容
save 900 1
save 300 10
save 60 10000
save 900 1表示:如果距离上一次创建RDB文件已经过去的900秒时间内,Redis中的数据发生了1次改动,则自动执行BGSAVE命令
save 300 10表示:如果距离上一次创建RDB文件已经过去的300秒时间内,Redis中的数据发生了10次改动,则自动执行BGSAVE命令
save 60 10000表示:如果距离上一次创建RDB文件已经过去了60秒时间内,Redis中的数据发生了10000次改动,则自动执行BGSAVE命令
每次执行BGSAVE命令创建RDB文件之后,服务器为实现自动持久化而设置的时间计数器和次数计数器就会被清零,并重新开始计数,所以多个保存条件的效果是不会叠加。用户也可以通过设置多个SAVE选项来设置自动保存条件,
Redis关于自动持久化的配置
rdbcompression yes 创建RDB文件时,是否启用压缩
stop-writes-on-bgsave-error yes 执行BGSAVE命令时发生错误是否停止写入
rdbchecksum yes 是否对生成RDB文件进行检验
dbfilename dump.rdb 持久化生成的备份文件的名字
# dbfilename dump-$(port).rdb 可以以端口号 进行区分
dir /var/lib/redis/6379 RDB文件保存的目录
除了上面的三种方式,注意还有一些触发机制:
- 全量复制 (与主从复制有关 后面会说 主会生成RDB文件)
- debug reload debug级别的重启(不清空内存数据)
- shutdown 关闭 会执行rdb文件的生成
(二)AOF
RDB有两个问题
1.耗时耗性能
Redis把内存中的数据dump到硬盘中生成RDB文件,首先要把所有的数据都进行持久化,所需要的时间复杂度为O(N),同时把数据dump到文件中,也需要消耗CPU资源,由于BGSAVE命令有一个fork子进程的过程,虽然不是完整的内存拷贝,而是基于copy-on-write的策略,但是如果Redis中的数据非常多,占用的内存页也会非常大,fork子进程时消耗的内存资源也会很多
磁盘IO性能的消耗,生成RDB文件本来就是把内存中的数据保存到硬盘当中,如果生成的RDB文件非常大,保存到硬盘的过程中消耗非常多的硬盘IO
2.不可控,丢失数据
自动创建RDB文件的过程中,在上一次创建RDB文件以后,又向Redis中写入多条数据,如果此时Redis服务停止,则从上一次创建RDB文件到Redis服务挂机这个时间段内的数据就丢失了
AOF((AppendOnlyFile))相当于日志的记录。
下图是AOF创建原理。
恢复的时候 AOF载入,执行命令恢复数据。
AOF安全性问题 – 数据丢失
虽然服务器执行一次修改数据库的命令,执行的命令就会被写入到AOF文件,但这并不意味着AOF持久化方式不会丢失任何数据
在linux系统中,系统调用write函数,将一些数据保存到某文件时,为了提高效率,系统通常不会直接将内容写入硬盘里面,而是先把数据保存到硬盘的缓冲区之中。等到缓冲区被填满,或者用户执行fsync调用和fdatasync调用时,操作系统才会将储存在缓冲区里的内容真正的写入到硬盘里。
对于AOF持久化来说,当一条命令真正的被写入到硬盘时,这条命令才不会因为停机而意外丢失。因此,AOF持久化在遭遇停机时丢失命令的数量,取决于命令被写入硬盘的时间。越早将命令写入到硬盘,发生意外停机时丢失的数据就越少,而越迟将命令写入硬盘,发生意外停机时丢失的数据就越多。
AOF提供三种策略让我们在AOF安全性和效能上进行权衡。
1、always
Redis每写入一个命令,always会把每条命令都刷新到硬盘的缓冲区当中然后将缓冲区里的数据写入到硬盘里。
这种模式下,Redis即使用遭遇意外停机,也不会丢失任何自己已经成功执行的数据
2.everysec
Redis每一秒调用一次fdatasync,将缓冲区里的命令写入到硬盘里,这种模式下,当Redis的数据交换很多的时候可以保护硬盘。即使Redis遭遇意外停机时,最多只丢失一秒钟内的执行的数据
3.no
服务器不主动调用fdatasync,由操作系统决定任何将缓冲区里面的命令写入到硬盘里,这种模式下,服务器遭遇意外停机时,丢失的命令的数量是不确定的
三种方式对比:
命令 | always | everysec | no |
---|---|---|---|
优点 | 不丢失数据 | 每秒一次 fsync丢1秒数据 | 不用管 |
缺点 | IO开销较大,一般的sata盘只有几百TPS | 丢1秒数据 | 不可控 |
一般不会选择第三种。
AOF重写功能
随着服务器的不断运行,为了记录Redis中数据的变化,Redis会将越来越多的命令写入到AOF文件中,使得AOF文件的体积来断增大,为了让AOF文件的大小控制在合理的范围,redis提供了AOF重写功能,通过这个功能,服务器可以产生一个新的AOF文件:
具体内容:
- 新的AOF文件记录的数据库数据和原有AOF文件记录的数据库数据完全一样
- 新的AOF文件会使用尽可能少的命令来记录数据库数据,因此新的AOF文件的体积通常会比原有AOF文件的体积要小得多
- AOF重写期间,服务器不会被阻塞,可以正常处理客户端发送的命令请求
AOF重写触发方式
1.向Redis发送BGREWRITEAOF命令
类似于BGSAVE命令,Redis主进程会fork一个子进程,由子进程去完成AOF重写
这里的AOF重写是将Redis内存中的数据进行一次回溯,得到一个AOF文件,而不是将已有的AOF文件重写成一个新的AOF文件
2、通过配置选项自动执行BGREWRITEAOF命令
(1)auto-aof-rewrite-min-size 触发AOF重写所需的最小体积:
只要在AOF文件的大小超过设定的size时,Redis会进行AOF重写,这个选项用于避免对体积过小的AOF文件进行重写
(2)auto-aof-rewrite-percentage 指定触发重写所需的AOF文件体积百分比:
当AOF文件的体积大于auto-aof-rewrite-min-size指定的体积,并且超过上一次重写之后的AOF文件体积的percent%时,就会触发AOF重写,如果服务器刚启动不久,还没有进行过AOF重写,那么使用服务器启动时载入的AOF文件的体积来作为基准值。
将这个值设置为0表示关闭自动AOF重写功能
涉及的两个统计项:
aof_current_size AOF当前尺寸(单位:字节)
aof_base_size AOF上次启动和重写的尺寸(单位:字节)
只有当上面两个条件同时满足时才会触发Redis的AOF重写功能
自动触发时机 根据统计项 尺寸大小 增长率
AOF重写流程可用下图表示
- 无论是执行bgrewriteaof命令还是自动进行AOF重写,实际上都是执行BGREWRITEAOF命令
- 执行bgrewriteaof命令,Redis会fork一个子进程,
- 子进程对内存中的Redis数据进行回溯,生成新的AOF文件
- Redis主进程会处理正常的命令操作
- 同时Redis把会新的命令写入到aof_rewrite_buf当中,当bgrewriteaof命令执行完成,新的AOF文件生成完毕,Redis主进程会把aof_rewrite_buf中的命令追加到新的AOF文件中
- 用新生成的AOF文件替换旧的AOF文件
配置文件中AOF相关选项
appendonly no # 改为yes,开启AOF功能
appendfilename "appendonly.aof" # 生成的AOF的文件名
appendfsync everysec # AOF同步的策略
no-appendfsync-on-rewrite no # AOF重写时,是否做append的操作
AOF重写非常消耗服务器的性能,子进程要将内存中的数据刷到硬盘中,肯定会消耗硬盘的IO
而正常的AOF也要将内存中的数据写入到硬盘当中,此时会有一定的冲突
因为rewrite的过程在数据量比较大的时候,会占用大量的硬盘的IO
在AOF重写后,生成的新的AOF文件是完整且安全的数据
如果AOF重写失败,如果设置为no则正常的AOF文件中会丢失一部分数据
生产环境中会在yes和no之间进行一定的权衡,通过优先从性能方面进行考虑,设置为yes
auto-aof-rewrite-percentage 100 # 触发重写所需的AOF文件体积增长率
auto-aof-rewrite-min-size 64mb # 触发重写所需的AOF文件大小
(三)RDB和AOF的选择
RDB和AOF的选择可以参考下表:
命令 | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
启动优先级解释: 如果两者都选择了情况下 重启redis redis加载数据 会先选择aof
RDB最佳策略
RDB是一个重操作
Redis主从复制中的全量复制(之前有提到)是需要主节点执行一次BGSAVE命令,然后把RDB文件同步给从Redis从节点来实现复制的效果。即使你RDB文件生成的配置给关闭了,全量复制并不受此限制。
如果对Redis按小时或者按天这种比较大的量级进行备份,使用RDB是一个不错的选择,集中备份管理比较方便。
在Redis主从架构中,可以在Redis从节点开启RDB,可以在本机保存RDB的历史文件,但是生成RDB文件的周期不要太频繁。
Redis的单机多部署模式对服务器的CPU,内存,硬盘有较大开销,实际生产环境根据需要进行设定。
AOF最佳策略
建议把appendfsync选项设定为everysec,进行持久化,这种情况下Redis宕机最多只会丢失一秒钟的数据。
如果使用Redis做为缓存时,即使数据丢失也不会造成任何影响,只需要在下次加载时重新从数据源加载就可以了。
Redis单机多部署模式下,AOF集中操作时会fork大量的子进程,可能会出现内存爆满或者导致操作系统使用SWAP分区的情况
一般分配服务器60%到70%的内存给Redis使用,剩余的内存分留给类似fork的操作
RDB和AOF的最佳使用策略
- 使用max_memory对Redis进行规划,例如Redis使用单机多部署模式时,每个Redis可用内存设置为4G,这样无论是使用RDB模式还是AOF模式进行持久化,fork子进程操作都只需要较小的开销。
- Redis分布式时,小分片会产生更多的进程,可能会对CPU的消耗更大。
- 使用监控软件对服务器的硬盘,内存,负载,网络进行监控,以对服务器各硬盘有更全面的了解,方便发生故障时进行定位
不要占用100%的内存。
Redis持久化开发涉及的问题:
1.fork操作
Redis的fork操作是同步操作
执行BGSAVE和BGAOF命令时,实际上都是先执行fork操作,fork操作只是内存页的拷贝,而不是完全对内存的拷贝。
fork操作在大部分情况下是非常快的,但是如果fork操作被阻塞,也会阻塞Redis主线程的运行。毕竟fork与内存量息息相关:Redis中数据占用的内存越大,耗时越长(与机器类型有关),可以通过info memory命令查看上次fork操作消耗的微秒数:latest_fork_usec:0
改善fork
- 优先使用物理机或者高效支持fork操作的虚拟化技术
- 控制Redis实例最大可用内存:maxmemory
- 合理配置linux内存分配策略:vm.overcommit_memory = 1
- 降低fork频率,例如放宽AOF重写自动触发机制,不必要的全量复制
2.进程外开销
(1.1)CPU开销
RDB和AOF文件的生成操作都属于CPU密集型
通常子进程的开销会占用90%以上的CPU,文件写入是非常密集的过程
(1.2)CPU开销优化
- 不做CPU绑定,不要把Redis进程绑定在一颗CPU上,这样Redis fork子进程时,会分散消耗的CPU资源,不会对Redis主进程造成影响
- 不和CPU密集型应用在一台服务器上部署,这样不会产生CPU资源的过度竞争
- 在使用单机部署Redis时,不要发生大量的RDB,BGSAVE,AOF的过程,保证可以节省一定的CPU资源
(2.1)内存开销
在linux系统中,有一种显式复制的机制:copy-on-write,父子进程会共享相同的物理内存页,当父进程有写请求的时候,会创建一个父本,此时才会消耗一定的内存。
在这个过程中,子进程会共享fork时父进程的内存的快照。
如果父进程没有多少写入操作时,fork操作不会占用过多的内存资源,可以在Redis的日志中看到
(2.2)内存开销优化:
- 在单机部署Redis时,不要产生大量的重写,这样内存开销也会比较小
- 尽量主进程写入量比较小时,执行BGSAVE或者AOF操作
- linux系统优化:echo never > /sys/kernel/mm/transparent_hugepage/enabled
(3.1)硬盘开销
AOF和RDB文件的写入,会占用硬盘的IO及容量,可以使用iostat命令和iotop命令查看分析
(3.2)硬盘开销优化:
- 不要和硬盘高负载服务部署在一起,如存储服务,消息队列等
- 修改Redis配置文件:在AOF重写期间不要执行AOF操作,以减少内存开销 : no-appendfsync-on-rewrite = yes
- 根据硬盘写入量决定磁盘类型:例如使用SSD
- 单机多部署模式持久化时,文件目录可以考虑分盘。即对不同的Redis实例以端口来进行区分,持久化文件也以端口来区分
AOF追加阻塞(AOF一般都是一秒中执行一次)
- 主线程负责写入AOF缓冲区
- AOF同步线程每秒钟执行一次同步硬盘操作,同时还会记录一次最近一次的同步时间
- 主线程会对比上次AOF同步时间,如果距离上次同步时间在2秒之内,则返回主线程
- 如果距离上次AOF同步时间超过2秒,则主线程会阻塞,直到同步完成
AOF追加阻塞是保证AOF文件安全性的一种策略
为了达到每秒刷盘的效果,主线程会阻塞直到同步完成
这样就会产生一些问题:
因为主线程是在负责Redis日常命令的处理,所以Redis主线程不能阻塞,而此时Redis的主线程被阻塞。如果AOF追加被阻塞,每秒刷盘的策略并不会每秒都执行,可能会丢失2秒的数据
AOF阻塞定位:
如果AOF追加被阻塞,可以通过命令查看:
127.0.0.1:6379> info persistence
# Persistence
loading:0
rdb_changes_since_last_save:1
rdb_bgsave_in_progress:0
rdb_last_save_time:1539409132
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_delayed_fsync:100 # AOF被阻塞的历史次数,无法看到某次AOF被阻塞的时间点
这五个专题串过之后,你会对Redis单体,有着非常好的理解了,后面再走就是看源码了。相信你到这一步已经可以独当一面了。
我再往下面写,就是Redis分布式领域相关的东西了,比如说Redis的主从复制、哨兵机制、 Redis cluster特性以及缓存设计存在的问题与优化等。等我~
对了,兄dei,如果你觉得这篇文章可以的话,给俺点个赞再走,管不管?这样可以让更多的人看到这篇文章,对我来说也是一种激励。还有如果你有什么问题的话,欢迎留言或者CSDN APP直接与我交流。
转载:https://blog.csdn.net/qq_42322103/article/details/104195908