1、缓存击穿:异步查询数据请求进入之后首先查询缓存,如果命中就返回;如果无法命中,首先开启多线程查询并声明一个原子性的CountDownLatch,查询出数据则放入redis,如果没有给redis值放入null(避免没有数据仍然每次都要去查一次数据库),处理完成后CountDownLatch做countDown处理,让同步等待的并发查询操作全部放行,此时所有请求都可以命中缓存;总结来说就是降低MySQL的压力,面对同时几万甚至更多的查请求只允许一个线程来查数据库,所有线程等待的时候也就一次的查询业务时间。
示例代码:
@RestController
@RequestMapping("/countDownLatch")
public class TestController1 {
@Autowired
RedisTemplate redisTemplate;
@Autowired
MemberService memberService;
private volatile CountDownLatch countDownLatch = new CountDownLatch(1);
@Async
@RequestMapping("/select")
public R select() throws InterruptedException {
MemberEntity data = (MemberEntity) redisTemplate.opsForValue().get("countDownLatch");
if (data == null) {
new Thread(() -> {
MemberEntity memberEntity = memberService.getById(3L);
if (memberEntity != null) {
redisTemplate.opsForValue().set("countDownLatch", memberEntity);
} else {
redisTemplate.opsForValue().set("countDownLatch", null);
}
//模拟请求阻塞
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
countDownLatch.countDown();
}).start();
countDownLatch.await();
MemberEntity data1 = (MemberEntity) redisTemplate.opsForValue().get("countDownLatch");
return R.ok().setData(data1);
}
return R.ok().setData(data);
}
}
2、双写一致性:
方案一:先删除缓存,再更新数据库,最后延迟删除;此方案虽然读到少量脏数据,但在并发量不是极高的系统推荐使用;sleep的时间大于读数据线程读数据+写缓存的时间即可,这样即使本次读到了脏数据,写数据线程随机便会将缓存删掉,新的读的请求会按照1的防止缓存击穿策略处理。
图示:
代码示例:
/**
* @author james
* @create 2022-02-17-2:12
*/
@RestController
@RequestMapping("/countDownLatch")
public class TestController1 {
@Autowired
RedisTemplate redisTemplate;
@Autowired
MemberService memberService;
@Async
@RequestMapping("/update")
public void update() throws InterruptedException {
redisTemplate.delete("countDownLatch");
MemberEntity memberEntity = new MemberEntity();
memberEntity.setId(3L);
memberEntity.setLevelId(2L);
memberService.updateById(memberEntity);
Thread.sleep(100);
redisTemplate.delete("countDownLatch");
}
}
方案二:先更新数据库,再删除缓存。解决方案是消息队列或者其他binlog同步,引入消息队列会带来更多的问题,不推荐直接使用;一般大公司会使用binlog,通过监听binlog的消息队列来做删除缓存的操作,也可以同步写入缓存,但具体是否需要写入根据业务场景来定。这样虽然消息延迟的问题虽然依然存在,但是相比单纯引入消息队列的做法更好一点。
图示:
方案三:设置缓存过期时间,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。此方案只对于一致性要求不是很高并且数据更新不是很频繁的业务,如果数据大量更新又并发不高可以不使用缓存或者方案一。
总结:可以说自己公司业务是以方案一为主,部分时效性不高的业务场景可以说方案三;极高并发下推荐方案二。