一文参透:缓存一致性策略以及雪崩、穿透等问题,java 系统架构设计详解
考虑数据一致性,缓存处的代码逻辑都较为标准化,首先取 Redis,击中则返回,未击中则通过数据库来进行查询和同步。
public Result query(String id) {
Result result = null;
//1.从 Redis 缓存中取数据
result = (Result)redisTemplate.opsForValue().get(id);
if (null != result) {
System.out.println("缓存中得到数据");
return result;
}
//2.通过 DB 查询,有则同步更新 redis,否则返回空
System.out.println("数据库中得到数据");
result = Dao.query(id);
if (null != result) {
redisTemplate.opsForValue().set(id,result);
redisTemplate.expire(id,20000, TimeUnit.MILLISECONDS);
}
return result;
}
其他的新增、删除和更新操作,可以直接采用先清空该 Key 下的缓存值再进行 DB 操作,这样逻辑清晰简单,维护的复杂度会降低,而付出代价就是多查询一次。
public void update(Entity entity) {
redisTemplate.delete(entity.getId());
Dao.update(entity);
return entity;
}
public Entity add(Entity entity) {
redisTemplate.delete(entity.getId());
Dao.insert(entity);
return entity;
}
2. 缓存更新策略
适用于做缓存的场景一般都是:访问频繁、读场景较多而写场景少、对数据一致性要求不高。如果上面三个条件都不符合,那维护一套缓存数据的意义并不大了,实际应用中通常都需要针对业务场景来选择合适的缓存方案,下面给出了四种缓存策略,由上到下就是按照一致性由强到弱的顺序。
更新策略特点适用场景
实时更新同步更新保证强一致性,与业务强侵入强耦合金融转账业务等
若实时异步更新(MQ/发布订阅/观察者模式),业务解耦,若一致性存在延迟不适合写频繁场景
失效机制设置缓存失效,有一定延迟,可能存在雪崩适用读多写少,能接受一定的延时
任务调度通过定时任务进行全量更新统计类业务,访问频繁且定期更新
二. 缓存雪崩和击穿
==========
1. 缓存雪崩概念
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
第一种方案比较容易实现,第二种的思路主要是从加阻塞式的排它锁来实现,在缓存查询不到的情况下,每此只允许一个线程去查询 DB,这样可避免同一个 ID 的大量并发请求都落到数据库中。
public Result query(String id) {
// 1.从缓存中取数据
Result result = null;
result = (Result)redisTemplate.opsForValue().get(id);
if (result ! = null) {
logger.info("缓存中得到数据");
return result;
}
//2.加锁排队,阻塞式锁
doLock(id);//多少个 id 就可能有多少把锁
try {
//一次只有一个线程
//双重校验,第一次获取到后面的都可以从缓存中直接击中
result = (Result)redisTemplate.opsForValue
().get(id);
if (result != null) {
logger.info("缓存中得到数据");
return result;//第二个线程,这里返回
}
result = dao.query(id);
// 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询
if (null != result) {
redisTemplate.opsForValue().set(id,result);
redisTemplate.expire(id,20000, TimeUnit.MILLISECONDS);
}
return provinces;
} catch(Exception e) {
return null;
}
finally {
//4.解锁
releaseLock(provinceid);
}
}
private void releaseLock(String userCode) {
ReentrantLock oldLock = (ReentrantLock) locks.get(userCode);
if(oldLock !=null && oldLock.isHeldByCurrentThread()) {
oldLock.unlock();
}
}
评论