前言
最近马D梅因为年前太任性辞了职,碰巧遇到这次疫情,搞得心慌慌。最近四处投简历找工作,毕竟还是要吃饭的。
这不就接到一个公司的面试通知,即将开始一轮尴尬的面试。。。
面试官:为了减轻数据库的压力,通常采用的解决方案有哪些?
马D梅:缓存是最常用一种。包括浏览器缓存、Nginx反向代理缓存、JVM缓存、缓存中间件还有数据库缓存这几个方面。
面试官:就缓存中间件和数据库的问题,对于查询和更新的操作,你是怎么处理的?
马D梅:对于更新请求,先更新数据库再删除缓存;当查询请求过来查询不到缓存时,就会从数据库查询数据并放到缓存中。
面试官:那如果查询请求和更新请求发生并发时,如图1所示,所示步骤是有可能发生的,那如何避免情况?
马D梅:(慌了,我倒还没考虑过,不是编译通过就行了吗。。。)那就先删除缓再更新数据库。
面试官:(这小子乱打一通?)如图2所示,也还是存在问题。
先更新数据库,再删除缓存
如图1所示,当更新请求与查询请求发生并发情况时,会导致缓存与数据库的数据不一致。
先删除缓存,再更新数据库
如图2所示,当更新请求与查询请求发生并发情况时,会导致缓存与数据库的数据不一致。
解决方案
大前提:使用缓存就要容忍数据不一致。能保证最终一致性即可。
设置有效期
好处:缓存超过有效期被淘汰之后,程序会从数据库获取最新的数据重新放入缓存,保持一致性。
坏处:因为缓存失效,从而引发缓存击穿、缓存雪崩等问题。
加锁
不管是单机锁还是分布式锁,这种做法可以保证不同线程之间的操作不会出现不确定的结果,但是实际上在项目中并不会采用这种做法。本来使用缓存就是为了提高效率,现在因为缓存而加锁,只会得不偿失。
所有涉及缓存和数据库的操作都加上锁,极大地降低了处理并发的能力。
缓存中心
上面这些问题归结起来,就是修改缓存的入口太多。
那么为了解决这些问题,我们可以把缓存统一由一个模块来进行管理。
建立缓存中心之后,业务系统就不需要对缓存进行管理,由缓存中心统一对缓存进行维护。
代码示例:
/**
* 建立缓存中心前
*/
public String findHotData() {
// 查询缓存
String result = redisTemplate.opsForValue().get(HOT_DATA_KEY);
if (StringUtils.isNotBlank(result)) {
return result;
}
// 缓存未命中则查询数据库
result = dao.findHotData();
if (StringUtils.isNoneBlank(result)) {
// 将查询的数据放入缓存
redisTemplate.opsForValue().set(HOT_DATA_KEY, result);
}
return result;
}
/**
* 建立缓存中心后
*/
public String findHotData() {
return redisTemplate.opsForValue().get(HOT_DATA_KEY);
}
数据预热
(示意图)
对于一些热点的数据,我们可以通过缓存中心进行以下操作:
- 应用系统或缓存中间件启动(重启)时,把数据加载到缓存中
- 通过后台任务定时刷新缓存
监听日志,更新缓存
(示意图)
(工具:canal)