redis原理及常见问题

Posted 猿起缘灭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis原理及常见问题相关的知识,希望对你有一定的参考价值。

概述

  由于互联网发展,用户量激增,传统的架构直接使用关系型数据库,已经不能扛得住现在的并发量了,mysql单机一般的配置并发达到2000基本就顶天了,而且如果打到这个负载,mysql的性能会非常差,所以redis基本是现在各大互联网公司的标配。

本篇文章会以问答的方式编写,方便大家查看。

 

一、redis是以单线程模式运行,性能为什么那么快?

  要明白这个问题,首先要明白redis的线程模型,参考这篇文章:Redis线程模型,我总结一下这篇文章的核心点如下:

  •  redis可用同时有多个并发,使用io多路复用来处理redis的多个socket连接,当有多个请求进来的时候,直接使用io多路复用把socket产生的事件放入队列中,而不是直接处理
  • 使用文件事件处理器消费队列,这样做的好处是防止并发一下子上来,处理不完,导致部分请求处理不过来丢失
  • 这里的事件总共有三个,可以看出连接应答处理器,就是处理有新的socket连接进来才会使用这个,命令请求处理器,这个就是用来处理用户发送过来的操作redis的请求的,命令回复处理器,就是用来回复用户的

  这里可能很多人有疑问,什么事io多路复用?

  看这几篇文章:操作系统层面聊聊BIO,NIO和AIO (epoll)Linux IO模式及 select、poll、epoll详解从底层入手,图解 Java NIO BIO MIO AIO 四大IO模型与原理

 

  以下内容来自知乎一个用户的回答,我觉得挺好,就贴在这里:

  1. 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。
  这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
  2. 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。
  3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。。。 
  这种就是IO复用模型,Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式
  这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。

   作者:柴小喵
   链接:https://www.zhihu.com/question/28594409/answer/52835876

   这个例子中,作者很形象的解释了什么是io多路复用,但是大家注意,大家不要认为第一种选择就是BIO,第二种选择就是NIO,其实第一种选择相当于BIO + 单线程,第二种选择相当于BIO + 多线程。说一点题外话,与本文主题无关的,如果想要改成单线程+NIO,和多线程+NIO是什么样子呢?
  单线程 + NIO:老师下去检查每个学生的答案,如果这个学生还没有做出来,就检查下一个,直到检查到某个学生做完了,就检查他的答案,就这样一直死循环,直到把所有同学的作业检查完为止,这里的NIO非阻塞就体现在老师不会等那个没有做完的同学。
  多线程 + NIO:还是老师去检查同学的作业,如果这个同学没有做完,就下一个,直到检查到一个同学做完,之后就创建一个分身去检查这个同学的答案,老师还是继续检查每个同学,直到所有同学的答案都检查完。
  其实吧,这里的io多路复用,作者是说学生自己报告说自己有没有答案,其实在实际的程序中,并不是学生自己说我有没有做完,而是有一个代理,这个代理代替老师,来监控学生,如果某个学生有了答案,他就通知老师,之后老师就可以直接来看这个学生的答案,在Linux系统中这个代理往往就是select,poll,epoll。
  通过上面的例子,不知道大家有没有发现使用NIO和IO多路复用的区别,其实主要的区别就是如果很多的学生都没有获得答案,这个老师就要一直不停的从头到尾检查,这个过程是非常浪费cpu资源的,可能有的兄弟又有疑问了,不就是一个循环吗,有什么浪费资源的,其实这里说的浪费资源主要是浪费在判断上,这个例子中就是老师要去看学生有没有做完,如果是在socket网络传输中,就是要判断socket中有没有接受到消息,这个判断的过程是很浪费资源的。

 

ok,以上只是解释了什么io多路复用,为什么写了那么多的篇幅,因为我发现写redis的文章,基本都没有解释这个,所以我这里就解释一下,其实决定redis快的因素还有另外两个:

  1. 采用单线程,避免了频繁的上下文切换
  2. 采用文件事件处理器,是基于内存操作的,性能非常高  

 

二、redis常见数据结构?

这一块很简单,常用的就五种string,list,hash,set,zset,参考如下文章:Redis五种数据类型

三、redis常见使用场景?

  1. session共享:常用于分布式系统中,spring-session就是其中的一种解决方案,有兴趣可以看看。
  2. 排行榜:这个可以使用redis中的zset数据类型,这个数据结构可以排序,不过是从小到大排序的,现实中使用一般是要从大到小排序,注意转一下,比如转位负数
  3. 计数:这个就是redis的incr和decr了,这个是一个原子操作,不用担心并发的问题,不过如果是高并发的情况下,还是要慎重使用
  4. 缓存:这个就不用说了吧,使用redis主要就是使用这个的,非常的快

redis还可以作为队列,但是有rabbitmq这种这么专业的队列,为什么要用redis呢,还有订阅发布功能,这个也不常用。

四、redis持久化策略?

redis有两种持久化策略,分别是rdb、aof

rdb

rdb是什么?

  rdb是一种持久化策略,将内存中数据制作成快照文件保存到磁盘上。

rdb触发机制?

1.通过执行save或者bgsave命令,save是主线程执行的,会阻塞redis,bgsave是fork出来一个子线程执行,不会阻塞主线程执行

2.通过在redis.conf中配置,如下

  save 900 1:表示900 秒内如果至少有 1 个 key 的值变化,则保存

  save 300 10:表示300 秒内如果至少有 10 个 key 的值变化,则保存
  save 60 10000:表示60 秒内如果至少有 10000 个 key 的值变化,则保存
redis中会维护两个变量,一个是dirty(上次save或者bgsave之后,redis执行的增删改的次数),一个是lastsave(上次执行save或者bgsave的时间),redis中周期性操作函数 severCron会每隔100ms检查一下上面的三个条件有没有满足的,有满足的就执行bgsave.
 
redis在什么条件下不会执行save,或者bgsave?
1.已经在执行save或者bgsave,就不会在执行,为了防止冲突
2.在执行BGREWRITEAOF的时候不能执行bgsave,防止redis负载过高
3.save命令和gbsave不能同时执行

参考:Redis RDB 持久化方式   ,   Redis详解(六)------ RDB 持久化

aof

aof是什么?

aof也是一种持久化策略,是通过保存redis增删改的操作日志的方式。

 

aof保存执行步骤是什么?

1. 命令追加

  redis每次执行增删改操作的时候,都会以协议格式的方式追加到aof缓存中

2.文件的写入和同步

write: 根据条件,将aof buf中的数据写入到aof 文件中

save: 根据条件,调用 fsync 或 fdatasync 函数,将aof文件保存到磁盘上

 

文件的写入和同步的方式有哪几种?

redis每次执行完事件之后,都会调用flushAppendOnlyFile函数判断是否需要将缓存区中的数据持久化到磁盘文件中,这个策略取决于redis.conf的appendfsync选项,该选项可以有三种选择:

1.always:每次执行一条增删改操作之后,都会执行上面的write和save操作,而且是由主线程执行的,会阻塞redis,所以这种方式的效率是最差的,但是是最安全的,如果redis突然挂了,只会丢失一个事件循环中所产生的命令数据。

2.everysec: 每秒执行一次将aof缓存中的数据持久化到磁盘上,redis会在后台启动一个线程来处理,不会影响主线程,redis默认是采用这种方式,效率很快,如果redis突然宕机,丢失的数据可能操作2s的数据,为什么是两秒,看下图;

                                     

在情况2的时候,由于上次执行的save的事件过长,超过2s,就会导致本次save的操作不会执行,如果这时候redis发生宕机,丢失的数据就会超过2s,参考:<Redis>AOF持久化

3.no: 不保存,就是说每次调用flushAppendOnlyFile函数,只会执行write操作,并不会执行save操作,这个write操作的执行是主线程执行的,会阻塞,同时下面介绍的3种save操作也会阻塞主线程,那什么时候会把write操作写的aof文件持久化到磁盘呢?

  • 关闭redis的aof功能的时候
  • 关闭redis的时候
  • 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行),这种方式相当于把aof缓存文件持久化到硬盘交给了操作系统来控制

可以发现以上三种方式,aof文件持久化到磁盘上的时间间隔越来越长,对redis性能影响确实是越来越低,但是安全性确实越来越差。

参考文章:Redis AOF 持久化详解

 

如果磁盘上的aof文件越来越大,磁盘不够用怎么处理?

 使用aof重写机制,举个例子,set a 1,set a 2 ,set a 3,这三条命令依次执行,其实可以合并成一条命令,就是set a 3,其实aof重写也是差不多这样,他是直接将redis中保存的key-value转化为命令,为了防止影响主线程,aof重写会新启动一个新的子线程,将这些命令写入到aof重写缓存中,那如果在这个重写的过程,redis中又有新的请求进来怎么处理,答案是直接写入到aof的重新缓存中,整个过程不会影响正常的aof写入,当aof重写完成之后,会给父进程发送信号,父进程会调用一个信号函数做如下工作:

  1.将aof重写缓存中的内容写入aof文件

  2.使用新的aof文件替换原来的aof文件

这两个过程由于是主进程完成的,所以会阻塞主进程。

 

aof的重写触发时机?

  • AOF重写可以由用户通过调用BGREWRITEAOF手动触发。
  • 服务器在AOF功能开启的情况下,会维持以下三个变量:

    • 记录当前AOF文件大小的变量aof_current_size
    • 记录最后一次AOF重写之后,AOF文件大小的变量aof_rewrite_base_size
    • 增长百分比变量aof_rewrite_perc
  • 每次当serverCron(服务器周期性操作函数)函数执行时,它会检查以下条件是否全部满足,如果全部满足的话,就触发自动的AOF重写操作:

    • 没有BGSAVE命令(RDB持久化)/AOF持久化在执行;
    • 没有BGREWRITEAOF在进行;
    • 当前AOF文件大小要大于server.aof_rewrite_min_size(默认为1MB),或者在redis.conf配置了auto-aof-rewrite-min-size大小;
    • 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比(在配置文件设置了auto-aof-rewrite-percentage参数,不设置默认为100%)

如果前面三个条件都满足,并且当前AOF文件大小比最后一次AOF重写时的大小要大于指定的百分比,那么触发自动AOF重写。

参考文章:Redis之AOF重写及其实现原理

五、redis内存淘汰机制?

  当redis的内存满了之后,如果这时再有新的写请求进来,redis会如何处理,redis中一下几种策略

  • noeviction:禁止驱逐数据。默认配置都是这个。当内存使用达到阀值的时候,所有引起申请内存的命令都会报错。
  • volatile-lru:从设置了过期时间的数据集中挑选最近最少使用的数据淘汰。
  • volatile-ttl:从已设置了过期时间的数据集中挑选即将要过期的数据淘汰。
  • volatile-random:从已设置了过期时间的数据集中任意选择数据淘汰。
  • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰。
  • allkeys-random:从数据集中任意选择数据淘汰。

六、redis部署方式?

单机模式

  优点:简单

  缺点:无法高可用

主从模式

  主节点负责写操作,然后同步到从节点,从节点只负责读操作

  优点:降低了master的读压力

  缺点:主节点挂了无法保证高可用,而且没有解决主节点负载过高的问题

哨兵模式

  Redis sentinel 是一个分布式系统中监控 redis 主从服务器,如果主服务器挂了,哨兵会重新选择一个从节点来作为主节点,sentinel节点个数一般设置3个就可以了,每个sentinel节点都会向redis的节点和别的sentinel节点发送ping请求,如果操作规定的时间没有返回,这个sentinel就会主观认为这个节点下线了,如果多数的节点都认为某个redis节点下线了,就会变成客观节点下线,如果是主节点,就会启动选主过程。

  优点:可以保证高可用

  缺点:选主阶段redis集群不可用,没有解决主节点压力过高问题,每个节点都保存了redis的所有数据,没有分布式存储

集群模式

  采用无中心的结构,redis的每个节点互相连接,每个节点都会设置一个或者多个从节点,可以设置成主节点只负责写,从节点负责读,读写分离的模式

  优点:每个redis不用保存全部的缓存数据,而是采用分片存储,可以节省空间,集群高可用,同时解决了哨兵模式中主节点写压力过大的问题

  缺点:数据异步同步,不保证强一致性

七、redis雪崩,缓存击穿,缓存穿透?

缓存雪崩:redis中多数的key在同一时间过期,导致大量的请求直接访问数据库,导致数据库压力过高。

  解决办法:为每个key的过期时间增加一个随机数,防止同一时间很多key同时过期

缓存击穿:少量的热点的key过期的时候,有大量的请求进来,直接访问数据,导致数据库负载过高

  解决办法:访问数据库的时候加上同步锁,一次只允许一个线程去访问数据库

缓存穿透: 有大量的请求进来,其中请求的key在redis和数据库中都不存,导致大量的请求直接访问数据库,导致数据库负载过高。

  解决办法:1.直接把没有结果的key在redis中也缓存下来,因为这种请求一般为黑客攻击,除非黑客用大量的肉鸡,使用这种方法就可以解决

       2.增加校验,把一些不合法的请求直接拒绝了,不让请求可以打到数据库,比如数据的id是自增的,来了一个id为负数的请求,直接拒绝。

八、redis是如何进行分片的?

  要明白redis如何进行分片,就要先明白什么是一致性hash,参考这篇文章:浅谈负载均衡算法与实现

   建设cluster模式有6个节点,3个主节点,3个从节点,如果来了一个请求set key2 22,这个key2是如何保存到redis中的,步骤如下:
    1.求出key2的CRC16的值
    2.用第一步求出来的值和16384取模
    3.使用第二部求出来的值在hash环上顺时针寻找,找到哪个节点就去访问对应的节点

九、redis分布式锁?

  如果是使用redis节点来实现分布式锁,还是很简单的,步骤如下:

  加锁:使用setnx lock uuid px 10命令(如果存在就不操作,如果不存在就设置),uuid的作用用来区分是哪个线程加的锁,之后为这个key增加一个过期时间,防止发生死锁。

  解锁:我一开始的想法是直接把redis上加锁的lock删除就可以,其实不行,比如:如果A加锁成功,但是由于A执行的时间太长了,导致锁过期了,B获取到了锁,如果A执行完了,准备释放锁,直接把lock删除了,但是这时候A的锁早就不存在了,就会有问题,所以在解锁的时候,要先判断是不是自己加的锁,如果是自己加的锁再删除,而且整个过程要是一个原子操作,使用lua脚本完成,脚本如下:

if redis.call("get", KEYS[1]) == ARGV[1]
then
    return redis.call("del", KEYS[1])
else
    return 0
end

  但是使用redis单节点解锁有一个坏处就是,如果加锁的那个redis节点挂了,可能会出问题,所以后来redis的作者就提出了redlock,我先叙述一下这个算法的流程。

  这个算法过程如下:
  1.获取服务器时间
  2.去各个节点获取锁
  3.再次获取服务器时间
  4.第三步的时间减去第一步的时间是否小于过期时间ttl
  5.如果小于,并且集群中多数节点都加锁成功,获取锁成功
  
  在这篇文章中:http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html,Martin批评说redis如此实现会有如下两个问题:
  1.如果线程A获取到锁,之后线程A进行了一个时间非常长的fullgc,导致线程A获取的锁过期,只时候线程B又获取到锁,假设线程A和B都要去更新数据库的同一条记录,如果B先更新了数据,之后A又更新数据库,这样子就会出问题,因为线程A先执行,正常情况下应该是线程A先去更新数据库,而不是线程B,这样就会导致数据错误。
  2.如果redis集群中有A,B,C,D,E五个节点,假设线程a获取锁,在A,B,C三个节点上上锁成功,但是由于redlock依赖于服务器的时间,如果服务器C的时间走的比较快,导致C上的锁过期,之后线程b进来在C,D,E上又上锁成功,那不就相当于一个系统中同时存在两个锁了,这时候锁就没有意义了。
  
  redis的作者也反驳了Martin,他认为第一点,只要有过期时间的锁都会有这个问题,不只是redis,关于第二点,antirez(redis作者)说服务器的时间会发生非正常的跳动有两种可能,第一是人为改动,第二是从NTP服务收到了一个跳跃时时钟更新。,第一种请求人为因素没有什么可说的,第二种需要将阶跃的时间更新到服务器的时候,应当采取小步快跑的方式。多次修改,每次更新时间尽量小。
 
总结:我个人认为,红锁的实现太锁复杂,因为要让redis的大多数节点都要加锁成功,如果是多线程并发的时候,都去竞争锁,会对redis的性能造成影响,同时Martin反驳所说的第一点其实是一个很大的问题,并没有很好的解决,所以推荐使用zookeeper来实现分布式锁。
 

十、redis常见java客户端?

 jedis

  最老牌的Java客户端,提供了非常丰富的api,但是有一个问题,就是在多线程使用jedis实例访问redis的时候,是线程不安全的,所以需要使用连接池,每个线程使用不同的jedis实例

lettuce

  spring-redis默认的Java客户端,线程安全

redission

  线程安全,提供了redis的分布式锁实现方式

建议:建议使用lettuce,如果有分布式上的需求,推荐使用redission

 

参考文章

史上最全Redis面试49题(含答案):哨兵+复制+事务+集群+持久化等

https://www.cnblogs.com/wuhen8866/p/11882142.html

以上是关于redis原理及常见问题的主要内容,如果未能解决你的问题,请参考以下文章

Redis集群分片原理及选举流程

redis主从同步原理及优化

redis主从同步原理及优化

详解Redis 主从复制及主从复制原理

redis集群搭建及原理

基于centos 7的Redis群集原理及配置