Redis学习日记(二):Redis 小工具及持久化

目录

七、Redis的瑞士军刀

1 . 慢查询

2 . pipeline(管道)

3 . 发布订阅

a、角色

b、模型

c、API

d、订阅模式和消息队列

4 . bitmap

a、这是什么?

b、怎么用?

c、用哪里?

5 . hyperloglog

八、Redis的持久化

1 、持久化

a、什么是持久化

b、持久化的方式

2 、RDB模式

a、什么是RDB

b、触发机制-主要三种

c、触发机制-不容忽略方式(以下方式会自动生成rdb文件)

d、RDB总结

3 、AOF模式

a、RDB模式的缺陷

b、什么是AOF

c、AOF的三种策略模式

d、AOF重写

4 、RDB和AOF的抉择

a、RDB和AOF的区别

b、最佳实践

5 、持久化中的问题

6、数据恢复



七、Redis的瑞士军刀

1 . 慢查询

首先上一张redis查询时的生命周期图

在这里我们可以看到总共有4个流程,每一个流程都可能造成客户端超时,而慢查询是在执行命令时(第三阶段)造成的。

 

现在有两个问题: 1. 什么命令属于慢查询呢?    2. 判定为慢查询的命令redis怎么处理?

 对于第一个问题,redis中有个配置叫做 slowlog-log-slower-than ,它的单位是微秒,默认制定了10000微秒,意思是超过10ms的查询会被当做慢查询处理,当指定为0时所有的都记录,<0 时什么都不记录。

第二个问题,redis处理慢查询时会把判定为慢查询的命令扔进一个先进先出的队列,这个队列是有固定长度的,还有直接保存在内存中的,我们可以指定队列的长度,当队列满的时候会把队列第一个弹出,这个队列默认长度为128。通过slowlog-max-len 来指定队列长度。

 

查看慢查询的API:

1 . slowlog get N : 得到慢查询队列中的N条命令,因为慢查询是用先进先出的队列保存的,所以在get的时候得到的是最后进入队列的N条慢查询。

2 . slowlog len  : 得到慢查询队列的当前长度。

3 . slowlog reset : 重置慢查询队列,相当于清空队列,此时 slowlog len  为0

 

下面有两种配置这个两个配置量的方法:

1 . 修改启动的配置文件 slow-max-len 1000;

2 . 因为这两个配置支持热配置,所以可以用config set slow-max-len 1000 的命令来指定最大队列长度为1000。

 

下面记录了几条建议:

 

2 . pipeline(管道)

根据redis的生命周期,一个命令从客户端发出到收到结果需要一次网络时间(发送命令和得到结果)和一次执行时间

现在我们想象一个使用情景,一个客户端发送了10K个 get 命令,那现在所需要的时间就是 10K 次网络时间+10K次执行时间,我们知道一个get命令在不阻塞的情况下执行时间是微秒级别的,而客户端请求服务器的网络时间因为地域,网络质量等原因算作毫秒级别,这样我们这个10K个命令都花在了网络时间上,这时就在考虑能不能把多个命令组合起来一起发送,结果一起返回,看起来就像一条命令一样呢?

所以这个pipeline的作用就是这样。他可以一次性把多个命令一次性发送到redis-server执行,然后按命令顺序返回结果。对的,这个和redis 提供的mset ,mget有点相似,都是操作多个key(对mset,mget操作不熟悉的,可以看我的redis学习日记(一))那他们有什么区别呢?

mset,mget和pipeline的区别:1.mget,mset都是原子操作,属于redis原生命令,不可拆分,如图左;pipeline相当于把多个命令组合发送到服务器,到达服务器执行时这些命令会拆分成原来的子命令顺序执行,但是这个顺序执行中可以会插入其他来源的命令,如图右。

                    

 

pipeline使用注意:①:pipeline使用时要注意携带的数据量,当太大时也会造成网络堵塞,所以要携带的数据量太大时要注意对数据量的拆分。②:pipeline每次只能作用于一个redis节点(不支持集群)。

下面给出pipeline的使用方法和mset,set,pipeline处理10000条命令的效率:

    @Test
    public void test4 () {
        Jedis jedis = JedisUtil.getJedis();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            jedis.hset("myhash1", "field" + i, "value" + i);
        }
        System.out.println("hash set 10000个字段花费时间 :" + (System.currentTimeMillis() - start));
        jedis.close();
    }

    @Test
    public void test5 () {
        Jedis jedis = JedisUtil.getJedis();
        HashMap<String, String> map = new HashMap<>();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            map.put("field" + i, "value" + i);
        }
        jedis.hmset("myhash2", map);
        System.out.println("hash hmset 10000个字段花费时间 :" + (System.currentTimeMillis() - start));
        jedis.close();
    }

    @Test
    public void test6 () {
        Jedis jedis = JedisUtil.getJedis();
        long start = System.currentTimeMillis();
        Pipeline pipeline = jedis.pipelined();
        for (int j =0; j < 10000; j++) {
            pipeline.hset("myhash3", "field" + j, "value" + j);
        }
        pipeline.syncAndReturnAll();
        System.out.println("hash pipeline hset 10000个字段花费时间 :" + (System.currentTimeMillis() - start));
        jedis.close();
    }

通过结果可以明显看出在处理10000条数据时,hset比hmset和pipeline慢了非常多,我这里是本地连接的阿里云的redis,所以在处理成片的命令时网络时间会占的特别多,在这种情况下使用pipeline会显著的提高效率。在可以使用m操作的命令时也可以使用mset,不能使用时可以使用pipeline,至于这两者的区别和pipeline注意事项在上文都已经提到了。

 

3 . 发布订阅

a、角色

                   ① : 订阅者     =====>     订阅科技频道的人,如自己 

                   ② : 频道       ======>     我收看的频道,如科技频道,电影频道,音乐频道等

                   ③ : 发布者     =====>     发布信息到频道的人,如搜狐科技发布科技信息到科技频道,电影发布网发布电影信息到电影频道

b、模型

  

我们可以看到,订阅者和发布者都是客户端,服务端相当于频道,这里server可以有多个频道,订阅者也可以订阅多个频道,一个新的订阅者不能收到订阅之前发布者发布在这个频道上的消息。

 

c、API

publish channel message : 向channel 中发布message信息 ,例如 publish douban loveMovie1

subscribe channel :订阅channel

unsubcribe channel :取消订阅channel

psubscribe [pattern]  :订阅匹配到pattern的频道,如psubscribe * ,则订阅所有频道;

pubsub channel : 列出至少有一个订阅者的频道。

pubsub numsub channel :列出给定频道的订阅者数量。

 

d、订阅模式和消息队列

发布订阅模式是所有订阅者都能收到频道中的内容(例如订阅科技新闻)。

消息队列是订阅者中抢频道中的内容,并且只能一个订阅者能得到(例如抢红包),消息队列可用list,lpush,rpop实现。

 

4 . bitmap

a、这是什么?

          位图:是按位存取的数据结构。(如下图所示就是big 按照位图的存储方式)

因为ASCII码用一个字节表示,所以这里用8位来表示一个字符。因为bitmap不是真正的数据类型,定义成的还是字符串,而一个字符串所能存储的最大是512M,换算成位,就是 2^32bit,也就是最多有2^32个单元格。

这里要注意一点,在初始的时候位图是没有赋值的,如当前索引在10,这时要给1000W处的索引出赋值,这时redis会把10~1000W之间都用0赋上,这个是非常耗时的,追其原因是因为偏移量一次性移动长度太大,所以这时就得避免在一个小的位图上突然给一个超级大的偏移量,这样一次偏移量大的操作可以分成多次操作完成。

 

b、怎么用?

1. setbit key offset value : 设置key的offset处为value。如setbit mybit 10 1,即把mybit 第10个位置设置为1,这个偏移量是从0开始的。

2. getbit keyoffset : 获得指定偏移量的值

3. bitcount key [start, end] : start ,end 可选择添加,不填则默认key的全部长度,获取长度范围内的1的数量

4. bitop [and, or, not, xor] destkey key1 key2 ... : and是交集,or是并集,not是非集,xor是异或,这里有4种操作,将key1,key2 的操作结果保存到destkey中。

5. bitpos key targetBit [start, end] : 结果是在范围内第一个值等于targetBit的索引值。

 

c、用哪里?

1. 使用bitmap做独立用户统计,每个用户id就是bitmap的索引值,可以用1表示访问,0代表不访问。最适合百万级别的用户访问统计。

2. 给用户做留存率,记录注册后留存情况。一个key是一个用户,如索引1 代表第一天的留存情况,所以60代表注册后第60天是否还留存。

 

5 . hyperloglog

这个数据结构使用极小的内存来保存独立用户的数量,本质数据结构还是string,其实使用了特别的算法来实现。

它的三个命令:

1. pfadd key value1 value2 value3... : 添加value值

2. pfcount key : 这个key 中拥有的key的数量

3. pfmerge destkey key1 key2 ... :将这些key合并成到一个destkey中

 

使用hyperloglog保存这些数据的时候内存极小,但是也有他的局限性。

1. 在得到总数时会有错误(错误率0.81%),要权衡是否能容忍这个错误

2. 不能得到单条数据,因为这个数据结构的内存极小,所以得到单条数据也是非常难的。

 

八、Redis的持久化

 

1 、持久化

 

a、什么是持久化

持久化是把数据保存到硬盘的过程。因为redis是保存在内存中的,电脑重启内存就空了,不做持久化的redis数据也就清空了。

 

b、持久化的方式

 

 

2 、RDB模式

 

a、什么是RDB

快照的实现方式,将redis此时此刻的状态和数据等信息保存成一个rdb文件(二进制),这个rdb文件保存在硬盘中,就是一个持久化文件,当想恢复数据时,加载这个rdb文件即可。这个rdb文件可以当做复制的媒介,在主从复制时就可以通过这个rdb文件实现。       

 

b、触发机制-主要三种

① : save 命令 (同步):使用save命令即可,redis会将数据同步阻塞式地将数据保存为一个rdb文件,它的阻塞是阻塞客户端的命令。

② : bgsave (异步):使用bgsave命令即可,redis会将数据异步阻塞式地将数据保存为一个rdb文件,这里的fork方法调用的是操作系统的方法,复制一个新的进程完成这个save操作,这里的fork函数会导致进程阻塞,但是相对于save的阻塞,这里的fork会快的多。

 

③ : 自动方式:save 900 1 指的是900秒钟内有一条更新操作就做bgsave保存rdb文件。

                          dbfilename 指的是保存rdb文件的名称

                          dir 指的是保存的路径

                          stop-writes-on-bgsave-error 指的是是否在bgsave失败时停止写入redis的操作

                          rdbcompression 是否对rdb文件进行压缩

上述这里三种的文件策略都是更新覆盖,rdb文件只保存一个,当存在rdb文件时覆盖原来的文件。

 

c、触发机制-不容忽略方式(以下方式会自动生成rdb文件)

1 . 全量复制 (主从全量复制时会生成rdb文件)

2 . debug reload (debug级别的重启,不清空数据的重启) 

3 . shutdown 操作

4 . flushall 命令

 

d、RDB总结

1 . RDB是以快照的方式把redis的数据持久化到硬盘的。

2 . save通常会阻塞redis。

3 . bgsave持久化时不会阻塞redis,但是在fork的时候会阻塞,但这个fork时间相对来说快的多。

4 . 自动化持久化会在满足任一save参数时触发。

 

 

3 、AOF模式

a、RDB模式的缺陷

      ①  RDB模式耗时,耗性能,每次要全量备份非常耗时,而且这样的写操作消耗cpu,并且加剧I/O损耗

      ② 第二RDB模式会造成数据的丢失,因为RDB的模式决定了它不能经常持久化,这样会造成大量的阻塞时间,在这个条件下的RDB模式如果在两次备份期间redis服务器因为外部原因宕机的话就就会造成数据的丢失,持久化的数据就是上次备份的数据,还没来的及持久化的数据丢失,因为两次持久化的间隔可能时间较长,因此这个数据的丢失影响较大。

 

b、什么是AOF

        AOF就是持久化的另一种方式---日志化管理。将redis执行的命令都以日志的方式写入文件,在恢复时就相当于重新执行一遍这些命令。

                  

c、AOF的三种策略模式

        上面讲到AOF是将命令以日志的形式写入文件,这里写入时并不是绝对的一句一句的写入,这里就有3种写入的策略。

redis在执行写命令时并不是马上写入日志中,而是先写入缓冲区,再根据缓冲区的策略写入到日志中。这里说的三种策略就是从缓冲区到硬盘的写入。

 

        ① ererysec : 每秒把缓冲区的内容写入AOF文件中,也就是说最多丢失1秒的数据(默认使用此策略)。

        ② always : always的策略是每条命令都写入到AOF文件中,redis的写入不会丢失,但是 I/O 开销很大。

        ③ no : 让操作系统来决定写入AOF文件的时机,这样我们就不用去管理这个时机,但也因为这样我们对AOF写入变的不可控,不推荐。

 

d、AOF重写

                   AOF存在的问题 : 当时间久了,AOF的文件会不断变大,恢复redis的时间也就会变的更长。

                  那现在AOF重写就是来解决这个问题的,他是怎么解决的呢?简单来说就是优化AOF中存储的命令。举例来说,自增1 的操作有100W次,那AOF重写就可以变成自增100W,而不是执行100W此操作。

                 AOF重写的作用是让硬盘的占有量变的更小,并且加速redis恢复时的速度。

 

                实现重写的两种方式

                         ① bgrewriteaof :  和bgsave的方式类似,也是复制一个新的进程来做重写任务,(注意!)这里的AOF重写并不是和上面的举例一样把几句命令合并成一句或者抽象成一些命令,而是回溯redis的命令,分别用不同的命令来写入key来达到和现在redis相同的redis,具体来说用set写入字符串键值,用hmset 写入hash键值,用lpush 写入list结构的键值,用sadd 写入set结构的键值,zadd等命令。这样就不用管redis之前做的删除或者修改操作,只要用新增命令来达到和现在redis一样结构的就行了。

 

 

                     ② 重写自动配置:  auto-aof-rewrite-min-size  ----->   AOF 文件重写需要的尺寸

                                                     auto-aof-rewrite-percentage ----->  AOF增长率,下一次重写的尺寸是 增长率 * 现在的尺寸 

                                                     也就是同时满足这两个条件的时候才会自动重写:现在的AOF大小(A),上一次重写时的大小(B),                                               AOF重写所需要的最小的尺寸(C),AOF 增长率为(D),那么 A > C && (A - B)/ B > D 时自动重写。

 

                    问题: 既然AOF备份是fork子进程来完成生成新的AOF文件,那么子线程备份时主线程进行写入,这时redis 怎么处理这部分命令数据的呢?

                     这里就牵扯到写时复制技术了(Copy-On-Write):写时复制是指当一个文件(A)在被其他多个进程读取时,A有新的内容写入,这时并不直接写入A,而是复制一个A的备份B,将新的内容写入B,当这些写操作处理完的时候再讲之前对A的引用指向B,完成。

                    应用在redis AOF流程就是 

                           1 . AOF开始备份(redis状态1,进程A);

                           2 . redis fork出一个新的进程(B)开始AOF备份; 

                           3 . B生成新的AOF文件覆盖原先的AOF文件。                

         注意:          如果在步骤2 的时候进程A又处理了新的数据(状态2),这时状态2和状态1的数据就不同了,进程A将这部分新增的数据存入内存,我们可以称为新增数据缓存区,当进程B备份数据到状态1的时候结束,在结束前再把这个缓冲区的数据写入AOF文件中达到和现在的状态一致的效果(写时复制)。自己画了一个流程图,有不对的欢迎指正。

            

                               

                      这里还有个自动化AOF备份所需的配置: no-appendfsync-on-rewrite  yes : 这个配置的意思是 是否在AOF重写的时候将新的写入命令写入AOF文件中,正常备份的时候不存在问题,但是如果AOF备份失败了就会导致这部分的数据丢失,因为AOF重写比较耗性能,所以在性能和数据准确性方面一般倾向于性能,选择在AOF重写的时候不备份。配置可以参考下面。

 

4 、RDB和AOF的抉择

a、RDB和AOF的区别

b、最佳实践

RDB+AOF

RDB每周定时全量备份 + AOF的自动增量备份。

关闭RDB的自动备份,定时集中化管理备份。开启AOF,设置为everysec策略。redis单点多台时小分片,限制每个redis的最大存储量。

 

5 、持久化中的问题

在持久化中无论是 bgsave 还是 bgrewriteaof 都会进行fork操作来创造子进程来完成任务,这时如果单机多部署的形式存在多个redis同时进行AOF重写,我们这时就得考虑这些子进程的开销和优化方式了。

 

 

6、数据恢复

        情景1 : RDB 方式和 AOF 方式同时开启,这时默认恢复是按照AOF文件来恢复的,如果这时 AOF 文件内容为空,存在全部内容的RDB 备份文件,这样恢复的时候redis也是没数据的,开启AOF就不会再用 RDB 文件恢复。

        情景2 : 仅存在RDB方式时使用RDB文件来恢复。

注意:开启 AOF 日志时把配置文件中的 appendonly 设置为 yes, 如果想把用 RDB 文件恢复先把 appendonly 项设置为no,这时正常的恢复就会使用 RDB 文件。

那么如何恢复数据呢?

第一步:查看appendonly 是否为yes,yes则会使用 AOF 文件恢复,no使用 RDB 文件恢复。

第二部:查看 RDB 文件或者 AOF 文件存储的路径 ----》 进入客户端界面使用 config get dir 命令查看 dir 路径,这个路径就是redis保存 RDB 文件和 AOF 文件的路径。

第三部 : 进入上述的路径,启动redis服务,如 redis-server config/6380.conf 。 这里就会自动按照 AOF 文件或者 RDB 文件恢复。

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章