在使用redis做全局缓存的时候,基本的流程大概是这样的:
大概流程即为:
①判断缓存是否存在
②若存在则直接返回调用端
③若不存在则从数据库加载数据
④将加载的数据写入缓存
⑤返回调用端
但是现实使用过程中还是有一些问题值得讨论一番
1、redis和数据库双写一致性问题:
当发生写数据库操作的时候,若是insert情况,那我们在插入数据库后,是否也需将新写入的数据更新到缓存里呢?若是update情况,我们是先删除缓存再更新数据库呢?还是先更新数据库再删除旧缓存呢?更新完数据库是否要将新value写入缓存呢?这里有一些最佳实践:
①insert操作情况下,若是热点数据则需写入缓存,若是非热点,可以等到有读请求时再获取数据写入缓存
②update情况,优先删除缓存再更新数据库,再根据具体业务场景判断是否写入缓存,若是更新较多的场景,那就无需写入,若是读较多则更新
③上述update情况,若是读操作大大多于写操作,可能存在此种状况:在数据库读写分离架构下,A线程更新数据时先删除缓存,然后更新了主库数据库值,此时B线程查询此缓存不存在,去从库数据库查到了旧数据(此时主从复制尚未完成)并将旧数据写入缓存,此时缓存中将一直存在的是旧数据。此时可以采取的策略是缓存双删策略,即是A线程先删除缓存再更新数据库,然后线程休眠一定时间(例如1s)后再将key删除,确保最新的数据会被读取并写入缓存。若是不想A线程阻塞休眠影响响应速度,可以起一个异步线程去完成缓存删除操作
2、缓存击穿问题
缓存穿透即是指有在缓存层查询无记录,然后请求击穿缓存层,将访问数据库获取数据,在高并发的场景下,使用频率较高的key失效后可能一瞬间会有大量的请求穿透缓存进行数据库访问,造成数据库层压力过大。还有一种缓存穿透问题,黑客故意虚拟了很多缓存中不存在的key作为请求,而这些请求由于缓存中无记录将全部击穿到数据库,参考解决方案如下:
1、针对恶意攻击可提供一个能迅速判断请求有效性的拦截机制,例如布隆过滤器,内部维护了尽可能全的可能存在的key,而请求到来时,判断若key在过滤器中存在才透传到缓存层,否则直接拒绝或返回空值
2、针对缓存中没有,数据库中也没有记录的情况,我们可以仍然将key缓存起来,但是设置一个较短的过期时间,这样如果下次仍然请求这个不存在的key也将取到缓存不再访问数据库,但是此种做法就是比较占用内存
3、针对缓存失效后大量请求击穿到数据库的问题,可以使用分布式锁来控制只有获取到锁的线程才可以去访问数据库,其余线程可等待一会儿再次查询缓存,样例如下,这样能保证只有一个请求能落到数据库,其余仍从缓存中获取:
public Object getValue(String key){
Jedis jedis = getJedis();
//若key存在则直接获取缓存后返回
if(jedis.exists(key)){
return jedis.get(key);
}else{
//若获取分布式锁成功则查询DB获取数据
if(jedis.setnx(key+"Mutex", "")==1){
Object value = getValueFromDB();
// 回写缓存
jedis.set(key, value.toString());
// 删除锁
jedis.del(key+"Mutex");
return value;
}else{
// 若未获取到锁则睡眠50毫秒再试
Thread.currentThread().sleep(50);
getValue(key);
}
}
}
3、缓存雪崩问题:
缓存雪崩即是大量的key在同一时间失效,高并发场景下造成大量请求直接访问数据库导致数据库压力过大影响响应速度甚至服务宕机,解决方案有如下思路:
1、在设置缓存失效时间时可在末位设置一个随机值,避免集体失效
2、使用互斥锁,此方案会导致吞吐量大大降低
3、双缓存策略,使用A,B两个缓存层,B缓存设置过期时间略晚于A,若A未取到缓存则继续访问B,得到value后再异步起一个线程去更新A、B缓存,此种情况需要B做缓存预热,即在应用启动的时候直接将缓存写入到缓存系统,无需请求来了之后再去写入缓存