Redis基础与缓存问题

缓存有哪些类型?
缓存是⾼并发场景下提⾼热点数据访问性能的⼀个有效⼿段,在开发项⽬时会经常使⽤到。
缓存的类型分为:本地缓存、分布式缓存和多级缓存
本地缓存:
本地缓存就是在进程的内存中进⾏缓存,⽐如我们的 JVM 堆中,可以⽤ LRUMap 来实现,也可以使⽤Ehcache 这样的⼯具来实现。
本地缓存是内存访问,没有远程交互开销,性能最好,但是受限于单机容量,⼀般缓存较⼩且⽆法扩展。
分布式缓存:
分布式缓存可以很好得解决这个问题。
分布式缓存⼀般都具有良好的⽔平扩展能⼒,对较⼤数据量的场景也能应付⾃如。缺点就是需要进⾏远程请求,性能不如本地缓存。
多级缓存:
为了平衡这种情况,实际业务中⼀般采⽤多级缓存,本地缓存只保存访问频率最⾼的部分热点数据,其他的热点数据放在分布式缓存中。
这也是最常⽤的缓存⽅案,单考单⼀的缓存⽅案往往难以撑住很多⾼并发的场景。在这里插入图片描述
Redis 为什么是单线程的,单线程为什么执⾏速度这么快?
官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)Redis利用队列技术将并发访问变为串行访问
1)纯内存操作,避免⼤量访问数据库,减少直接读取磁盘数据,redis将数据储存在内存⾥⾯,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快。
2)单线程操作,避免了不必要的上下⽂切换和竞争条件,也不存在多进程或者多线程导致的切换⽽消耗CPU,不⽤去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁⽽导致的性能消耗
3)采⽤了⾮阻塞I/O多路复⽤机制,非阻塞IO优点:

  • 速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
  • 支持丰富数据类型,支持string,list,set,sorted set,hash
  • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
  • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除如何解决redis的并发竞争key问题

4)数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
5)使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
在这里插入图片描述

Redis 的线程模型
Redis 内部使⽤⽂件事件处理器 file event handler ,这个⽂件事件处理器是单线程的,所以 Redis才叫做单线程的模型。它采⽤ IO 多路复⽤机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进⾏处理。
⽂件事件处理器的结构包含 4 个部分:

  • 多个 Socket
  • IO 多路复⽤程序
  • ⽂件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
    在这里插入图片描述

多个 Socket 可能会并发产⽣不同的操作,每个操作对应不同的⽂件事件,但是 IO 多路复⽤程序会监听多个 Socket,会将 Socket 产⽣的事件放⼊队列中排队,事件分派器每次从队列中取出⼀个事件,把该事件交给对应的事件处理器进⾏处理。

Redis数据结构底层实现

在这里插入图片描述

String
(1)Simple dynamic string(SDS)的数据结构:
SDS 类似于 Java 中的 ArrayList,可以通过预分配冗余空间的⽅式来减少内存的频繁分配。

struct sdshdr{
    
    
//记录buf数组中已使⽤字节的数量
//等于 SDS 保存字符串的⻓度
int len;
//记录 buf 数组中未使⽤字节的数量
int free;
//字节数组,⽤于保存字符串
char buf[]}

它的优点: (1)不会出现字符串变更造成的内存溢出问题
(2)获取字符串⻓度时间复杂度为1 (3)空间预分配, 惰性空间释放free字段,会默认留够⼀定的空间防⽌多次重分配内存
应⽤场景: String 缓存结构体⽤户信息,计数

Hash:
数组+链表的基础上,进⾏了⼀些rehash优化;
1.Reids的Hash采⽤链地址法来处理冲突,然后它没有使⽤红⿊树优化。
2.哈希表节点采⽤单链表结构。
3.rehash优化 (采⽤分⽽治之的思想,将庞⼤的迁移⼯作量划分到每⼀次CURD中,避免了服务繁忙)
应⽤场景: 保存结构体信息可部分获取不⽤序列化所有字段

List: 有序列表
应⽤场景: (1):⽐如twitter的关注列表,粉丝列表等都可以⽤Redis的list结构来实现
(2):list的实现为⼀个双向链表,即可以⽀持反向查找和遍历

  • 消息队列:Redis的链表结构,可以轻松实现阻塞队列,可以使⽤左进右出的命令组成来完成队列的设计。⽐如:数据的⽣产者可以通过Lpush命令从左边插⼊数据,多个数据消费者,可以使⽤BRpop命令阻塞的“抢”列表尾部的数据。
  • ⽂章列表或者数据分⻚展示的应⽤。
    ⽐如,我们常⽤的博客⽹站的⽂章列表,当⽤户量越来越多时,⽽且每⼀个⽤户都有⾃⼰的⽂章列表,⽽且当⽂章多时,都需要分⻚展示,这时可以考虑使⽤Redis的列表,列表不但有序同时还⽀持按照范围内获取元素,可以完美解决分⻚查询功能。⼤⼤提⾼查询效率。

Set: 是⽆序集合,会⾃动去重的那种。
内部实现是⼀个 value为null的HashMap,实际就是通过计算hash的⽅式来快速排重的,这也是set能提供判断⼀个成员 是否在集合内的原因。

直接基于 Set 将系统⾥需要去重的数据扔进去,⾃动就给去重了,如果你需要对⼀些数据进⾏快速的全局去重,你当然也可以基于 JVM 内存⾥的 HashSet 进⾏去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进⾏全局的 Set 去重。
可以基于 Set 玩⼉交集、并集、差集的操作,⽐如交集吧,我们可以把两个⼈的好友列表整⼀个交集,看看俩⼈的共同好友是谁?对吧。反正这些场景⽐较多,因为对⽐很快,操作也简单,两个查询⼀个Set搞定。

Zset:
内部使⽤HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap⾥放的是成员到score的映射,⽽跳跃表⾥存放的是所有的成员,排序依据是HashMap⾥存的score,使⽤跳跃表的结构可以获得⽐较⾼的查找效率,并且在实现上⽐较简单。
跳表:每个节点中维持多个指向其他节点的指针,从⽽达到快速访问节点的⽬的
应⽤场景:

  • 实现延时队列:使⽤sortedset,拿时间戳作为score,消息内容作为key调⽤zadd来⽣产消息,消费者⽤zrangebyscore指令获取N秒之前的数据轮询进⾏处理。
  • 排⾏榜:有序集合经典使⽤场景。例如视频⽹站需要对⽤户上传的视频做排⾏榜,榜单维护可能是多⽅⾯:按照时间、按照播放量、按照获得的赞数等。

为啥redis zset使⽤跳跃链表⽽不⽤红⿊树实现?
(1):skiplist的复杂度和红⿊树⼀样,⽽且实现起来更简单。
(2):在并发环境下红⿊树在插⼊和删除时需要rebalance,性能不如跳表。

异步队列

如何使⽤Redis做异步队列?
⼀般使⽤list结构作为队列,rpush⽣产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep⼀会再重试。
可不可以不⽤sleep呢?
list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。
能不能⽣产⼀次消费多次呢?
使⽤pub/sub主题订阅者模式,可以实现1:N的消息队列。
pub/sub有什么缺点?
在消费者下线的情况下,⽣产的消息会丢失,得使⽤专业的消息队列如rabbitmq等。

延时队列

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

keys命令

1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使⽤keys指令可以扫出指定模式的key列表。 如果这个redis正在给线上的业务提供服务,那使⽤keys指令会有什么问题?

  • keys指令:redis的单线程的。keys指令会导致线程阻塞⼀段时间,线上服务会停顿,直到指令执⾏完毕,服务才能恢复。
  • scan指令:使⽤scan指令,scan指令可以⽆阻塞的提取出指定模式的key列表,但是会有⼀定的重复概率,在客户端做⼀次去重就可以了 ,但是整体所花费的时间会⽐直接⽤keys指令长。
  • SMEMBERS smembers命令可以返回集合键当前包含的所有元素
  • scan增量迭代命令,因为在对键的迭代过程中,键可能会被修改,所以增量迭代命令只能对返回的元素提高有限的保证

expire命令设置的过期时间是与电脑设备的时钟相关的,比如你设置某key的过期时间为1000,但是在1000之内的时间范围内,你修改了电脑的时间为2000之后,那么此key会立即过期。所以redis的过期时间不是要持续多长时间,而是和电脑时钟相关联。
setnx命令
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

特殊数据结构

Bitmap :
位图是⽀持按 bit 位来存储信息,可以⽤来实现 布隆过滤器(BloomFilter)和 日常打卡:统计用户信息,活跃,不活跃! 登录 、 未登录! 打卡,365打卡!;

位存储
Bitmap 位图,数据结构! 都是操作二进制位来进行记录,就只有0 和 1 两个状态!

HyperLogLog:
提供不精确的去重计数功能,⽐较适合⽤来做⼤规模数据的去重统计,例如统计 UV;
优点:占用的内存是固定,2^64 不同的元素的技术,只需要废 12KB内存!如果要从内存角度来比较的话 Hyperloglog 首选!
Geospatial:
可以⽤来保存地理位置,并作位置距离计算或者根据半径计算位置等。有没有想过⽤Redis来实现附近的⼈?或者计算最优地图路径?
pub/sub:
功能是订阅发布功能,可以⽤作简单的消息队列。
缺点:在消费者下线的情况下,⽣产的消息会丢失,得使⽤专业的消息队列如rabbitmq等。

其他用法

Pipeline:
可以批量执⾏⼀组指令,⼀次性返回全部结果,可以减少频繁的请求应答。可以将多次IO往返的时间缩减为⼀次,前提是pipeline执⾏的指令之间没有因果相关性。使⽤redisbenchmark进⾏压测的时候可以发现影响redis的QPS峰值的⼀个重要因素是pipeline批次指令的数⽬。
Lua:
Redis ⽀持提交 Lua 脚本来执⾏⼀系列的功能。
事务:
Redis 提供的不是严格的事务,Redis 只保证串⾏执⾏命令,并且能保证全部执⾏,但是执⾏命令失败时并不会回滚,⽽是会继续执⾏下去。
Redis事务没有没有隔离级别的概念!
所有的命令在事务中,并没有直接被执行!只有发起执行命令的时候才会执行!Exec
Redis单条命令式保存原子性的,但是事务不保证原子性!

Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
1.redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2.如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
3.如果在一个事务中出现运行错误,那么正确的命令会被执行。
1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

持久化

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。
Redis 提供了 RDB 和 AOF 两种持久化⽅式,
RDB 是把内存中的数据集以快照形式写⼊磁盘,实际操作是通过 fork ⼦进程执⾏,采⽤⼆进制压缩存储;Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件
在这里插入图片描述
原理:fork和cow。fork是指redis通过创建⼦进程来进⾏RDB操作,cow指的是copy on write,⼦进程创建后,⽗⼦进程共享数据段,⽗进程继续提供读写服务,写脏的页⾯数据会逐渐和⼦进程分离开来。

AOF 是以⽂本⽇志的形式记录 Redis 处理的每⼀个写⼊或删除操作。(读操作不记录)
在这里插入图片描述只许追加文件但不可以改写文件

RDB 把整个 Redis 的数据保存在单⼀⽂件中,⽅便数据恢复,最⼤化redis性能,恢复⼤数据集速度更快,⽐较适合⽤来做灾备,但缺点是快照保存完成之前如果宕机,这段时间的数据将会丢失,另外保存快照时可能导致服务短时间不可⽤。

AOF 对⽇志⽂件的写⼊操作使⽤的追加模式,有灵活的同步策略,⽀持每秒同步、每次修改同步和不同步,保存数据更完整,在redis重启是会重放这些命令来恢复数据,操作效率⾼,故障丢失数据更少,但是⽂件体积更⼤,缺点就是相同规模的数据集,AOF 要⼤于 RDB,AOF 在运⾏效率上往往会慢于 RDB。

这两种机制各⾃优缺点是啥?
RDB 冷备 是对数据执行周期性的持久化
优点:对性能影响小
1、适合大规模的数据恢复!
2、对数据的完整性要不高!
缺点:快照文件,默认5分钟生成一次,那容易丢失5分钟之内的数据,如果文件很大,客户端也可能暂停几毫秒或者几秒
1、需要一定的时间间隔进程操作!如果redis意外宕机了,这个最后一次修改数据就没有的了!
2、fork进程的时候,会占用一定的内容空间

AOF 热备 对每条写入命令作为日志,以append-only追加的模式写入一个日志文件中
优点:如果一秒一写,最多丢失一秒数据。 只用追加方式写数据,少去很多磁盘寻址开销
1、每一次修改都同步,文件的完整会更加好!
2、每秒同步一次,可能会丢失一秒的数据
3、从不同步,效率最高的!
缺点:一样的数据,AOF文件比RDB文件要大
1、相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
2、Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化

Redis如何做持久化
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致⼤量丢失数据 ,所以需要aof来配合使⽤。在redis实例重启时,会使⽤bgsave持久化⽂件重新构建内存,再使⽤aof重放近期的操作指令来 实 现完整恢复重启之前的状态。

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

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

⾼可⽤

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

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

  • slave 的 priority 设置的越低,优先级越⾼;
  • 同等情况下,slave 复制的数据越多优先级越⾼;
  • 相同的条件下 runid 越⼩越容易被选中。

在 Redis 集群中,sentinel 也会进⾏多实例部署,sentinel 之间通过 Raft 协议来保证⾃身的⾼可⽤。
Redis Cluster 使⽤分⽚机制,在内部分为 16384 个 slot 插槽,分布在所有 master 节点上,每个master 节点负责⼀部分 slot。数据操作时按 key 做 CRC16 来计算在哪个 slot,由哪个 master 进⾏处理。数据的冗余是通过 slave 节点来保障。

哨兵
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

哨兵必须⽤三个实例去保证⾃⼰的健壮性的,哨兵+主从并不能保证数据不丢失,但是可以保证集群的⾼可⽤。
为啥必须要三个实例呢?我们先看看两个哨兵会咋样。在这里插入图片描述
master宕机了 s1和s2两个哨兵只要有⼀个认为你宕机了就切换了,并且会选举出⼀个哨兵去执⾏故障,但是这个时候也需要⼤多数哨兵都是运⾏的。
那这样有啥问题呢?M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下S2一个了,没有哨兵去允许故障转移了,虽然另外⼀个机器上还有R1,但是故障转移就是不执⾏。

经典的哨兵集群
在这里插入图片描述
M1所在的机器挂了,哨兵还有两个,两个看M1他挂了,那我们之中就选举⼀个出来执⾏故障转移
主要功能:
集群监控:负责监控 Redis master 和 slave 进程是否正常⼯作。
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会⾃动转移到 slave node 上。
配置中⼼:如果故障转移发⽣了,通知 client 客户端新的 master 地址。

主从
master主写,数据同步给slave机器,slave主读,分发掉⼤量的请求⽽且轻松实现⽔平扩容。
在这里插入图片描述Redis的同步机制
(1):全量拷贝
1.slave第⼀次启动时,连接Master,发送PSYNC命令,
2.master会执⾏bgsave命令来⽣成rdb⽂件,期间的所有写命令将被写⼊缓冲区。
3. master bgsave执⾏完毕,向slave发送rdb⽂件
4. slave收到rdb⽂件,丢弃所有旧数据,开始载⼊rdb⽂件
5. rdb⽂件同步结束之后,slave执⾏从master缓冲区发送过来的所以写命令。
6. 此后 master 每执⾏⼀个写命令,就向slave发送相同的写命令。
7. 主节点根据偏移量把复制积压缓冲区⾥的数据发送给从节点,保证主从复制进⼊正常状态。
(2):增量拷贝 如果出现⽹络闪断或者命令丢失等异常情况,从节点之前保存了⾃身已复制的偏移量和主节点的运⾏ID
redis集群模式性能优化
(1) Master最好不要做任何持久化⼯作,如RDB内存快照和AOF⽇志⽂件
(2) 如果数据⽐较重要,某个Slave开启AOF备份数据,策略设置为每秒同步⼀次
(3) 为了主从复制的速度和连接的稳定性,Master和Slave最好在同⼀个局域⽹内
(4) 尽量避免在压⼒很⼤的主库上增加从库
(5) 主从复制不要⽤图状结构,⽤单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-Slave3…这样的结构⽅便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以⽴刻启⽤Slave1做Master,其他不变。

Redis缓存

在这里插入图片描述

redis的过期策略以及内存淘汰机制:定期+惰性+内存淘汰
缓存淘汰策略
(1):先进先出算法(FIFO)
(2):最近使⽤最少Least Frequently Used(LFU)
(3):最长时间未被使⽤的Least Recently Used(LRU)
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况⽐较严重
redis过期key删除策略
(1):惰性删除,cpu友好,但是浪费cpu资源
(2):定时删除(不常⽤)
(3):定期删除,cpu友好,节省空间
key 失效机制
Redis 的 key 可以设置过期时间,过期后 Redis 采⽤主动和被动结合的失效机制,⼀个是和 MC ⼀样在访问时触发被动删除,另⼀种是定期的主动删除。定期+惰性+内存淘汰

redis采用的是定期删除+惰性删除策略。
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.

定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
内存淘汰策略
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

手写LRU代码

leetcode LRU
linkedhashmap HashMap和双向链表

class LRUCache {
    
    
    private int capacity;
    private LinkedHashMap<Integer, Integer> map;
    public LRUCache(int capacity) {
    
    
        this.capacity = capacity;
        map = new LinkedHashMap<>();
    }
    
    public int get(int key) {
    
    
        if(map.containsKey(key)){
    
    
            int value = map.get(key);
            map.remove(key);
            map.put(key, value);
            return value;
        }else{
    
    
            return -1;
        }
    }
    
    public void put(int key, int value) {
    
    
        if(map.containsKey(key)){
    
    
            map.remove(key);
        }
        if(map.size() >= capacity){
    
    
            map.remove(map.keySet().iterator().next());
        }
        map.put(key, value);
    }
}

常见缓存问题

在这里插入图片描述

缓存更新⽅式
这是决定在使⽤缓存时就该考虑的问题。
缓存的数据在数据源发⽣变更时需要对缓存进⾏更新,数据源可能是 DB,也可能是远程服务。更新的⽅式可以是主动更新。数据源是 DB 时,可以在更新完 DB 后就直接更新缓存。
当数据源不是 DB ⽽是其他远程服务,可能⽆法及时主动感知数据变更,这种情况下⼀般会选择对缓存数据设置失效期,也就是数据不⼀致的最⼤容忍时间。
这种场景下,可以选择失效更新,key 不存在或失效时先请求数据源获取最新数据,然后再次缓存,并更新失效期。
但这样做有个问题,如果依赖的远程服务在更新时出现异常,则会导致数据不可⽤。改进的办法是异步更新,就是当失效时先不清除数据,继续使⽤旧的数据,然后由异步线程去执⾏更新任务。这样就避免了失效瞬间的空窗期。另外还有⼀种纯异步更新⽅式,定时对数据进⾏分批更新。实际使⽤时可以根据业务场景选择更新⽅式。

数据不⼀致
第⼆个问题是数据不⼀致的问题,可以说只要使⽤缓存,就要考虑如何⾯对这个问题。缓存不⼀致产⽣的原因⼀般是主动更新失败,例如更新 DB 后,更新 Redis 因为⽹络原因请求超时;或者是异步更新失败导致。
解决的办法是,如果服务对耗时不是特别敏感可以增加重试;如果服务对耗时敏感可以通过异步补偿任务来处理失败的更新,或者短期的数据不⼀致不会影响业务,那么只要下次更新时可以成功,能保证最终⼀致性就可以。

如何解决⼀致性问题?
延迟双删
⼀般来说,如果允许缓存可以稍微的跟数据库偶尔有不⼀致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持⼀致性的话,最好不要做这个⽅案,即:读请求和写请求串⾏化,串到⼀个内存队列⾥去。
串⾏化可以保证⼀定不会出现不⼀致的情况,但是它也会导致系统的吞吐量⼤幅度降低,⽤⽐正常情况下多⼏倍的机器去⽀撑线上的⼀个请求。把⼀些列的操作都放到队列⾥⾯,顺序肯定不会乱,但是并发⾼了,这队列很容易阻塞,反⽽会成为整个系统的弱点,瓶颈

缓存穿透(查不到)
缓存穿透。产⽣这个问题的原因可能是外部的恶意攻击,例如,对⽤户信息进⾏了缓存,但恶意攻击者使⽤不存在的⽤户id频繁请求接⼝,导致查询缓存不命中,然后穿透 DB 查询依然不命中。这时会有⼤量请求穿透缓存访问到 DB。
解决的办法如下。

  • 接⼝层增加校验,⽐如⽤户鉴权校验,参数做校验,不合法的参数直接代码Return,⽐如:id 做基础校验,id <=0的直接拦截等。
  1. 缓存空对象:对不存在的⽤户,在缓存中保存⼀个空对象进⾏标记,防⽌相同 ID 再次访问 DB。不过有时这个⽅法并不能很好解决问题,可能导致缓存中存储⼤量⽆⽤数据。
  2. 布隆过滤器:使⽤ BloomFilter 过滤器,BloomFilter 的特点是存在性检测,如果 BloomFilter 中不存在,那么数据⼀定不存在;如果 BloomFilter 中存在,实际数据也有可能会不存在。⾮常适合解决这类的问题。
    在这里插入图片描述
    在这里插入图片描述

缓存击穿(量太大,缓存过期!)
缓存击穿,就是某个热点数据失效时,⼤量针对这个数据的请求会穿透到数据源。
解决这个问题有如下办法。

  • 设置热点数据永不过期:从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
  1. 加互斥锁:可以使⽤互斥锁更新,保证同⼀个进程中针对同⼀个数据不会并发请求到 DB,减⼩ DB 压⼒。
  2. 使⽤随机退避⽅式,失效时随机 sleep ⼀个很短的时间,再次查询,如果失败再执⾏更新。
  3. 针对多个热点 key 同时失效的问题,可以在缓存时使⽤固定时间加上⼀个⼩的随机数,避免⼤量热点 key 同⼀时刻失效。

缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机,产⽣的原因是缓存挂掉,这时所有的请求都会穿透到 DB。
解决⽅法:
6. 限流降级:使⽤快速失败的熔断策略,减少 DB 瞬间压⼒;
7. ⾼可⽤:使⽤主从模式和集群模式来尽量保证缓存服务的⾼可⽤。
8. 数据预热:数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
9. 缓存失效时间分散开。

缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;

缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。

缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

热点数据和冷数据是什么
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
对于上面两个例子,寿星列表、导航信息都存在一个特点,就是信息修改频率不高,读取通常非常高的场景。
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。

hot key出现造成集群访问量倾斜解决办法
(1):使⽤本地缓存
(2): 利⽤分⽚算法的特性,对key进⾏打散处理(给hot key加上前缀或者后缀,把⼀个hotkey 的数量变成 redis 实例个数N的倍数M,从⽽由访问⼀个 redis key 变成访问 N * M 个redis key)

如果有⼤量的key需要设置同⼀时间过期,⼀般需要注意什么?
如果⼤量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们⼀般需要在时间上加⼀个随机值,使得过期时间分散⼀些。

最经典的KV、DB读写模式么?
最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放⼊缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

为什么是删除缓存,⽽不是更新缓存?
原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
⽐如可能更新了某个表的⼀个字段,然后其对应的缓存,是需要查询另外两个表的数据并进⾏运算,才能计算出缓存最新的值的。
另外更新缓存的代价有时候是很⾼的。是不是说,每次修改数据库的时候,都⼀定要将其对应的缓存更新⼀份?也许有的场景是这样,但是对于⽐较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改⼀个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
举个栗⼦:⼀个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有⼤量的冷数据。
实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算⼀次⽽已,开销⼤幅度降低。⽤到缓存才去算缓存
其实删除缓存,⽽不是更新缓存,就是⼀个 Lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会⽤到,⽽是让它到需要被使⽤的时候再重新计算。
像 Mybatis,Hibernate,都有懒加载思想。查询⼀个部门,部门带了⼀个员⼯的 List,没有必要说每次查询部门,都⾥⾯的 1000 个员⼯的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问⾥⾯的员⼯,那么这个时候只有在你要访问⾥⾯的员⼯的时候,才会去数据库⾥⾯查询 1000 个员⼯。

与memcache的区别
redis相比memcache来说,拥有更多的数据结构,支持更丰富的数据操作。
支持集群模式,memcache没有原生的集群模式,依靠客户端集群分片
redis单核,memcache多核,在存储小数据的时候,redis性能更高

猜你喜欢

转载自blog.csdn.net/qq_44961149/article/details/108599670