Redis 知识点整理

1. 应用场景

  • 秒杀库存扣减
  • 首页访问流量高峰

2. 基础知识

2.1 特性

基于内存的单进程单线程模型的KV数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数);

完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程切换导致的消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

使用I/O多路复用模型,非阻塞IO;

使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

  1. 那他是单线程的,我们现在服务器都是多核的,那不是很浪费?
    他是单线程的,但是,我们可以通过在单机开多个Redis实例嘛。

  2. 既然提到了单机会有瓶颈,那你们是怎么解决这个瓶颈的?
    我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster支撑N个Redis master node,每个master node都可以挂载多个slave node。
    这样整个Redis就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的master节点,每个master节点就能存放更多的数据了。

2.2 线程模型

Redis内部使用文件事件处理器(file event handler),这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。
它采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含4个部分:

  1. 多个Socket
  2. IO多路复用程序
  3. 文件事件分派器
  4. 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个Socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是I/O多路复用程序会监听多个Socket,会将Socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

2.3 数据结构

基础数据类型:字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。

还有HyperLogLog、Geo、Pub/Sub;

Redis Module,像BloomFilter,RedisSearch,Redis-ML。

2.3.1 字符串 String:普通的set和get,做简单的KV缓存

应用场景:

  1. 缓存功能:String字符串是最常用的数据类型,利用Redis作为缓存,再配合其它数据库作为存储层。利用Redis支持高并发的特点,可以大大加快系统的读写速度、以及降低后端数据库的压力。
  2. 计数器:使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
  3. 共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,每次用户Session的更新和获取都可以快速完成,大大提高效率。

2.3.2 字典 Hash:类似Map的一种结构。

应用场景:

  1. 可以将结构化的数据给缓存在Redis里,比如一个对象(前提是这个对象没嵌套其他的对象),然后每次读写缓存的时候,可以就操作Hash里的某个字段。

2.3.3 List:有序列表

应用场景:

  1. 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西,通过lrange命令,读取某个闭区间内的元素,可以基于List实现分页查询,基于Redis实现简单的高性能分页,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
  2. 可以搞个简单的消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使用右进左出的命令组成来完成队列的设计。比如:数据的生产者可以通过Rpush命令从右边插入数据,多个数据消费者,可以使用BLpop命令阻塞的“抢”列表头部的数据。

2.3.4 Set:自动去重的无序集合

应用场景:

  1. 进行全局的Set去重
  2. 进行交集、并集、差集的操作,比如把两个人的好友列表整一个交集

2.3.5 Sorted Set:去重可排序的Set

写进去的时候给一个分数,自动根据分数排序,利用分数进行成员间的排序,而且是插入时就排序好。

应用场景:

  1. 排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。
  2. 用Sorted Sets来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
  3. 微博热搜榜,就是有个后面的热度值,前面就是名称

2.3.6 Redis高级用法

类型 用法
Bitmap 位图是支持按bit位来存储信息,可以用来实现布隆过滤器(BloomFilter)
基数统计 HyperLogLog 供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计UV
地理信息 Geospatial 可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等
发布订阅 Pub/Sub 功能是订阅发布功能,可以用作简单的消息队列
管道 Pipeline 可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答
脚本 Lua Redis支持提交Lua脚本来执行一系列的功能

2.4 keys命令

  1. 假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
    使用keys指令可以扫出指定模式的key列表。

  2. 如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
    redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

    使用SMEMBERS命令可以返回集合键当前包含的所有元素,但是对于SCAN这类增量式迭代命令来说,因为在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证。

2.5 del命令

删除命令del是阻塞的,如果密钥与包含数百万个元素的聚合值,服务器可以阻止长时间(甚至几秒钟)以完成操作。
由于上述原因,Redis还提供了非阻塞删除原语,例如UNLINK(非阻塞DEL)和FLUSHALL的ASYNC选项,以及
FLUSHDB命令,以便在后台回收内存。这些命令在固定时间内执行。另一个线程将逐步释放尽可能快地在后台对象。

2.6 异步队列

一般使用list结构作为队列,Rpush生产消息,Lpop消费消息。当Lpop没有消息的时候,要适当sleep一会再重试。

  1. 如果对方追问可不可以不用sleep呢?
    list还有个指令叫BLpop,在没有消息的时候,它会阻塞住直到消息到来。

  2. 如果对方接着追问能不能生产一次消费多次呢?
    使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

  3. pub/sub有什么缺点?
    在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如RocketMQ等。

2.7 延时队列

使用sorted set,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

2.8 管道 pipeline

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:

  1. 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
  2. 服务端处理命令,并将结果返回给客户端。

Redis管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应。可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。

使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

3. 持久化

RDB(redis database)做镜像全量持久化,是对Redis中的数据执行周期性的持久化。

AOF(append only file)做增量持久化,对每条写入命令作为日志,以append-only的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。

两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF更适合做热备。

  1. RDB(redis database)优缺点:
    优点:
    他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis里面的数据,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。

    RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,采用二进制压缩存储,而且他在数据恢复的时候速度比AOF来的快。

    缺点:
    RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。

    还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,就会出大问题。

  2. AOF(append only file)优缺点:
    优点:
    上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。

    AOF在对日志文件写入操作是以追加模式,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。有灵活的同步策略,支持每秒同步、每次修改同步和不同步。

    AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了。

    缺点:
    相同规模的数据集,AOF要大于RDB,AOF在运行效率上往往会慢于RDB。

    AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志。

  3. 机器断电后对数据丢失的影响
    取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

  4. RDB原理
    fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

  5. 那两者怎么选择?
    两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

    把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,你再回放一下日志,数据不就完整了嘛。不过Redis本身的机制是AOF持久化开启且存在AOF文件时,优先加载AOF文件;AOF关闭或者AOF文件不存在时,加载RDB文件;加载AOF/RDB文件后,Redis启动成功;AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

    单独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么时候第一时间用RDB恢复,然后AOF做数据补全。

4. 缓存

缓存是高并发场景下提高热点数据访问性能的一个有效手段。

4.1 类型

缓存的类型分为:本地缓存、分布式缓存和多级缓存。

本地缓存:
本地缓存就是在进程的内存中进行缓存,比如我们的JVM堆中,可以用LRUMap来实现,也可以使用Ehcache这样的工具来实现。

本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。

分布式缓存:
分布式缓存可以很好得解决这个问题。

分布式缓存一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。

多级缓存:
为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。

4.2 内存淘汰机制

key失效机制
Redis 的key可以设置过期时间,过期后Redis采用主动和被动结合的失效机制,一个是和Memcache一样在访问时触发被动删除,另一种是定期的主动删除。

最大内存 maxmemory
可以设置内存使用限制为指定的字节数。当达到内存限制时,Redis将尝试删除密钥,根据选择的淘汰机制。如果Redis无法根据策略删除密钥,或者策略为设置为’noeviction’,Redis将开始回复命令错误将使用更多的内存,例如SET,LPUSH等,并将继续回复诸如GET之类的只读命令。

  1. Redis的过期策略,是有定期删除+惰性删除两种。
    定期删除:默认100s就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了;

    惰性删除:如果一直没随机到很多key,就会惰性删除,我不主动删,我等你来查询了我看看你过期没,过期就删了还不给你返回,没过期该怎么样就怎么样

  2. 如果定期没删,我也没查询,那可咋整?
    内存淘汰机制!

    不管是本地缓存还是分布式缓存,为了保证较高性能,都是使用内存来保存数据,由于成本和内存限制,当存储的数据超过缓存容量时,需要对缓存的数据进行剔除。

内存淘汰机制 解释
noeviction 不要逐出任何东西,只需在写操作中返回错误。
allkeys-lru 尝试回收最近最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru 尝试回收最近最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-lfu 尝试回收最少使用的键(LFU),使得新添加的数据有空间存放。
volatile-lfu 尝试回收最少使用的键(LFU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random 回收随机的键,使得新添加的数据有空间存放。
volatile-random 回收随机的键,使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction差不多了。

4.3 缓存雪崩、击穿、穿透

4.3.1 缓存雪崩

缓存雪崩产生的原因是缓存挂掉,这时所有的请求都会穿透到 DB。

目前电商首页以及热点数据都会去做缓存,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒6000个请求,本来缓存在可以扛住每秒5000个请求,但是缓存当时所有的Key都失效了。此时1秒6000个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。

解决方法:

  1. 在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效;
    setRedis(Key,value,time + Math.random() * 10000);
    
  2. 如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题;
  3. 设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险;
  4. 使用快速失败的熔断策略,减少 DB 瞬间压力;
  5. 使用主从模式和集群模式来尽量保证缓存服务的高可用;

4.3.2 缓存击穿

缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

解决方法:

  1. 设置热点数据永远不过期;
  2. 使用互斥锁更新,保证同一个进程中针对同一个数据不会并发请求到DB,减小DB压力;
  3. 使用随机退避方式,失效时随机sleep一个很短的时间,再次查询,如果失败再执行更新;

4.3.3 缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的id都是1开始自增上去的,如发起为id值为-1的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。

解决方法:

  1. 在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id做基础校验,id<=0的直接拦截等。
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx可以让运维对单个IP每秒访问次数超出阈值的IP都拉黑。
  3. 使用布隆过滤器(Bloom Filter),特点是存在性检测,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在就return,存在你就去查了DB刷新KV再return。

4.4 缓存常见问题

  1. 缓存更新方式?
    缓存的数据在数据源发生变更时需要对缓存进行更新,数据源可能是DB,也可能是远程服务。更新的方式可以是主动更新。

    数据源是DB时,可以在更新完DB后就直接更新缓存。

    当数据源不是DB而是其他远程服务,可能无法及时主动感知数据变更,这种情况下一般会选择对缓存数据设置失效期,也就是数据不一致的最大容忍时间。

    这种场景下,可以选择失效更新,key不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可用。

    改进的办法是异步更新,就是当失效时先不清除数据,继续使用旧的数据,然后由异步线程去执行更新任务。这样就避免了失效瞬间的空窗期。另外还有一种纯异步更新方式,定时对数据进行分批更新。实际使用时可以根据业务场景选择更新方式。

  2. 数据不一致?
    缓存不一致产生的原因一般是主动更新失败,例如更新DB后,更新Redis因为网络原因请求超时;或者是异步更新失败导致。

    解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

5. 最经典的KV、DB读写模式

最经典的缓存+数据库读写的模式,就是Cache Aside Pattern。

读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

更新的时候,先更新数据库,然后再删除缓存。

  1. 为什么是删除缓存,而不是更新缓存?
    很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。

    比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

    另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?

    举个栗子:一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存更新20次、100次;但是这个缓存在1分钟内只被读取了1次,有大量的冷数据。

    实际上,如果你只是删除缓存的话,那么在1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

    使用set的情况:操作缓存成功,操作数据库失败,会导致,缓存里是set后的数据,数据库里是之前的数据,数据不一致,业务无法接受。并且,一般来说,数据最终以数据库为准,写缓存成功,其实并不算成功。

    使用delete的情况:操作缓存成功,操作数据库失败,会导致,缓存里没有数据,数据库里是之前的数据,数据没有不一致,对业务无影响。只是下一次读取,会多一次cache miss。

    如果涉及很多其他的逻辑操作,应该淘汰缓存;如果只是更改缓存中的值,无其他逻辑操作,可以直接更新。

6. 事务

Redis事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  1. 批量操作在发送 EXEC 命令前被放入队列缓存。
  2. 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
  3. 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:开始事务;命令入队;执行事务。

Redis提供的不是严格的事务,Redis只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去。

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

7. 分布式锁

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

  1. 如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?
    这个锁就永远得不到释放了。set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

  2. 你有没有考虑过,如果你多个系统同时操作(并发)Redis带来的数据问题?
    系统A、B、C三个系统,分别去操作Redis的同一个Key,本来顺序是1,2,3是正常的,但是因为系统A网络突然抖动了一下,B,C在他前面操作了Redis,这样数据不就错了么。

    就好比下单,支付,退款三个顺序你变了,你先退款,再下单,再支付,那流程就会失败,那数据不就乱了?你订单还没生成你却支付,退款了?明显走不通了,这在线上是很恐怖的事情。

  3. 那这种情况怎么解决呢?
    某个时刻,多个系统实例都去更新某个 key。可以基于Zookeeper实现分布式锁。每个系统通过Zookeeper获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。

    你要写入缓存的数据,都是从MySQL里查出来的,都得写入MySQL中,写入MySQL中的时候必须保存一个时间戳,从MySQL查出来的时候,时间戳也查出来。

    每次要写之前,先判断一下当前这个Value的时间戳是否比缓存里的Value的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。

  4. 你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
    解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不一致不会影响业务,那么只要下次更新时可以成功,能保证最终一致性就可以。

    一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求“缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。

    串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。把一些列的操作都放到队列里面,顺序肯定不会乱,但是并发高了,这队列很容易阻塞,反而会成为整个系统的弱点,瓶颈。

8. 集群的高可用

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis支持主从同步,提供Cluster集群部署模式,通过Sentinel哨兵来监控Redis主服务器的状态。当主挂掉时,在从节点中根据一定策略选出新主,并调整其他从slaveof到新主。

  1. 为啥要用主从这样的架构模式?
    单机QPS是有上限的,而且Redis的特性就是必须支撑读高并发的,那你一台机器又读又写,这谁顶得住啊,不当人啊!但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。

Redis选主的策略简单来说有三个:

  1. slave的priority设置的越低,优先级越高;
  2. 同等情况下,slave复制的数据越多优先级越高;
  3. 相同的条件下runid越小越容易被选中。

Redis Cluster使用分片机制,在内部分为16384个slot插槽,分布在所有master节点上,每个master节点负责一部分slot。数据操作时按key做CRC16来计算在哪个slot,由哪个master进行处理。数据的冗余是通过slave节点来保障。

哨兵集群sentinel
哨兵必须用三个实例去保证自己的健壮性的,哨兵+主从并不能保证数据不丢失,sentinel之间通过Raft协议来保证自身的高可用。

  1. 为啥必须要三个实例呢?我们先看看两个哨兵会咋样。
    master宕机了s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障,但是这个时候也需要大多数哨兵都是运行的。

    M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2个裸屌了,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移就是不执行。

总结下哨兵组件的主要功能:

  1. 集群监控:负责监控Redis master和slave进程是否正常工作。
  2. 消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  3. 故障转移:如果master node挂掉了,会自动转移到slave node上。
  4. 配置中心:如果故障转移发生了,通知client客户端新的 master 地址。

9. 集群的同步机制

主从同步
数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得Redis可执行单层树复制。存盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

  1. 全量同步
    Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
    1)从服务器连接主服务器,发送SYNC命令;
    2)主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
    3)主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
    4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
    5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
    6)从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
    完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

  2. 增量同步
    Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
    增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

注意点
如果多个Slave断线了,需要重启的时候,因为只要Slave启动,就会发送sync请求和主机全量同步,当多个同时出现的时候,可能会导致Master IO剧增宕机。

10. 与Memcache区别

Redis支持复杂的数据结构
Redis相比Memcached来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作,Redis会是不错的选择。

Redis原生支持集群模式
在redis3.x版本中,便能支持Cluster模式,而Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。

性能对比
由于Redis只使用单核,而Memcached可以使用多核,所以平均每一个核上Redis在存储小数据时比Memcached性能更高。而在100k以上的数据中,Memcached性能要高于Redis,虽然Redis最近也在存储大数据的性能上进行优化,但是比起 Remcached,还是稍有逊色。

Memcache
Memcache处理请求时使用多线程异步IO的方式,可以合理利用CPU多核的优势,性能非常优秀;

Memcache功能简单,使用内存存储数据;

Memcache的内存结构以及钙化问题我就不细说了,大家可以查看官网了解下;

Memcache对缓存的数据可以设置失效期,过期后的数据会被清除;

失效的策略采用延迟失效,就是当再次使用数据时检查是否失效;

当容量存满时,会对缓存中的数据进行剔除,剔除时除了会对过期key进行清理,还会按LRU策略对数据进行剔除。

另外,使用MC有一些限制,这些限制在现在的互联网场景下很致命,成为大家选择Redis、MongoDB的重要原因:
key不能超过250个字节;

value不能超过1M字节;

key的最大失效时间是30天;

只支持K-V结构,不提供持久化和主从同步功能。

11. 学习思路

一般避免以上情况发生我们从三个时间段去分析下:

事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃;

事中:本地ehcache缓存 + Hystrix限流+降级,避免MySQL被打死;

事后:Redis 持久化RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据;

好处:
数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。只要数据库不死,就是说,对用户来说,3/5的请求都是可以被处理的。 只要有3/5的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。

12. 考点

面试的时候问你缓存,主要是考察缓存特性的理解,对 MC、Redis 的特点和使用方式的掌握。

要知道缓存的使用场景,不同类型缓存的使用方式,例如:

  1. 对 DB 热点数据进行缓存减少 DB 压力;对依赖的服务进行缓存,提高并发性能;
  2. 单纯 K-V 缓存的场景可以使用 MC,而需要缓存 list、set 等特殊数据格式,可以使用 Redis;
  3. 需要缓存一个用户最近播放视频的列表可以使用 Redis 的 list 来保存、需要计算排行榜数据时,可以使用 Redis 的 zset 结构来保存。
    要了解 MC 和 Redis 的常用命令,例如原子增减、对不同数据结构进行操作的命令等。

了解 MC 和 Redis 在内存中的存储结构,这对评估使用容量会很有帮助。

了解 MC 和 Redis 的数据失效方式和剔除策略,比如主动触发的定期剔除和被动触发延期剔除

要理解 Redis 的持久化、主从同步与 Cluster 部署的原理,比如 RDB 和 AOF 的实现方式与区别。

要知道缓存穿透、击穿、雪崩分别的异同点以及解决方案。

不管你有没有电商经验我觉得你都应该知道秒杀的具体实现,以及细节点。

13. 加分项

是要结合实际应用场景来介绍缓存的使用。例如调用后端服务接口获取信息时,可以使用本地+远程的多级缓存;对于动态排行榜类的场景可以考虑通过 Redis 的 Sorted set 来实现等等。

最好你有过分布式缓存设计和使用经验,例如项目中在什么场景使用过 Redis,使用了什么数据结构,解决哪类的问题;使用 MC 时根据预估值大小调整 McSlab 分配参数等等。

最好可以了解缓存使用中可能产生的问题。比如 Redis 是单线程处理请求,应尽量避免耗时较高的单个请求任务,防止相互影响;Redis 服务应避免和其他 CPU 密集型的进程部署在同一机器;或者禁用 Swap 内存交换,防止 Redis 的缓存数据交换到硬盘上,影响性能。再比如前面提到的 MC 钙化问题等等。

要了解 Redis 的典型应用场景,例如,使用 Redis 来实现分布式锁;使用 Bitmap 来实现 BloomFilter,使用 HyperLogLog 来进行 UV 统计等等。

知道 Redis4.0、5.0 中的新特性,例如支持多播的可持久化消息队列 Stream;通过 Module 系统来进行定制功能扩展等等。

14. 问题

  1. 在集群模式下,Redis 的 Key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 Hash 算法吗?
  2. 使用Redis有哪些好处?
  3. Redis相比Memcached有哪些优势?
  4. Redis常见性能问题和解决方案
  5. MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?
  6. Memcache与Redis的区别都有哪些?
  7. Redis 常见的性能问题都有哪些?如何解决?
  8. 在什么样的场景下可以充分的利用Redis的特性,大大提高Redis的效率?
  9. Redis的缓存雪崩、穿透、击穿了解么?有什么异同点?分别怎么解决?
  10. Redis的基本类型有哪些?他们的使用场景了解么?比较高级的用法你使用过么?
  11. Redis主从怎么同步数据的?集群的高可用怎么保证?持久化机制了解么?
  12. 为什么 redis 单线程却能支撑高并发?
  13. 如何保证缓存和数据库数据的一致性?
  14. 项目中是怎么用缓存的,用了缓存之后会带来什么问题?

参考:
《吊打面试官》系列-Redis基础
《吊打面试官》系列-缓存雪崩、击穿、穿透
《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
《吊打面试官》系列-Redis终章
《吊打面试官》系列-Redis常见面试题(带答案)
《吊打面试官》系列-秒杀系统设计
「课代表」帮你总结了全网最全的Redis知识点
I/O多路复用技术(multiplexing)是什么?
缓存,究竟是淘汰,还是修改?
究竟先操作缓存,还是数据库?
Redis 的主从同步,及两种高可用方式
Redis 教程
Redis 中文官网 redis.conf

发布了57 篇原创文章 · 获赞 11 · 访问量 9863

猜你喜欢

转载自blog.csdn.net/qq_36160730/article/details/103427452