基础
    缓存类型
    本地缓存:本地缓存就是在进程的内存中进行缓存,比如我们的 JVM 堆中,可以用 LRUMap 来实现,也可以使用 Ehcache 这样的工具来实现。本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,一般缓存较小且无法扩展。
    分布式缓存:一般都具有良好的水平扩展能力,对较大数据量的场景也能应付自如。缺点就是需要进行远程请求,性能不如本地缓存。
    多级缓存:为了平衡这种情况,实际业务中一般采用多级缓存,本地缓存只保存访问频率最高的部分热点数据,其他的热点数据放在分布式缓存中。
    Redis是单线程还是多线程
    单线程指的是网络请求模块使用了一个线程(所以不需考虑并发安全性),即一个线程处理所有网络请求,其他模块仍用了多个线程
    Redis 用 TCP 协议 Socket 来监听和读写网络请求,将需要监听的事件放入 Epoll(IO多路复用)事件管理器,然后收集触发的事件并循环一个一个处理进行相应的命令处理
    Redis 是单线程的,服务器是多核的会不会浪费资源
    在 Redis 6.0版本之后开始引入了多线程处理网络请求,将读取网络数据到输入缓冲区、协议解析和将执行结果写入输出缓冲区的过程变为了多线程,执行命令仍然是单线程。之所以这么设计是不想 Redis 因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。多线程 IO 的读(请求)和写(响应)在实现流程是一样的,只是执行读还是写操作的差异并且这些 IO 线程在同一时刻只能全部是读或者写。
    Redis 6.0版本的流程就会变为如下:

    主线程负责接收连接请求,有事件到来(收到请求)则放到一个全局等待处理队列
    主线程处理完请求后,通过轮询将所有连接分配给这些 IO 线程,然后主线程处于等待状态
    IO 线程读取请求的网络数据并解析完成(这里只是读数据和解析,并不执行)
    最后由主线程串行化执行所有命令并清空整个请求等待处理队列
    主线程再次将每个事件对应的结果分配给 IO 线程去并行返回请求结果
    Redis为什么不使用多线程
    Redis 核心就是如果我的数据全都在内存里,我单线程去操作就是效率最高的,为什么呢?因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换。对于一个内存的系统来说,它没有上下文的切换就是效率最高的。Redis 用单个 CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案。
    单线程情况下没有数据一致性问题,所以不需要竞争锁或进行 CAS 操作。
    Redis 为什么那么快
    完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
    采用单线程,避免了上下文切换、竞争条件和创建、销毁线程等一系列操作
    使用 Epoll (多路I/O复用模型),非阻塞IO;
    Redis 数据结构简单,操作简单
    Redis直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
    Redis 单线程模型
    Redis 是基于 Reactor 模式开发了自己的网络事件处理器, 这个处理器被称为文件事件处理器。这个事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
    文件事件处理器的结构包含 4 个部分:

    多个 Socket
    IO 多路复用程序
    文件事件分派器
    事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
    多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,文件事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
    Redis 6.0版本之后将连接应答处理器及命令回复处理器修改为了多线程,只有命令请求处理器仍然是单线程的。

    IO多路复用(Epoll)原理
    简单描述:

    执行 epoll_create 函数会在内核的高速缓存区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着应用程序执行 epoll_ctl 函数添加文件描述符会在红黑树上增加相应的结点。
    执行 epoll_ctl 的 add 操作时,不仅将文件描述符放到红黑树上,而且也注册了 callBack 函数。内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
    执行 epoll_wait 函数只用观察就绪链表中有无数据即可,最后将链表的数据及就绪的数量返回给应用程序,应用程序只需要遍历依次处理即可。这里返回的文件描述符是通过内存映射函数 mmap 让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。
    mmap:将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间。同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间、用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
    具体原理可以查看下面这篇博客:
    https://blog.csdn.net/armlinuxww/article/details/92803381

    Epoll 更高效的原因
    select、poll 都需要将有关文件描述符的数据结构拷贝进内核,最后再返回可读/可写的文件描述符集合给用户态。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用 mmap 函数文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。
    select、poll 采用轮询的方式来检查文件描述符是否处于就绪态,而 epoll 采用回调机制。造成的结果就是,随着 fd 的增加,select 和 poll 的效率会线性降低,而 epoll 不会受到太大影响,除非活跃的 socket 很多。
    Redis 的瓶颈
    因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈是机器内存的大小或者网络带宽。
    我们可以通过 Redis 自带的测试脚本 redis-benchmark(一般在/usr/local/bin目录下)进行测试

    redis-benchmark -h 127.0.0.1 -c 500 -n 1000000 -q -t set
    1

    Redis 单机的一秒钟处理请求数可以达到10万以上,随着跨服务器、跨局域网导致网络IO的消耗每秒钟可以处理的请求数逐步降低。

    Redis 的优势
    速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
    支持丰富数据类型,支持string,list,set,sorted set,hash等
    支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
    丰富的特性:可用于缓存,消息队列,分布式锁等,按key设置过期时间,过期后将会自动删除
    技术选型:为什么选择用 Redis 做缓存
    Memcached 存储类型只有字符串,Redis 支持更为丰富的数据类型
    Memcached 部署集群需要依赖第三方,Redis 本身支持集群部署
    Redis 可以持久化数据到磁盘上,防止服务器重启后数据丢失
    Memcached 挂掉后数据不可恢复,Redis数据丢失后可以通过Aof日志文件进行恢复
    Redis 的数据类型
    常用的五种数据类型:

    String:可以存储字符串、整数、浮点数,允许设置过期时间自动删除。
    Hash:包含键值对的无序散列表。通过 “数组 + 链表” 的链地址法来解决部分哈希冲突,value 只能是字符串。
    List:双向链表,可以充当队列和栈的角色。
    Set:相当于Java 中的 HashSet ,内部的键值对是无序、唯一的。key 就是元素的值,value 为 null。
    Zset:相当于 Java 中的 SortedSet 和 HashMap 的结合体,内部的键值对是有序、唯一的。可以为每个元素赋予一个 score 值,用来代表排序的权重。
    不常用的4种数据类型:

    BitMap:即位图,其实也就是 byte 数组,用二进制表示,只有 0 和 1 两个数字。实际上来说是归属于 String 类型下面。
    Hyperloglogs:用来做基数统计的算法。
    Geospatial:将用户给定的地理位置信息储存起来, 并对这些信息进行操作。
    Pub/Sub:支持多播的可持久化的消息队列,用于实现发布订阅功能。
    每个类型都有属于自己的应用场景。

    五种基本数据类型的底层数据结构
    类型 编码 对象 描述
    REDIS_STRING INT 使用整数值实现的字符串对象 如果一个字符串可以转成为long类型,对象类型就会使用int类型表示
    REDIS_STRING EMBSTR 使用 embstr 编码的简单动态字符串实现的字符串对象 当字符串对象的长度小于44个字节时使用,RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配
    REDIS_STRING RAW 简单动态字符串实现的字符串对象 当字符串对象的长度大于44个字节时使用,RedisObject 对象头和 SDS 对象是分开存储,需要使用 malloc 方法进行两次分配
    REDIS_LIST ZIPLIST(3.2版本之前) 使用压缩列表实现的列表对象 节省内存空间,因为它所存储的内容都是在连续的内存区域当中的。当列表对象元素不多,每个元素也不大的时候,就采用ziplist存储。当数据量过大时,每次插入都会重新进行 remalloc
    REDIS_LIST LINKEDLIST(3.2版本之前) 使用双向链表实现的列表对象 节点中存放pre和next两个指针,还有节点相关的信息。当每增加一个node的时候,就需要重新 malloc 一块内存。
    REDIS_LIST QUICKLIST(3.2版本开始) 使用压缩列表和双向链表结合实现的列表对象 将 linkedList 按段切分,每一段 quickListNode 使用 zipList 来紧凑存储多个数据,多个 quickListNode 之间使用双向指针串接起来。
    REDIS_HASH ZIPLIST 使用压缩列表实现的哈希对象 压缩列表按照key1,value1,key2,value2这样的顺序存放来存储的。当对象数目不多且内容不大时,这种方式效率是很高的。
    REDIS_HASH HASHTABLE 使用字典实现的哈希对象 当一个哈希对象包含的键值对比较多或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
    REDIS_SET INTSET 使用整数集合实现的集合对象 当存储对象都是整数值并且元素个数不超过512个时
    REDIS_SET HASHTABLE 使用字典实现的集合对象 同上,每次增加 value 时需判断 key 对应的 value 是否已存在
    REDIS_ZSET ZIPLIST 使用压缩列表实现的有序集合对象 同上,当有序集合保存的元素个数小于128并且所有元素的长度小于64个字节时
    REDIS_ZSET SKIPLIST 使用跳跃表+字典实现的有序集合对象 跳表是一种随机化的数据结构,是一种可以与平衡树媲美的层次化链表结构。在查找、删除、添加等操作都可以在对数期望时间下完成。
    注意:3.2版本之前是以39个字节为界限,使用embstr或row。3.2版本开始修改为44个字节为界限。

    SDS 是什么?有什么优点?
    SDS 是 Redis 字符串底层的具体实现,embstr 和 row 是由 RedisObject 和 SDS 构成。
    SDS 相比于 C 语言的字符数组实现有什么优势?

    常数复杂度获取字符串长度
    杜绝缓冲区溢出
    减少修改字符串时内存重分配的次数
    二进制安全:程序不会执行更改文件内容的功能或操作,避免不同编码读取文件内容导致数据错乱的问题。客户端按照存储数据时相同的编码读取数据,即可得到原有值。
    Redis 常见使用场景
    缓存——提升热点数据的访问速度
    共享数据——数据的存储和共享的问题
    全局 ID —— 分布式全局 ID 的生成方案(分库分表)
    分布式锁——进程间共享数据的原子操作保证
    在线用户统计和计数 —— 使用位图进行位运算
    队列、栈——跨进程的队列/栈
    消息队列——异步解耦的消息机制
    服务注册与发现 —— RPC 通信机制的服务协调中心(Dubbo 支持 Redis)
    共享用户 Session —— 用户Session的更新和获取都可以快速完成
    排行榜—— 通过分配元素 score 值进行排序
    为什么要禁止使用 keys 指令?
    原因:因为 Redis 是单线程的,如果存储的数据量大 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕服务才能恢复。
    解决:这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表。绝大多数情况下是可以替代keys命令的,可选性更强。
    介绍一下 Redis 中 Zset 类型的跳跃表
    当 Zset 存储的元素数量小于 128 个并且所有元素的长度都小于 64 字节时使用 Zlist(有序链表)来存储数据,任何一个条件不满足就会进化成跳跃表进行存储。
    跳跃表(skiplist)是一种随机化的数据结构,是一种可以与平衡树媲美的层次化链表结构——查找、删除、添加等操作都可以在对数期望时间下完成。
    跳跃表 skiplist 受到多层链表结构的启发而设计出来的。按照生成链表的方式,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个新增节点随机出一个层数(level,默认最大值为64)。比如,一个节点随机出的层数是 3,那么就把它链入到第 1 层到第 3 层这三层链表中。这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)。

    进阶
    Redis 的分布式锁是怎么实现的
    通过 setNx(set if not exist) 方法实现,如果不存在则插入,其可以很好的用来实现我们的分布式锁。对于某个资源加锁我们只需要

    setNx resourceName value
    1
    这里有个问题,加锁了之后如果机器宕机那么这个锁就不会得到释放。所以会加入过期时间,加入过期时间需要和setNx同一个原子操作,在 Redis2.8 之前我们需要使用 Lua 脚本达到我们的目的,但是 Redis2.8 之后支持 nx 和 ex 操作是同一原子操作。

    set key value ex 5 nx
    ex:设置键的过期时间为 second 秒
    nx:只在键不存在时,才对键进行设置操作。
    1
    2
    3
    Redis 分布式锁在集群环境下的问题
    Redis 只做了主从集群,当 Master 节点由于宕机发生了主从切换,就会出现锁丢失的情况。

    客户端A在Redis 的 Master 节点上拿到锁,但是还没来的及同步到 Slave 节点就宕机了
    客户端B就可以从新的 Master 节点上拿到同一个资源的锁,就导致了并发问题
    RedLock 分布式锁
    使用 Redis 部署集群,至少3个Master节点。

    先获取当前时间 T1,然后向所有的 Master 节点执行获取锁的操作。与 Redis 单节点获取锁的逻辑一致,快速尝试获取锁,如果失败立即尝试下一个 Master 节点。(获取锁失败含节点不可用或锁已被其他线程持有)

    只要超过获取半数 Master 节点上的锁成功时,才会进入下一步;

    获取半数锁成功:计算获取锁的时间,锁的剩余过期时间 = 锁的过期时间 - (当前时间 - T1)。判断锁的剩余时间是否大于0,是则获取锁成功。

    获取半数锁失败或获取锁时间大于过期时间:立即释放在 Master 节点上持有的锁。

    需要获取超过半数 Master 节点的锁才算获取锁成功,不会存在两个客户端同时获取锁的情况

    集群中有Master 节点发生宕机,RedLock 产生的问题
    假设有三个 Master 节点( A、B、C),客户端获取 A和B两个节点上的锁成功后表示获取分布式锁成功可以执行业务;
    此时有一个 Master 节点B宕机,由于客户端B没来得及同步到Slave节点导致锁丢失。Slave 进行故障转移,选举出一个新的 Master 节点;
    此时另一个客户端会获取B和C节点上的锁成功,导致并发情况出现;
    解决方案:Master 延迟进行故障转移,延迟时间超过锁的过期时间就不会出现两个客户端获取同一个资源锁的情况

    Redis 怎么做异步队列
    一般使用list结构作为队列,rpush生产消息,blpop消费消息(在没有消息的时候,它会阻塞住直到消息到来),只能实现 1:1 的消息队列。
    使用pub/sub主题订阅者模式,只要有消息到了订阅的频道就会收到这条消息,可以实现 1:N 的消息队列。
    Pipeline 作用及注意点
    可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用Pipeline执行速度比逐条执行要快,特别是客户端与服务端的网络延迟越大,性能体现越明显。
    注意使用 Pipeline 组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的 Pipeline 命令完成。因为 Pipeline 是多条命令的组合,为了保证它的原子性,Redis 提供了简单的事务。
    Redis 持久化机制
    RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间、不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

    RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢复数据。
    AOF 采用日志的形式来记录每个写操作并追加到文件中,Redis 默认不开启。它的出现是为了弥补 RDB 的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启时会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
    RDB 和 AOF 各自优缺点
    RDB 优势

    RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
    生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘 IO 操作。
    RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
    RDB 劣势

    RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。
    在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。
    AOF 优势

    AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。
    AOF 在增量日志达到64M(默认,可配置)的时候,会自动触发重建操作。将当前 Redis 内存的数据以 RDB 的方式写入日志文件中,后续客户端的增删改操作继续以日志的形式追加。
    AOF 劣势

    对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB存的是数据快照)。
    虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证。
    注意:当 Redis 启动时, 如果 RDB 持久化和 AOF 持久化都被打开了, 那么程序会优先使用 AOF 文件来恢复数据集, 因为 AOF 文件所保存的数据通常是最完整的。

    Copy-on-write(写时复制)
    Redis 会通过创建子进程来进行 RDB 操作,子进程创建后,父子进程共享数据段。直到父进程试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给父进程,而子进程所见到的最初的资源仍然保持不变。

    RDB 和 AOF 多久写一次磁盘?
    RDB 默认有以下三个规则,满足其一就会进行磁盘写入

    save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
    save 300 10 # 400 秒内至少有 10 个 key 被修改
    save 60 10000 # 60 秒内至少有 10000 个 key 被修改
    1
    2
    3
    AOF 持久化策略(硬盘缓存到磁盘),默认 everysec

    no 表示不执行 fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;
    always 表示每次写入都执行 fsync,以保证数据同步到磁盘,效率很低;
    everysec 表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。通常选择 everysec ,兼顾安全性和效率。
    AOF 重建为什么能减少文件大小
    因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子:如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
    为了处理这种情况, Redis 支持一种特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 BGREWRITEAOF 命令或当增量日志达到 64MB (默认,可配置)的时候, Redis 会 fork 出一个子进程负责生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令(即以 RDB 的存储数据格式)。而在子进程重建的过程中,客户端执行的操作会被保存在内存缓存区中。等待重建完成后,以操作日志的形式追加到日志文件的末尾。

    内存回收
    Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory)触发内存淘汰。

    定时过期(主动淘汰):每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
    惰性过期(被动淘汰):只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。第二种情况,每次写入 key 时,发现内存不够,调用 activeExpireCycle 释放一部分内存。
    定期过期:每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。如果有超过25%的key过期,那么会不断重复过期检测,直到过期的keys的百分比低于25%。这意味着,在任何给定的时刻,最多会清除1/4的过期keys。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
    Redis 中同时使用了惰性过期和定期过期两种过期策略。

    淘汰策略
    Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算决定清理掉哪些数据,以保证新数据的存入。
    redis.conf 淘汰策略设置:maxmemory-policy noeviction

    LRU:最近最少使用的
    LFU:使用评率最低的
    策略 含义
    volatile-lru 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。如果没有可删除的键对象,回退到 noeviction 策略。
    allkeys-lru 根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
    volatile-lfu 在带有过期时间的键中选择最不常用的。
    allkeys-lfu 在所有的键中选择最不常用的,不管数据有没有设置超时属性。
    volatile-random 在带有过期时间的键中随机选择。
    allkeys-random 随机删除所有键,直到腾出足够内存为止。
    volatile-ttl 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。
    noeviction 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时 Redis 只响应读操作。
    高频
    缓存雪崩
    原因:指 Redis 缓存在短时间内大面积失效,所有请求都直接访问数据库。可能导致数据库 CPU 瞬间飙升甚至宕机,由于大量的应用服务依赖数据库和 Redis 服务,这个时候很快会演变成各服务器集群的雪崩,最后网站彻底崩溃。

    解决:

    设置随机过期时间,避免同一时间大面积失效
    如果是集群部署,将热点数据均匀分布在不同的 Redis 库上避免全部失效
    设置热点数据永不过期,有更新缓存
    对源服务访问进行限流、资源隔离(熔断)、降级等
    如果是时点数据,可以进行预加载。等时点达到后,进行切换
    缓存穿透
    原因:缓存穿透是指查询一个一定不存在的数据。一个请求查询缓存没有命中,就需要从数据库查询。因为查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

    解决:

    查询前先做规则校验,不合法的数据直接拦截返回
    查询数据库没有数据也写一个 NULL 值到缓存里面并设置一个过期时间(不要超过1分钟,避免正常情况下也不能使用),下一次查询在缓存失效前就能命中缓存直接返回
    使用布隆过滤器,利用高效的数据结构和算法快速判断出这个 Key 是否在数据库中存在(需要提前将数据库所有数据放入到布隆过滤器的集合上,且布隆过滤器最大缺点就是无法删除数据。替换方案:布谷鸟过滤器)
    缓存击穿
    原因:某个 Key 非常热点、访问非常频繁,处于集中式高并发访问的情况。当这个 Key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

    解决:

    热点数据可以设置永不过期
    增加分布式锁,等待抢到锁的请求构建完缓存后再释放锁。其他未抢到锁的请求进行阻塞等待,被唤醒后重新请求缓存获取数据
    缓存更新
    我们应该什么时候去更新缓存,保证数据是实时、有效

    定期清理过期缓存
    设置自动过期时间,下一次查询直接从数据库读取重新缓存
    缓存预热
    指系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,直接查询数据库,然后再将数据缓存的问题。

    用户更新数据是更新缓存还是删除缓存
    如果选择更新缓存,可能在多次修改数据的情况下都没有一次读取会导致缓存被频繁更新降低性能。一般建议删除缓存,用户在读取的时候直接查询数据库保存最新值到缓存。

    先更新数据库还是先删除缓存
    先删除缓存,再更新数据库:在并发的情况下,可能线程A删除缓存还没来得及修改数据库,同时另一条线程B又从数据库重新读取旧数据插入到缓存中。这样就会导致脏数据存在,直到缓存失效或被删除为止。
    先更新数据库,再删除缓存:产生脏数据的概率较小,但是会出现一致性的问题:若更新操作的时候,同时进行查询操作则查询得到的数据是旧的数据。但是不会影响后面的查询,最多读取一次脏数据.
    LRU 算法实现
    LRU 是一个数据淘汰算法,淘汰掉最近最久未使用的数据。可以基于 Java 的 LinkedHashMap 进行实现