一、缓存穿透
1.含义
缓存穿透是指用户获取数据的时候,发现redis数据库中没有数据,进而从持久层数据库中进行查询。若未查询出来数据,则查询失败。低并发的条件下,不会出现问题;若在高并发的条件,当大部分用户访问时,若缓存没有命中,则大量的请求都落在持久层数据库,这时数据库将承受着很大的压力,甚至导致崩溃。简而言之,缓存穿透就是访问绕过了缓存,进而直接访问持久层数据库,最终导致持久层数据库崩溃。(缓存穿透是指缓存和持久层数据库中都不存在数据)
2. 图解
3. 解决方案
(1) 缓存空对象
此方案针对是只访问某些固定的值有效,当访问请求的时候,发现缓存中并没有命中,则去持久层数据库中获取,若发现持久层数据库中并没有数据,则将数据缓存成空对象,放至缓存中,当下一次同样的请求过来后,能够直接从缓存中获取,不再访问持久层数据库,解决该问题。
(2)布隆过滤器
当访问的时候,用户访问的请求不是落在某些固定的值的时候,采用上述的缓存空对象方法并不适用,因为访问不同的key,将会都达到持久层数据库中进行获取,上述的方法反而会带来消耗大量内存等问题。此时可以采用布隆过滤器进行解决。
布隆过滤器是采用了是判断key是否存在于集合的方式,来判断持久层数据库中是否存在该值的方式进行。其采用了二进制向量的方式,对key值进行hash运算,而后取余,得到的数据通过二进制向量的下标,获取该值,若值为1,则代表数据可能在持久层数据库中存在,而值为0的时候,代表数据一定不存在。其采用以错误率换取时间的方式,来进行数据的过滤。其数据结构的图解如下:
由于布隆算法采用的hash进行计算,故降低错误率的方法就是减少hash碰撞,布隆过滤器减少hash碰撞的两种方法有:1. 使二进制数组尽可能的大 2.多次hash,将多次获取的结果取与。
算法代码如下:
public class BloomFilter { // 二进制数组的长度 private static final int BIT_SIZE = 2 << 24; // 计算出不同的hash, hash次数为5次 private static final int[] PROTONS = new int[]{3, 7, 15, 31, 63}; // 二进制数组 private BitSet bit = new BitSet(BIT_SIZE); // hash值 private Hash[] hash = new Hash[PROTONS.length]; // 构造函数 public BloomFilter() { for (int i = 0; i < PROTONS.length; i++) { hash[i] = new Hash(BIT_SIZE, PROTONS[i]); } } // 添加key public void add(String key) { for (Hash hash1 : hash) { bit.set(hash1.hash(key), Boolean.TRUE); } } // 判断key是否存在 public boolean existKey (String key) { boolean ret = Boolean.TRUE; for (Hash hash1 : hash) { ret = ret & bit.get(hash1.hash(key)); } return ret; } // 计算hash内部类 private class Hash { // 长度 private int cap; // 质子 private int proton; public Hash(int cap, int proton) { this.cap = cap; this.proton = proton; } // 计算hash并取余 public int hash(String key) { int result = 0; if (key == null) { return (cap - 1) & result; } for (int i=0; i< key.length(); i++) { result = this.proton * result + key.charAt(i); } return (cap - 1) & result; } } }
二、缓存击穿
1. 含义
缓存击穿是指热点数据在大量请求同时访问的时候,缓存中的数据过期,大量的请求直接去持久层数据库中获取数据,导致持久层数据库压力过大甚至崩溃。(缓存击穿是指数据在缓存中不存在,在持久层数据库中存在的情况)
2. 图解
3. 解决方案
(1) 设置热点数据永不过期。
(2) 利用互斥锁访问持久层数据库。代码如下:
public class Mutex { private ReentrantLock reentrantLock = new ReentrantLock(); public Object getData(String key) throws InterruptedException { Object value = getDataFromRedis(key); if (value == null) { // 获取锁 if (reentrantLock.tryLock()) { // 从持久层数据库中获取数据 value = getDataFromDb(key); if (value != null) { // 添加数据至redis中 setDataToRedis(key, value); } // 释放锁 reentrantLock.unlock(); } else { // 休眠100ms,然后重新访问 Thread.sleep(100); value = getData(key); } } return value; } private Object getDataFromRedis(String key) { // 从redis中获取数据(此处不实现) return null; } private Object getDataFromDb(String key) { // 从持久层数据库中获取数据(此处不实现) return null; } private void setDataToRedis(String key, Object value) { // 添加数据至redis中,此处不实现 } }
三、缓存雪崩
1. 含义
缓存雪崩是指缓存出现宕机或者数据在一瞬间全部失效,大量的并发请求全部从持久层数据库上获取数据,导致持久层数据库压力过大设置崩溃。(缓存雪崩是指数据在缓存中不存在,在持久层数据库中存在的情况)
2. 图解
3. 解决方案
(1)缓存高可用
搭建缓存集群,当一台机器宕机,其他的机器还可以使用。
(2)限流降级
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。
(3)数据预热
在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
四、总结
本博客借鉴了https://blog.csdn.net/cdm0881/article/details/105197011/,这是本人的第一个博客,望一起学习,一起进步。