缓存雪崩

概念

缓存雪崩指的是同一时间**内缓存数据大面积过期失效**,导致后续请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
image.png

解决方案

1、缓存数据的过期时间设置随机值,降低同一时间大量数据过期的概率

2、如果并发量不是特别多,可以考虑加锁排队

  1. public object getMyBook(){
  2. int cacheTime = 30;
  3. String cacheKey = "book_list";
  4. String lockKey = cacheKey;
  5. Object cacheValue = cacheHelper.get(cacheKey);
  6. // 如果缓存中的值尚未失效
  7. if (cacheValue != null)
  8. {
  9. return cacheValue;
  10. }
  11. else
  12. {
  13. // 锁定
  14. synchronized (lockKey)
  15. {
  16. // 双重保障
  17. cacheValue = cacheHelper.get(cacheKey);
  18. if (cacheValue != null)
  19. {
  20. return cacheValue;
  21. }
  22. else
  23. {
  24. cacheValue = getBookListFromDB(); //这里一般是 sql查询数据。
  25. cacheHelper.add(cacheKey, cacheValue, cacheTime);
  26. }
  27. }
  28. return cacheValue;
  29. }
  30. }

加锁排队的方式仅仅为了减轻数据库压力(不会同时有大量请求直接到达数据库),但是没能提高系统吞吐量

高并发场景下,大多数请求被阻塞,用户访问依旧超时,且可能引发分布式环境的分布式锁问题,用户体验不佳,一般不采用

3、添加缓存过期标识,如果缓存过期会触发线程后台查询数据库更新key缓存。另外,设置数据缓存过期时间=缓存标识过期时间2,这样,*当缓存标识过期,实际缓存还能返回旧数据,等待后台线程更新缓存完成后,才返回新缓存。

  1. public object getMyBook(){
  2. int cacheTime = 30;
  3. String cacheKey = "book_list";
  4. //缓存标识,存放于redis中,标识某个key是否过期
  5. String cacheSign = cacheKey + "_sign";
  6. // 获取缓存标识
  7. Object sign = cacheHelper.get(cacheSign);
  8. //获取缓存值
  9. var cacheValue = cacheHelper.get(cacheKey);
  10. // 当缓存标识不为空,则表示目标key未过期
  11. if (sign != null)
  12. {
  13. return cacheValue; //未过期,直接返回。
  14. }
  15. else
  16. {
  17. // 先将当前key的缓存标识置为非空,即尚未过期,有效时间cacheTime
  18. cacheHelper.add(cacheSign, "1", cacheTime);
  19. // 后台线程访问数据库并更新缓存
  20. ThreadPool.QueueUserWorkItem((arg) =>
  21. {
  22. cacheValue = getBookListFromDB(); //这里一般是 sql查询数据。
  23. cacheHelper.add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。
  24. });
  25. // 返回缓存值【如果后台线程没有执行完,这里返回的是旧值】
  26. return cacheValue;
  27. }
  28. }

这样去处理,可以一定程度上提高系统吞吐量

缓存击穿

概念

缓存击穿指的是缓存中某个数据没有而数据库中有【一般是缓存过期了】,瞬时所有关于这个数据的请求都落到了数据库上,造成数据库压力过大。
image.png

解决方案

1、设置热点数据永不过期

热点数据指的是某一时间会有大并发访问的数据,如电商爆款产品。这些数据设置缓存永不过期

缺点是占用内存,但是安全一些,逻辑上可以做个定时任务定时更新缓存,避免数据库压力过大

2、加互斥锁【**mutex key**】

该方法是比较普遍的做法,即在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试

单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)

  1. public String get(key) {
  2. String value = redis.get(key);
  3. if (value == null) { //代表缓存值过期
  4. // 调用redis的setnx命令设置一个固定的key_mutex,相当于为key上了锁
  5. // 这个方法只有在redis中不存在对应key的时候才会返回true
  6. // 否则返回false,从而保证同一时间仅有一个线程在在执行if里面的内容
  7. // 设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
  8. if (cacheHelper.setNx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
  9. value = getBookListFromDB();
  10. cacheHelper.add(key, value, expire_secs);
  11. cacheHelper.del(key_mutex); // 删除固定key_mutex,相当于解除对于key的锁
  12. } else { // 当目标key已经有线程在占用从数据库获取数据
  13. // 当前请求休眠50秒
  14. sleep(50);
  15. // 重试
  16. get(key);
  17. }
  18. } else {
  19. return value;
  20. }
  21. }

这种方式优点是思路简单,保证数据一致性,缺点是代码复杂,可能导致死锁

缓存穿透

概念

缓存穿透指的是访问**缓存和数据库中都没有的数据**,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

image.png

解决方案

1、如果数据库中不存在则短时间缓存一个空值,下次请求就会走缓存

  1. public object getMyBook() {
  2. int cacheTime = 30;
  3. String cacheKey = "book_list";
  4. Object cacheValue = cacheHelper.get(cacheKey);
  5. if (cacheValue != null)
  6. {
  7. return cacheValue;
  8. }
  9. else
  10. {
  11. cacheValue = getBookListFromDB(); //数据库查询不到,为空。
  12. if (cacheValue == null)
  13. {
  14. cacheValue = String.Empty; //如果发现为空,设置个默认值,也缓存起来。
  15. }
  16. cacheHelper.add(cacheKey, cacheValue, cacheTime);
  17. return cacheValue;
  18. }
  19. }

2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
**
我们也可以自己设计布隆过滤器,将必定不存在的数据存入单独的缓存,在请求到达的时候首先校验要查询的key,然后再放行给后面的正常缓存处理逻辑。

总结区别与解决方案

问题 维度 原因 解决方案
缓存雪崩 大面积数据 同一时间内缓存数据大面积过期失效,导致后续请求都落到数据库上 1、过期时间设置随机值
2、加锁排队
3、添加缓存过期标识
缓存击穿 单个数据 缓存中某个数据没有而数据库中有,瞬时所有关于这个数据的请求都落到了数据库上 1、热点数据永不过期
2、**互斥锁**
缓存穿透 单个数据 访问缓存和数据库中都没有的数据,导致所有的请求都落到数据库上 1、短时间缓存空值
2、布隆过滤器

相关概念

缓存预热

缓存预热就是系统上线后,事先将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去数据库加载相关的数据。

实现方案:
1、直接写个缓存刷新页面,上线服务时手动调用即可【建议】

2、项目启动自动加载【数据量不大的时候可以】

3、定时任务刷新缓存

热点数据和冷数据

热点数据:指**在redis数据库中修改频率不高,但读取频率很高的数据

冷数据:指的是大部分还没有被再次访问就已经无用的数据,不仅占用内存,且价值不大

一般数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。

比如:商城爆款推荐数据、论坛点赞数、评论数、分享数就属于热点数据,每个用户打开都需要去访问,且频率极高

而其他比如特别偏门的数据比如业务数据【积分细则等】就属于冷数据,不会怎么访问

热点key

缓存中的某些Key(可能对应用某个促销商品)对应的value存储在集群中一台机器,使得所有流量涌向同一机器,成为系统的瓶颈,该问题的挑战在于它无法通过增加机器容量来解决。就拿Redis-cluster集群方案来说,它可能会导致整个集群流量不均衡,个别节点访问量极大,甚至超出承受能力。

产生原因:

1、突发事件导致某个数据被大量访问,如双十一活动抢购

2、Redis分区过于集中,导致热点数据集中在某个Server上,人为加重负载压力

危害:

1、流量过于集中,超出主机网卡上限,可能导致Redis服务器宕机

2、可能导致缓存雪崩和缓存穿透问题【热点key失效导致请求同步】

解决方案:

1、拆分数据结构:将复杂的数据结构比如哈希类型,当哈希集合元素个数较多的时候,可以考虑将当前结构进行拆分,这样热点数据可以拆分成若干个不同的key分不到不同的Redis节点,减轻压力

2、多点备份热key:把热点key在多个redis上都存一份,有热key请求进来的时候,我们就在有备份的redis上随机选取一台

3、将热点key存放在本地缓存,针对这种热点key从本地JVM取出比Redis更快