聊聊细节 – 你知道缓存的正确打开方式么?(2)
- 2019 年 10 月 8 日
- 筆記
当我们通过一些方式:如后台管理系统更新了相关的数据信息,或者用户在一些操作的时候更新了一些数据信息,如果这些信息正好也在缓存里,那一般也需要在更新数据库的时候,也更新缓存.
那更新的流程是什么呢?很多人可能觉的很简单,示例如下?
public function setData($data) { //更新数据库 $db->update($data); //更新缓存 $redis->set($key, $data); return true; }
嗯,咋一看,没毛病,但真的是这样么?
场景一:如果更新数据库失败了?
结果可有两个
- 更新数据库抛异常了,中断,缓存也不影响
- 数据库不抛异常,缓存继续更新,结果会导致 数据库和缓存不一致
- 缓存更新失败,数据库和缓存不一致
如果是第二种情况,可能就会带来线上的bug了,这时同学可能会有如下的优化:
public function setData($data) { try { //更新数据 $ret = $db->update($data); if($ret) { //数据库更新成功,则更新缓存 $redis->set($key, $data); } return true; }catch (Throwable $e) { //TODO 异常处理 } }
针对第三种情况,好像只能不断的重试了.
假设我们操作redis比较正常,但这样就OK了么?
场景二:并发更新的问题?
假如两个请求在并发操作相同的一条数据,由于db的update和cache的set并不是原子性的,所以存在下面的时序可能性:
- db 更新了 data1
- db 更新了 data2
- cache缓存了data2
- cache 缓存了 data1
这样就造成了缓存里的数据是老数据(data1),从而导致缓存与数据库不一致
那怎么处理呢?
有同学可能会说,我先set cache ,再 update db呢?
问题或许更严重,db操作失败的概率可能大于 cache 操作的概率,这样可能导致更多数据不一致的情况
如果要严格的要求更新数据库后,缓存能实时的一致更新 ,确实没有完美的的方案,上述场景中,第二种属于逻辑上的bug,碰到概率比较高,所以我们可以优化一下 ,让不一致的情况变的更少
优化一:set cache 变delete cache
public function setData($data) { try { //更新数据 $ret = $db->update($data); if($ret) { //数据库更新成功,则更新缓存 $redis->delete($key, $data); } return true; }catch (Throwable $e) { //TODO 异常处理 } }
这样的话,并发更新的问题就不存在了,如以下时序:
- db 更新了 data1
- db 更新了 data2
- cache删除了data2
- cache 删除了 data1
都是删除cache,都是会回源到db拉到最新数据
(另一个问题:如果先delete cache再update db, 会有什么问题,欢迎留言)
那这个方式是不是就完美了呢?并不了,还存在一些极端的问题,看如下场景:
- 请求1读取缓存
- 缓存失效,回源数据库
- 请求2 更新db
- 请求2 删除cache
- 请求1 设置cache
这样也导致cache是老数据,但这种场景概率还是很低的(需满足缓存失效,读取db比update db时间还要长)
优化二:异步更新
可以把缓存更新的放到一个异步对列里,进行异步更新,这种方式会带来几个问题
1、逻辑变得更重
2、又引入了一个新的队列依赖
如果不用消息队列,是否可行?
也是可行的,可以直接通过db的binlog进行更新
总结:
利用缓存,本身就要做好数据不一致的预期,但我们还是可以通过细节的把握,让数据不一致的情况尽可能减少。
最后用一张图对比一下:

(思考下最后一种方式带来什么更好的改进?)
下一篇我们来聊聊用redis做锁的一些细节