我们都知道系统中使用缓存,其实就两个主要的用途:高并发和高性能。
那么用了缓存后会有哪些弊端呢?
1. redis为什么是单线程模型?效率为什么高?
Redis 内部使用文件事件处理器file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。
文件事件处理器的结构包含4 个部分:
1、多个 socket
2、IO 多路复用程序
3、文件事件分派器
4、事件处理器
效率高:
1、纯内存操作
2、核心是基于非阻塞的 IO 多路复用机制
3、单线程反而避免了多线程的频繁上下文切换问题
2. 有哪些数据类型?
Redis 主要有以下几种数据类型:
Strings:字符串,最简单的数据类型,用于存储键值对。
Hashes:哈希表类似于map,用于存储键值对
Lists:有序列表,可以存储排行榜、文章评论列表之类的东西
Sets:无序集合,不能存储重复元素
Sorted Sets:排序的set,去重且排序
3. redis的过期策略有哪些?淘汰机制呢?LRU了解吗?
过期策略就是:定期删除+惰性删除
1、定期删除就是redis默认每隔100ms随机抽取一些过期时间的key,删除过期的key(redis不是查找所有的key进行检查)
2、 惰性删除就是当访问一个key时,如果发现key过期了,就删除这个key
这样总会有漏网之鱼,怎么办?内存淘汰机制!
淘汰机制有以下几个:
1、allkeys-lru:全局最近最少使用
2、allkeys-ttl:全局过期时间最短的
3、allkeys-random:全局随机选择
4、volatile-ttl:过期时间最短的
5、volatile-random:随机选择
6、volatile-lru:最近最少使用
手写LRU算法:
第一种:使用哈希表+双向链表实现LRU缓存(复杂)
第二种:JDk底层自己实现的LRU缓存(简单)
实例:
/** * 继承 LinkedHashMap 快速实现 LRU Cache * @param <K> key * @param <V> value */publicclassLRUCacheLinkedHashMap<K,V>extendsLinkedHashMap<K,V>{// 缓存最大容量privatefinalintmaxCapacity;/** * 构造器开启访问顺序排序 * @param capacity 缓存容量 */publicLRUCacheLinkedHashMap(intcapacity){// initialCapacity: 初始容量,loadFactor: 负载因子,accessOrder=true 开启LRU访问排序super(capacity,0.75f,true);this.maxCapacity=capacity;}/** * 钩子方法:put 执行完毕后回调 * 返回 true → 删除最久未使用的元素(链表头部) */@OverrideprotectedbooleanremoveEldestEntry(Map.Entry<K,V>eldest){// 当前元素个数超过最大容量则淘汰returnsize()>maxCapacity;}// 测试入口publicstaticvoidmain(String[]args){// 缓存容量为3LRUCacheLinkedHashMap<Integer,String>lru=newLRUCacheLinkedHashMap<>(3);lru.put(1,"一号数据");lru.put(2,"二号数据");lru.put(3,"三号数据");System.out.println("存入1,2,3:"+lru);// 新增第4个,超出容量,淘汰最久未使用的1lru.put(4,"四号数据");System.out.println("存入4,淘汰1:"+lru);// 访问key=2,2变为最新元素,移到末尾lru.get(2);System.out.println("访问2之后:"+lru);// 新增5,此时最久未使用是3,淘汰3lru.put(5,"五号数据");System.out.println("存入5,淘汰3:"+lru);// 更新key=4的值,4变为最新lru.put(4,"四号更新");System.out.println("更新4之后:"+lru);}}4. 什么是 Redis 的雪崩、穿透和击穿问题?
**雪崩:**大量key同时过期失效或者redis服务挂了,大量请求直接打到数据库,导致数据库压力很大,甚至崩溃
解决:
事前:主从复制➕哨兵模式
事中:本地缓存➕服务层限流、降级
事后:持久化数据,避免数据丢失
**穿透:**查询数据库中根本不存在的数据,绕过缓存,直接查询数据库,比如传-1或者空值
解决:
1、缓存空值,数据库查询为空时,让redis写入key:null,设置过期时间为5分钟
2、使用布隆过滤器
**击穿:**缓存中有超高并发热点key刚刚好过期
解决:
1、使用互斥锁,确保只有一个线程查询数据库
2、使用缓存预热,提前查询数据库,将数据缓存起来,避免直接查询数据库
3、设置永不过期
5. Redis 的持久化有哪几种方式?
**RDB:**快照持久化,按照时间点快照,把当前内存中全部数据一次性保存到磁盘二进制文件中。
1、自动触发:
#900秒至少1次修改、300秒至少10次、60秒至少10000次修改,自动bgsave save9001save30010save60100002、手动触发:fork子进程,子进程写RDB,父进程继续处理请求。
**AOF:**记录每一条修改命令(set/hset/del等),追加写入.aof 文本文件,重启时重放命令恢复数据。
默认关闭,开启配置:
appendonly yes appendfilename"append.aof"AOF 重写机制:
AOF 持续追加会文件越来越大,重写:
fork 子进程,读取当前内存数据,生成精简命令替换旧 AOF,删除冗余操作。
触发条件配置:
文件比上次重写后增大100%且超过64MB自动重写。 auto-aof-rewrite-percentage100auto-aof-rewrite-min-size64mb6. 如何保证redis的高可用和高并发呢?
高并发:
1、主从复制
一主多从,主负责写,并且将数据复制到其它的 slave 节点,所有的读请求全部走从节点。
原理及流程:全量复制➕增量复制
(1)从节点发起连接,握手认证,发送replconf配置
(2)发送PSYNC命令(核心):PSYNC
runid:主节点唯一运行ID,每次重启变更
offset:复制偏移量,记录从节点上次复制到的偏移量,用于断线重连
2种情况:
1、首次链接/runid不匹配:主节点执行全量复制
2、断线重连/runid一致、offset在主节点复制积压缓冲区:增量复制
(3)主节点执行全量复制:生成一份RDB持久化文件,同时写入所有命令,存入复制积压缓冲区,生成完毕,通过socket发送给从节点
(4)从节点接收RDB文件,加载数据,同时从主节点订阅写命令,持续增量同步
无磁盘复制:
主节点在内存中直接创建RDB ,然后发送给从节点,不会在自己本地落地磁盘了。只需要在配置文件中开启 repl-diskless-sync yes 即可。
过期key处理:
slave 不会过期key,只会等待 master 过期key。如果 master 过期了一个key,或者通过 LRU 淘汰了一个key,那么会模拟一条 del 命令发送给 slave。
高可用:
1、使用哨兵模式
哨兵:sentinel是 Redis 集群架构中非常重要的一个组件,主要有以下功能:
集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
此模式导致数据丢失的情况:
**1、异步复制导致:**主从复制是异步的,有部分数据还没复制到从节点,主节点就宕机了
**2、脑裂导致:**主节点突然脱离了正常的网络,和其他从节点不连接,但是自己还运行着,此时哨兵认为你已经挂了,会选举新的主节点,集群里就会有两个主节点,这就是脑裂。
此时某个从节点被选择为了主节点,但是client还没来及切换,还在往旧的主节点写数据。因此就主节点再次恢复的时候,会被认为是新的从节点从主节点同步数据,但是新的主节点
并没有client写入的数据,所以就丢失了。
解决方法:
至少有1个 slave,数据复制和同步的延迟不能超过10秒 min-slaves-to-write1min-slaves-max-lag10如何保证缓存与数据库的双写一致性?
场景1:读多写少、允许短暂不一致(90% 普通业务)
标准实现:延迟双删 + 过期兜底
更新数据库;
删除缓存;
异步延迟 N ms 再次删除缓存;
缓存统一设置过期时间,兜底最终一致。
// 1. 更新数据库updateDb(entity);// 2. 立即删缓存redis.del(key);// 3. 异步延迟删除threadPool.execute(()->{Thread.sleep(500);redis.del(key);});场景2:强一致性要求(金融、订单、库存,不允许脏数据)
分布式锁(串行化读写)
写流程:加锁 → 更新 DB → 删除缓存 → 释放锁
读流程:加锁 → 查询缓存,无则查 DB 写入缓存 → 释放锁
11. 写在最后
喜欢的点赞、收藏、转发呀!