一、为什么DB被打崩了
1.1 穿透到DB被打崩场景还原
代码逻辑:
Redis缓冲查询不到(被穿透),查询DB重建缓冲
但代码不健壮,发生大量并发线程,并发访问热点DB数据
此类情况,轻造成重复浪费,重则DB被打崩
-
真实案例:突增了每秒一万次查询后,导致DB CPU100%,造成某个模块故障
- 如图,某个时刻Redis被击穿,大量查询DB, 可以看到DB在一段时间除以不可用状态

1.2 原始代码有什么问题
public void loadData(){
//1、待查询redis的id list
List<String> ids;
//2、从redis查询
List<String> redisValues = redisUtils.queryByCacheKeys("业务场景"+ids);
//3、如果请求的ID,在redis没有全部查询到,下沉到DB查询
if(CollectionUtils.isEmpty(redisValues) || ids.size()>redisValues.size()) {
//Redis 未查询到的id list
List<String> redisMissIds;
//查询db
List<Object> objectList = dao.queryObjectFromMasterDB(redisMissIds);
//4、回源从db设置redis缓冲
redisUtils.batchSetCaches(buildCache(dos), redis超时时间);
}
}
1.2.1 问题1:redis穿透的id,未限流全部下沉查询DB
redis穿透的id ,理论上热点数据是存在较多重复的,所以对重复数据应该进行加锁达到限流目的。
避免瞬时大并发重复下沉到DB查询,导致DB变慢,特别是一些耗时比较大的查询
1.2.2 问题2:DB查不到时,无法设置Redis,造成持续Redis穿透
如代码,只有db查询到的结果,才能回源设置了redis
当db查询结果是空时,无法在Redis设置缓冲,下次查询时候依然穿透到DB进行查询
二、优化解决策略
- 策略1:穿透Redis的id,去重加锁限流后查询DB,确保同一时间一个id只访问一次DB
- 策略2:DB不存在的值,也要设置Redis,防止持续穿透
- 策略1解决思路如下
- 如果6次sleep共计90ms后 Redis缓冲依然未建立,则查询DB,并回源重建redis缓冲
- 后续redis穿透的的id,如果发现存在查询DB处理中标识,线程sleep后,再访问重建后Redis缓冲
- 由于线程宝贵,sleep时间采用衰减策略,第一次sleep 20ms,后续每次次衰减减少2毫秒
- 大部分情况20ms,可以查询db并重建redis,衰减机制可以根据中间件性能,自行优化调整
- 之所以用衰减方式,而非直接sleep 200ms,是因为线程是JVM中非常宝贵的限量资源,新开需要花费额外成本
- 对每个需访问DB的id,在Redis设置处理中标识,并继续访问DB
- 代码样例如下:
//一、id分类,处理中的id(用于限流),待处理的id,锁list //1.1 被其他线程锁持有ID(其他线程在查询db处理中) List<String> processingIds = Lists.newArrayList(); //1.2 本线程成功加锁持有的ID,需要查询db List<String> lockedIds = Lists.newArrayList(); //1.3 本线程设置的redis锁标识,用于处理完成后,统一释放 List<ILock> alLocks = Lists.newArrayList(); //二、redisMissIds 表示从redis中没有查到的ID list for (String redisMissId : redisMissIds) { //2.1设置redis锁标识 ILock lock = iLockFactory.getLock(“业务类型-”+redisMissId,锁有效期毫秒); alLocks.add(lock); //2.2该ID加锁失败,说明被其他线程加锁并查询db中 //放弃竞争锁,等待其他线程处理完成后从redis中取值 if (!lock.lock(lockKeysEnum.getTimeout())) { processingIds.add(redisMissId); } else { //该ID加锁成功,需要到db查询 lockedIds.add(redisMissId); } } //三、成功获取锁的ID,查DB并放入Redis try { if (!CollectionUtils.isEmpty(lockedIds)) { //3.1 db查询有结果,设置redis List<Object> dos = dao.getByIdsFromMaster(lockedIds); redisUtils.batchSetCaches(buildCache(dos), redis超时时间); //3.2 db查询无结果,也设置redis //结构参考文档之前提到的内容 } } finally { // redis处理中标识释放 for (ILock lock : locks) { lock.releaseLock(); } } //四、未获取锁的ID,获取其他线程重建后的缓存 if (!CollectionUtils.isEmpty(processingIds)) { //衰减6次,共计90ms ,从redis 获取值 for (int i = 1; i <= 6; i++) { try { //4.1 建议用pipeline + mget提升性能 redisUtils.queryByCacheKeys(“业务场景”+processingIds); //4.2 判断全部请求key ,都取到redis值,查询终止直接返回 //4.3 部分ID未取到redis值,线程休眠等待 //线程很宝贵,第一次休眠后,大概率可以取到值,后续休眠时间衰减 Thread.sleep(20 - i * 2); } catch (InterruptedException e) { Logger.warn("Thread.sleep异常", e); } } } //五、如果缓存中获取6次(90ms)依然未拿到,放弃等待查询db - 策略2解决思路如下
- redis结构为(String结构为例)
key:业务场景+id, value:json:{ key:业务场景+id, dataEmpty:true|false,false表示该值为空,无需查db重建 dataValue:xxx }
-
TIPS 对于Redis没有,DB也没有的数据,如果会高频查询,必须在Redis设置标识,减少查询DB
- 如果设置了空缓冲,当DB设置有数据时,一定要移除或修改Redis缓冲标识
本文分析了DB被打崩的原因,重点在于Redis穿透问题,包括未限流的查询DB和DB查不到时无法设置Redis,导致持续穿透。提出了两种优化策略:一是对穿透Redis的id进行去重加锁限流;二是即使DB不存在的值,也要设置Redis以防止持续穿透。详细阐述了策略的实现思路和代码示例。

155

被折叠的 条评论
为什么被折叠?



