常见的缓存问题有:缓存与数据库数据不一致;缓存穿透、缓存击穿、缓存雪崩;缓存并发竞争等;
insert,新增数据至数据库;update,删除缓存中的对应的数据,修改数据库的数据;delete,删除缓存中对应数据,删除数据库的数据;query,查询缓存中的数据,存在数据即返回查询的数据,缓存中不存在数据则查询数据库,数据库存在数据则返回数据库中数据并同步更新到缓存,缓存和数据库都不存在数据返回null。
缓存穿透指用户请求数据,系统缓存中不存在该数据,直接请求了数据库,并且数据库也没有该数据。当大量该类请求发送是,系统在缓存中都没有获取到数据,直接请求了数据库,这会给数据库造成很大压力,甚至直接宕机。
解决方案:
缓存空对象 当请求从缓存和数据都获取不到数据时,将返回的空对象缓存起来,同时设置一个过期时间。但是这种方法会存在两个问题:如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。接口层增加校验(布隆过滤器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; } }缓存击穿指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
解决方案:
定时刷新:后台通过定时任务,定时将要过期的数据进行刷新。检查更新:通过每一次请求判断当前数据的过期时间,如果小于某一定值,进行刷新。(假如在这莫一定值范围内没有请求,则还是会发生缓存击穿现象)多级缓存:采用多级缓存,如二级缓存,一级缓存失效时间短,二级缓存失效时间长。(该方法容易造成缓存空间浪费)互斥锁,参考实现代码如下: 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机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。数据预热:数据预热的含义就是在正式部署之前,先把可能的数据预先加载到缓存中。限流(加锁,分布式锁)、缓存降级(设置备用缓存,可能存在数据不一致问题)。创建 Bloom Filter
BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), 1000000, 0.00003);