聊聊缓存

前言

如今开发一定规模的web系统时一般会在项目中加入缓存模块块来缓存数据以减轻数据库的压力。从而提高系统的访问速度。最近的面试过程中也有不少是缓存相关的问题,那么下面就来聊下期间被问到的一个缓存更新策略的问题。 我们以Redis作为缓存作为例子。那么问题来了,当缓存的数据需要更新时该如何做才能确保在保证一致性(没有脏数据)的情况下还能有不错的并发量。

面试过程

通常我们项目中和缓存交道的架构图大体如下:

cache架构图

面试过程中,由于我说自己使用过redis来缓存系统中的一些经常读取但是比较少修改的数据,比如说游戏配置,活动礼包配置,玩家所玩过的游戏这些。 于是面试官遍提出了如下的问题:

你在更新缓存的时候是使用的什么策略

其实工作中并没有怎么考虑这些问题,就直接回答:

这么更新啊,成功写数据库之后再更新Redis中的数据就好啦

接着面试官深入该问题,提出

如果此时写数据库成功了,但是Redis中的数据还没更新,那么就会造成数据不一致,这问题怎么解决?

然后我就想啊:

那我们在更新数据库的数据之前把Redis中对应的缓存数据删除就可以了

面试官接着问:

那如果在删除了对应的缓存,但是还没有写如成功到数据库的时候,这时候又有一个请求来读取这个数据,那旧数据又会被存入缓存,依旧会造成上面提到的数据不一致的问题。

然后我又想啊,的确是有这个问题,那能咋办呢?想想并发相关的数据正确性问题,回答:

那就在写数据的时候对对应的数据加锁。完成写之后才能读取

至此,该问题结束了。面试官也没有再深入的问了。不过我感觉到,面试官似乎并不是那么满意嘛。于是在回去后查询了些资料。查看了缓存更新的相关策略 不过似乎都没有击到痛点。直到前端时间看到了几篇推文,算是有种问题解决的感觉。


问题总结

总结下推文中的初始问题事件: 现在假设有个账户余额表account(uid,money),场景为读多写少。读取用于余额的流程如下:

  1. 根据uid去缓存中读取money
  2. 如果缓存中有(缓存命中),则直接返回对应uid的money
  3. 如果缓存中没有,则从数据库中读取,并将数据存入缓存中

那么问题来了,考虑到多并发,当用户余额需要更新时:

  1. 是先更新数据库中的数据呢,还是先更新缓存中的数据呢
  2. 是直接更新缓存中的数据呢,还是直接淘汰掉缓存中对应的用户的缓存呢

解决方案

对应解决方案:

  1. 那到底是选择更新缓存还是淘汰缓存呢,主要取决于“更新缓存的复杂度”。不过,淘汰缓存操作简单,并且带来的副作用只是增加了一次cache miss,建议作为通用的处理方式。
  2. 如果出现不一致,谁先做对业务的影响较小,就谁先执行。
    • 假设先写数据库,再淘汰缓存:第一步写数据库操作成功,第二步淘汰缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。
    • 假设先淘汰缓存,再写数据库:第一步淘汰缓存成功,第二步写数据库失败,则只会引发一次Cache miss。
    • 结论:数据和缓存的操作时序,结论是清楚的:先淘汰缓存,再写数据库。
  3. 让同一个数据的访问能串行化
    • 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
    • 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的

最后放出前面提到的推文: