Redis缓存穿透、缓存击穿、缓存雪崩及解决方案

一、为什么要使用缓存?

  1. 提升应用从程序性能
  2. 降低数据库成本
  3. 减少后端负载
  4. 可预测的性能
  5. 清除数据库热点
  6. 提高读取吞吐量(IOPS)

二、缓存的带来的问题

常见的缓存问题有:缓存与数据库数据不一致;缓存穿透、缓存击穿、缓存雪崩;缓存并发竞争等;

三、缓存的设计

insert,新增数据至数据库;update,删除缓存中的对应的数据,修改数据库的数据;delete,删除缓存中对应数据,删除数据库的数据;query,查询缓存中的数据,存在数据即返回查询的数据,缓存中不存在数据则查询数据库,数据库存在数据则返回数据库中数据并同步更新到缓存,缓存和数据库都不存在数据返回null。

四、缓存穿透

缓存穿透指用户请求数据,系统缓存中不存在该数据,直接请求了数据库,并且数据库也没有该数据。当大量该类请求发送是,系统在缓存中都没有获取到数据,直接请求了数据库,这会给数据库造成很大压力,甚至直接宕机。

解决方案:

  1. 缓存空对象
    当请求从缓存和数据都获取不到数据时,将返回的空对象缓存起来,同时设置一个过期时间。但是这种方法会存在两个问题:如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
  2. 接口层增加校验(布隆过滤器Bloom Filter)
    2.1 Bloom Filter概念
    Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。

    2.2 Bloom Filter原理(不存在漏报,存在误报)
    布隆过滤器底层通过bit数组进行存储(bit数组空间占用少、能支持快速的查找),并且通过hash函数(hash函数能保证任意输入固定输出,并且多次计算结果一致)来计算元素在数组中的下标位置。由于不同元素值可能存在相同的hash值(Hash碰撞),所以布隆过滤器存在一定的误判率。
    布隆公式:
    n:存储元素的个数
    p:希望达到的误判率
    m:bit数组的长度m
    k:hash函数的个数

    2.3 Java 手写 Bloom Filter
    Java中没有bit类型的数据类型,但是可以通过基础数据类型来转换为bit数组,如int[10]相当于bit[32 * 10],long[10]相当于bit[64 * 10]。
    byte:1byte = 8bit     1个字节是8个bit
    short:2byte
    int:4byte
    long:8byte
    float:4byte
    double:8byte
    boolean:1byte
    char:2byte
    代码如下:
import java.util.BitSet;

/**
 * 缓存穿透解决方案:bloomFilter
 */
public class MyBloomFilter {
    // 需要存储的元素个数
    private int n;
    // 希望达到的误判率
    private double p;
    // bit数组的长度
    private int m;
    // hash函数的个数
    private int k;
    // 存储元素的数组
    BitSet bitSet;

    public MyBloomFilter(int n, double p) {
        this.n = n;
        this.p = p;
    }

    /**
     * 添加元素
     * @param ele
     */
    public void addElement(String ele) {
        if (this.bitSet == null) {
            // 初始化bitSet及相关参数
            init();
        }
        // 获取元素ele 在k个哈希函数中计算的下标值(数组)
        int[] indexArray = getIndexs(ele);

        // 将BitSet中下标设置为true
        for (int index : indexArray) {
            this.bitSet.set(index);
        }
    }

    /**
     * 判断ele 元素是否存在集合中
     * @param ele
     * @return
     */
    public boolean isExist(String ele) {
        if (this.bitSet == null || this.bitSet.isEmpty()) {
            return false;
        }
        boolean flag = true;
        // 获取元素ele 在k个哈希函数中计算的下标值(数组)
        int[] indexArray = getIndexs(ele);
        for (int index : indexArray) {
            flag = flag && this.bitSet.get(index);
        }
        return flag;
    }

    /**
     * 元素ele 在k个哈希函数中计算的下标值(数组)
     * @param ele
     * @return
     */
    private int[] getIndexs(String ele) {
        int[] indexArray = new int[this.k];
        for (int i = 0; i < k; i++) {
            indexArray[i] = HashUtil.md5Hash(ele + "-" + k);
        }
        return indexArray;
    }

    /**
     * 初始化bitSet及相关参数
     */
    private void init() {
        // 获取 bit数组的长度
        this.m = (int) ((-this.n * Math.log(this.p)) / (Math.log(2) * Math.log(2)));
        // 获取 hash函数的个数
        this.k = (int) ((this.m / this.n) * Math.log(2));
        bitSet = new BitSet(this.m);
    }

    public static void main(String[] args) {
        int n = 1000000;
        double p = 0.0003;
        // 测试
        MyBloomFilter bloomFilter = new MyBloomFilter(n, p);

        // 添加元素
        for (int i = 1; i <= n; i++) {
            bloomFilter.addElement("key" + i);
        }

        int num = n * 2;
        // 测试:误判率
        int count = 0;
        for (int i = 1; i <= num; i++) {
            if (bloomFilter.isExist("key" + i)) {
                count++;
            }
        }
        System.out.println("进行判断的key个数:" + num);
        System.out.println("系统判断存在的key:" + count);
        System.out.println("期望误判个数:" + n * p);
        System.out.println("实际误判个数:" + (count - n));
    }
}

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class HashUtil {
    /**
     * md5 hash函数
     * @param str
     * @return
     */
    public static int md5Hash(String str) {
        if (str == null) {
            str = "";
        }
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("md5");
            md5.update(str.getBytes());
            BigInteger bigInteger = new BigInteger(md5.digest());
            return Math.abs(bigInteger.intValue());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return -1;
    }
}

五、缓存击穿

缓存击穿指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

解决方案:

  1. 定时刷新:后台通过定时任务,定时将要过期的数据进行刷新。
  2. 检查更新:通过每一次请求判断当前数据的过期时间,如果小于某一定值,进行刷新。(假如在这莫一定值范围内没有请求,则还是会发生缓存击穿现象)
  3. 多级缓存:采用多级缓存,如二级缓存,一级缓存失效时间短,二级缓存失效时间长。(该方法容易造成缓存空间浪费)
  4. 互斥锁,参考实现代码如下:
static Lock reenLock = new ReentrantLock();
 
public List<String> getData(String key) throws InterruptedException {
	List<String> result = new ArrayList<String>();
	// 从缓存读取数据
	result = getDataFromCache(key);
	if (result.isEmpty()) {
// 获取锁,获取成功,去数据库获取数据,并更新缓存
		if (reenLock.tryLock()) {
			try {
				// 从数据库查询数据
				result = getDataFromDB(key);
				// 将查询到的数据写入缓存
				setDataToCache(key, result);
			} finally {
				reenLock.unlock();// 释放锁
			}
 		} else {
			result = getDataFromCache(key);// 先查一下缓存
			if (result.isEmpty()) {
				Thread.sleep(100);
				return getData(key);// 重试
			}
		}
	}
	return result;
}

六、缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  3. 数据预热:数据预热的含义就是在正式部署之前,先把可能的数据预先加载到缓存中。
  4. 限流(加锁,分布式锁)、缓存降级(设置备用缓存,可能存在数据不一致问题)。

第三方Bloom Filter

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>18.0</version>
</dependency>

创建 Bloom Filter

BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000, 0.00003);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值