缓存雪崩、缓存击穿、缓存穿透
-
缓存雪崩,由于⼤量的key在同⼀时间失效,导致流量直接打到数据库,最终导致数据库宕机
- 解决方案
- 可以将key的过期时间设置随机值,避免同⼀时间过期
- 缓存中间件宕机,可以通过对缓存中间件做高可用集群来避免。
- 并发量不多的时候可以采⽤加锁排队
- 给每⼀个缓存数据加⼀个缓存标记来记录缓存是否失效,如果失效就更新
- 设置热点数据永远不过期。
- 解决方案
-
缓存击穿,⼤量⽤户访问某个key时,这个key刚好失效,导致流量直接打到数据库,最终导致数据库宕机
-
解决方案
-
设置热点数据永不过期
-
加互斥锁 (分布式锁)
-
业界比较常用的做法。简单地来说,就是在缓存失效的时候(判断拿出来的值是否为空),不是立即去加载数据库,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX)去set一个mutex key,当操作返回成功时,再进行加载数据库的操作并回设缓存;否则,就重试整个get缓存的方法。
-
public String get(key) { String value = redis.get(key); if (value == null) { //代表缓存值过期 //设置3分钟的超时,防止删除操作失败的时候,下次缓存过期不能一直加载数据库 if (redis.setnx(key_mutex,1, 3 * 60) == 1) { //代表设置成功 value = db.get(key); // 去加载数据库 redis.set(key, value, expire_secs); // 将数据库的数据放在redis中,并设置 过期时间 redis.del(key_mutex); } else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓 存值即可 sleep(50); get(key); //重试 } } else { return value; } }
-
-
-
-
缓存穿透,⽤户频繁使⽤缓存和数据库中不存在的数据进⾏访问,导致流量直接打到数据库,最终导致数据库宕机
- 解决方案
- 接⼝层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 如果缓存中不存在该值,就缓存空值到缓存中
- 使⽤布隆过滤器,布隆过滤器是⼀个位图,如果它说不存在就⼀定不存在,如果说存在只能是
可能存在,可以将可能存在的key放⼊bitmap进⾏过滤
- 解决方案
常⻅数据类型
- string 类型,可以用来存储字符串或JSON字符串
- 应用场景: 分布式session、分布式锁、计数
- incr: 线程安全、计数类、软限流、库存、分布式ID
- 常用指令有 set、get、setnx、setex等
- 底层数据结构
- 采用动态字符串sds实现
- 应用场景: 分布式session、分布式锁、计数
- hash类型 基于键值对存储的集合,比string类型更好管理以及节省内存
- 应用场景: 购物车
- 常用指令: hset 、hget 、hgetall 、hdel 、hincrby等
- 注意:不能对field设置过期时间,在集群模式下,不建议大规模使用
- 底层数据结构
- 采用字典dict实现,当数据量比较小时,采用压缩列表ziplist存储,数据大小和元素阈值可以参数设置
- list类型,基于链表实现的有序可重复集合
- 应用场景:微信公众号消息流,栈,队列,阻塞队列,发布订阅
- 常⽤的指令lpush、lpop、rpush、rpop、blpop、brpop、lrange
- 其中栈的实现可以通过lpush+lpop实现、队列可以通过lpush+rpop实现、阻塞队列可以通过
lpush+brpop实现 - 底层数据结构
- 采用quicklist快速列表和压缩列表ziplist实现
- set类型,无序不可重复集合
- 应用场景: 抽奖、点赞收藏关注、共同关注、可能认识的人
- 常⽤的指令有sadd、srem、smembers、scard、srandmember、sismember、spop、sinter、
sunion、sdiff、sinterstore、sdiffstore - 抽奖可以使⽤srandmember实现、共同关注可以使⽤sinter实现、可能认识的⼈可以使⽤sdiff实现
- set类型不适合存放⼤量数据建议5k以下,如果超过5k会导致性能下降,如果遇到这种情况可以进⾏
将key拆分为多个key进⾏存储数据 - 底层数据结构
- 采⽤ ⼀个 value 为 null 的字典 dict 实现
- 注意 set一定是无序的 ? 不一定 整型可能是有序的 这个跟底层数据结构有关 intset hashtable
- zset,有序不可重复集合
- 应用场景:排行榜
- 常⽤指令zadd、zrem、zscore、zincrby、zrange、zreverange、zrangebyscore、zreverangescore、
zunionscore、zinterscore - 排⾏榜可以通过zrange/zreverange/zunionscore实现
- 如果需要进⾏数据分⻚可以通过zrange或zreverange进⾏实现
- 如果需要实现让某元素过期删除可以将score存放具体过期时间,然后通过定时任务去获取最近到期
的元素进⾏删除 - 底层数据结构( 重点加分 )
- 采⽤ 字典 dict 和跳表 skiplist 实现,当 数据量⽐较⼩是,采⽤压缩列表 ziplist存储
- bitmap,可以看作⼀个数组只存0和1,数组的下标对应的是偏移量
- 应⽤场景有⽉打卡、⽉活跃数
- ⽉打卡可以通过将 当前第⼏天作为偏移量 ,如果打卡对应的位置为1,反之为0
压缩列表 本身就是⼀个数组,只是增加了列表的⻓度、尾部偏移量、列表元素个数以及列表
结尾的标志,这样就有利于快速的寻找列表的头、尾节点,但对于寻找⾮头尾元素效率不是很
⾼效,只能⼀个个遍历跳表 就是在链表的基础上增加了多级索引,通过多级索引位置的转跳,实现快速查找元素
无序 (存储数据的时候就已经无序了)
set key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
- NX:当数据库中key不存在时,可以将key-value添加到数据库
- XX:当数据库中key存在时,可以将key-value添加数据库,与NX参数互斥
- EX:key的超时秒数
- PX:key的超时毫秒数,与EX互斥
- value中若包含空格、特殊字符,需用双引号包裹
谈⼀下布隆过滤器
- 基于bitmap实现的
- 优点就是占⽤内存⼩
- 缺点就是存在误判、不能获取原元素、不能从布隆过滤器中删除元素
单线程为什么这么快
- Redis的单线程并不是真正意义上的单线程,只是对于⽹络IO和键值的读写以及指令执⾏是交给⼀个线程
去进⾏的,⽽持久化、异步删除、集群数据同步都会有额外的线程来分担 - Redis快的原因
- 基于内存进⾏读写操作,没有磁盘IO的性能开销
- 单线程有效地避免了多线程之间锁的竞争以及上下⽂切换的性能开销,正是因为单线程所以对哪些
⽐较耗时的指令如keys要少使⽤,避免阻塞整个线程影响性能 - 本身k.v结构,时间复杂度接近O(1)(hash冲突)
- 采⽤epoll机制,让单线程循环遍历描述符,⼀旦描述符操作就绪就会⽴刻通知线程去执⾏相应的读
写操作,减少了⽹络IO的开销 - 对每种数据类型进⾏了相应的优化
为什么要引⼊多线程
- Redis对于⼤部分场景使⽤单线程进⾏⽹络IO和键值读写执⾏已经⾜够了,但是对于⼀些特定的场景处理
的数据量还完全不够,所以引⼊多线程可以充分的发挥多核处理器的性能来帮助处理⼤量⽹络IO读写操
作,但是指令的执⾏还是交给⼀个线程去做的
什么是IO多路复⽤
- IO多路复⽤就是 通过⼀种机制实现监听多个描述符,⼀旦描述符就绪就会通知程序去执⾏相应的读写操作
- 常⻅的有select、poll、epoll
持久化机制
- 持久化机制就是将数据持久化到磁盘中防⽌服务宕机⽽导致数据丢失
- Redis提供的持久化⽅式有2种分别是RDB和AOF
- RDB持久化会根据将内存中的数据写⼊到指定路径的dump**.rdb⽂件**中,然后当Redis重启时会通过这个⽂件进⾏数据恢复
- AOF持久化会将每次执⾏的写命令写⼊到aof⽂件中,然后当Redis重启时会通过这个⽂件进⾏数据恢复
持久化⽅式如何选择
- 如果数据不敏感,可以不开启持久化
- 如果数据⽐较重要并且允许⼏分钟的数据丢失,可以使⽤RDB
- 如果作为内存数据,建议都开启RDB和AOF这两种⽅式,优先会从aof⽂件中进⾏数据恢复,因为aof⽂件
数据更完整
主从复制原理
。。。。
主从复制⻛暴
。。。
集群⽅案(less)
-
Sentinel哨兵模式
- 由于主从架构⽆法进⾏故障转移,⽆法实现⾼可⽤并且配置复杂所以引出了哨兵模式
-
Cluster集群模式
- 由于哨兵模式进⾏故障转移的时候会出现⽹络瞬断以及只有⼀个主节点⽆法实现⾼并发所以引出了集群模式
Cluster集群选举流程
…
主从选举的脑裂问题
…
Cluster集群模式下数据倾斜问题如何解决
…
CAP理论
- C是一致性,分布式系统的数据要保持一致
- A是可用性,分布式系统能进行故障转移
- P是分区容错性,分布式系统出现网络问题能正常运行
- CAP理论是指分布式系统中不能保证三者同时存在,只能两两组合
热点缓存并发重建
- 冷数据突然变为热数据,当处于高并发场景下,重建缓存不是短时间完成的,所以为了减少重建缓存的次数可以使用DCL机制
- 可以先查询一次,如果有缓存就返回,如果没有就加锁,在加锁后再查询一次,如果有就直接返回,如果没有就进行重建工作
- 多次查询为了保证当有线程已经完成了重建工作而其他线程无需多次进行缓存重建
- 对于缓存重建,可能因为突发性热点访问导致系统压力暴增,所以需要提升系统承受的并发量,可以使用“串行变并行”的思想来解决,让多个线程尝试获取锁一段时间,倘若缓存已经重建好,就能让多个线程同时拿到缓存返回。
数据库和缓存双写不⼀致
- 延时双删,先删除缓存,再写⼊数据库,延时500ms,再删除缓存
- 问题:为什么要延时500ms
- 为了我们在第二次删除缓存之前,能完成数据库的更新操作,保证数据库的值最新
- 问题: 为什么要2次删除缓存
- 第一次删除缓存是为了更新数据,保证数据库的值是最新,第二次是为了保障拿到缓存数据是最新的
- 如果不进行第二次删除缓存,可能查到的是未修改的缓存数据,进行第二次删除之后,会从数据库中重新查,保证了数据的一致性
- 问题:为什么要延时500ms
- 使⽤Redisson的读写锁,实现机制和ReentrantReadWriteLock⼀致
- 使⽤canal监听binlog及时去更新缓存
Redis分布式锁 (精简版)
-
Redis实现分布式锁的两种方式
-
Redis提供的 **
SET key value NX PX milliseconds
**指令,这个指令是设置一个key-value,如果key不存在,则返回1,否则返回0 -
基于Redission客户端来实现,Redission提供了分布式锁的封装方法,我们只需要调用api中的lock()和unlock()方法
- Redission所有指令都通过lua脚本执行并支持lua脚本原子性执行
- Redission中有一个watch dog的概念,它会在你获取锁之后,每隔10秒帮你把key的超时时间设置为30s,就算一直持有锁,也不会出现key过期了。“看门狗”的逻辑保证了没有死锁发生
- Redisson是基于lua脚本实现的,它的⼤致流程
- 当某个线程抢到了锁,倘若业务还没有执⾏完,会定时去进⾏锁续命,
- ⽽那些没有抢到锁的线程会订阅抢到锁线程的channel,然后它们会自旋⼀定时间去尝试获取锁,获取锁失败后会被安排到阻塞队列中阻塞,
- ⼀旦抢到锁的线程释放锁,它们就会被通知到然后持续唤醒出队继续去抢锁
-
Redis分布式锁实现
-
分布式锁主要是⽤来保证分布式系统数据同步操作的,可以通过让多个操作的串⾏执⾏来保证整体的原⼦性
-
⼀般情况下可以想到⽤setnx来实现分布式锁,复杂情况下,存在很多问题
-
Redis实现分布式锁的两种方式Redis提供的 **
SET key value NX PX milliseconds
**指令,这个指令是设置一个key-value,如果key不存在,则返回1,否则返回0NX:当数据库中key不存在时,可以将key-value添加到数据库
PX:key的超时毫秒数,与EX互斥
-
-
对于复杂的操作可以配合lua脚本来实现⼀个完善的分布式锁,Redisson框架帮助我解决了这些问题
-
Redisson是基于lua脚本实现的,它的⼤致流程
- 当某个线程抢到了锁,倘若业务还没有执⾏完,会定时去进⾏锁续命,
- ⽽那些没有抢到锁的线程会订阅抢到锁线程的channel,然后它们会自旋⼀定时间去尝试获取锁,获取锁失败后会被安排到阻塞队列中阻塞,
- ⼀旦抢到锁的线程释放锁,它们就会被通知到然后持续唤醒出队继续去抢锁
-
分布式锁该如何实现集群模式下的⾼可⽤,在集群模式下会出现⼀个问题就是如果master中的锁key在同
步给slave的过程中突然宕机,slave发现master宕机,然后正好⾃⼰被选举为新master,在新master同步
原master数据的时候,此时原master⼜突然恢复了,数据还没开始清空,就会出现多把锁的情况,对于这
个问题Redisson提供了redLock,是从CP机制⻆度去实现的,只有当过半以上的节点加锁成功,才算加锁
成功,已经违背了Redis的AP机制,如果⾮要考虑⼀致性问题,可以考虑使⽤ZK去实现分布式锁
Redis实现分布式锁的两种方式
Redis提供的 **
SET key value NX PX milliseconds
**指令,这个指令是设置一个key-value,如果key已经存在,则返回0,否则返回1基于Redission客户端来实现,Redission提供了分布式锁的封装方法,我们只需要调用api中的lock()和unlock()方法
- Redission所有指令都通过lua脚本执行并支持lua脚本原子性执行
- Redission中有一个watch dog的概念,它会在你获取锁之后,每隔10秒帮你把key的超时时间设置为30s,就算一直持有锁,也不会出现key过期了。“看门狗”的逻辑保证了没有死锁发生
过期键的删除策略 (过期策略)
- 惰性删除
- 只有当访问⼀个key的时候,才会判断的当前key是否已过期,已过期就会删除
- 这个策略可以节省CPU资源,但是占⽤内存,可能因为⼤量的key不被再次访问,导致⼀直不清楚从
占⽤内存
- 定期删除
- 每隔⼀段时间会扫描⼀定数量的过期key,并且清除已过期的key
- 这个策略属于折中⽅案,可以有效的平衡CPU资源以及内存资源
- 强制删除
- 当已使⽤内存超过Redis最⼤允许内存,会触发内存淘汰策略
- Redis中同时使⽤了惰性删除和定期删除
淘汰算法
- LRU
- 最近最少使用,根据时间区分
- LFU
- 最近不经常使用,根据使用频率
内存淘汰策略有哪些
- 默认策略noeviction, 当内存不足以容纳新写入数据时,新写入操作会报错
- 针对过期时间的key
- volatile-lru 按照LRU算法删除
- volatile-lfu 按照LFU算法删除
- volatile-radom 随机删除
- volatile-ttl 按过期时间顺序删除
- 针对所有key
- allkeys-random 随机删除
- allkeys-lru 按照LRU算法删除
- allkeys-lfu 按照LFU算法删除
Redission分布式锁的使用
-
配置
-
@Configuration public class MyRedissonConfig { /** * 所有对Redisson的使用都是通过RedissonClient * @return * @throws IOException */ @Bean(destroyMethod="shutdown") public RedissonClient redisson() throws IOException { //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); //2、根据Config创建出RedissonClient实例 //Redis url should start with redis:// or rediss:// RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
-
-
使用
-
RLock lock = redisson.getLock("anyLock"); // 最常见的使用方法 需要手动释放锁 lock.lock(); //阻塞式等待 未指定解锁时间 看门狗机制 默认30s 自动续期 // 加锁以后 10 秒钟自动解锁 // 无需调用 unlock 方法手动解锁 lock.lock(10, TimeUnit.SECONDS); // 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁 boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } }
-
/* * 7、整合redisson作为分布式锁等功能框架
* 1)、引入依赖
* <dependency>
* <groupId>org.redisson</groupId>
* <artifactId>redisson</artifactId>
* <version>3.12.0</version>
* </dependency>
* 2)、配置redisson
* MyRedissonConfig给容器中配置一个RedissonClient实例即可
* 3)、使用
* 参照文档做。
*/
缓存雪崩、缓存穿透、缓存击穿
答题思路:
- 三者的概念,发生场景、解决方案
- 三者的区别和影响
涉及知识点:Redis第七章:缓存问题—缓存穿透、缓存雪崩、缓存击穿
穿透:不存在的key
雪崩:大量的key失效
击穿:一个key或一些key 热点key
大Key,热点Key的处理
Hot Key
答题思路:
- hot key的概念,场景,问题
- hot key的发现
- hot key的处理
Big Key
答题思路:
- big key的概念、场景,影响
- String > 10k list大于5000个
- big key的发现
- big key的处理
数据库一致,缓存失效,数据并发竞争
数据库一致
答题思路:
- Catch Aside Pattern
- 数据源不一致
- 场景的适用性(互联网)
- 保证最终一致,一致的时间处理
缓存失效
答题思路:
- 缓存失效带来的问题:缓存穿透、缓存雪崩、缓存击穿(高并发)
- 会让数据库压力过大而宕机
- redis的缓存过期策略: LRU
- Redis设置的expiretime TTL
缓存失效的处理:
- Redis做DB时,不能失效保证数据的完整性,数据一致问题,定时任务,在DB变化后,更新缓存
- 可以失效但不穿DB,失效后读取本地缓存或服务熔断
- 异步更新DB,数据时时同步
数据并发竞争
答题思路:
- 数据并发竞争的概念、场景
- 数据并发竞争的影响
- 解决方案:
- 将并发串行化:分布式锁+时间戳、利用队列
- 使用CAS:秒杀
热点数据和冷数据是什么
答题思路:
- 热数据:hot key 位于Redis中命中率尽量高
- 冷数据:不经常访问的数据位于DB中
- 冷热的交换:maxmemory+allkeys LRU
- 交换比例:热20万、冷200万
- Redis作为DB时,冷数据不能驱逐,保证数据的完整性
单线程的redis为什么这么快
答题思路:
- redis在内存中操作,持久化只是数据的备份,正常情况下内存和硬盘不会频繁swap
- 多机主从,集群数据扩展
- maxmemory的设置+淘汰策略
- 数据结构简单,有压缩处理,是专门设计的
- 单线程没有锁,没有多线程的切换和调度,不会死锁,没有性能消耗
- 使用I/O多路复用模型,非阻塞IO;
- 构建了多种通信模式,进一步提升性能
- 进行持久化的时候会以子进程的方式执行,主进程不阻塞
redis的过期策略以及内存淘汰机制
答题思路:
- 为什么要过期
- 什么情况下不能过期
- 如何设置过期
- expires 原理
- 如何选择缓存淘汰策略
Redis 为什么是单线程的,优点
答题思路:
- Redis采用单线程多进程集群方案
- Redis是基于内存的操作,CPU不是Redis的瓶颈
- 瓶颈最有可能是机器内存的大小或者网络带宽
- 单线程的设计是最简单的
- 但是对多核CPU利用率不够,所以Redis6采用多线程。
单线程优点:
- 代码更清晰,处理逻辑更简单不用去考虑各种锁的问题,
- 不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- 不存在多进程或者多线程导致的切换而消耗CPU
如何解决redis的并发竞争key问题
Rediskey的设计,尽量不竞争
必须竞争:秒杀、分布式锁
同数据并发竞争
Redis分布式锁问题
答题思路:
- 分布式锁的概念,应用场景
- Redis的实现方式
- 分布式锁的本质分析
- redis、zookeeper、etcd三者的对比和应用场景
- redisson的使用
有没有尝试进行多机redis 的部署?如何保证数据一致的?
答题思路:
- redis多机部署方案:Redis主从+哨兵、codis集群、RedisCluster
- 多机:高可用、高扩展、高性能
- 三者的区别,适用场景
- 数据一致性指的是主从的数据一致性
- Redis是AP模型,主从同步有时延。所以不能保证主从数据的时时一致性,只能保证数据最终一致性
保证数据一致性方案:
- 1、忽略如果业务能够允许短时间不同步就忽略,比如:搜索,消息,帖子,职位
- 2、强制读主库,从库只做备份使用
- 使用一个高可用主库提供数据库服务
- 读和写都落到主库上
- 采用缓存来提升系统读性能
- 3、选择性读主
- 写主库时将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里
- 读时先在cache中查找:
- cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询 cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询