聊聊细节 – 你知道缓存的正确打开方式么?(2)

  • 2019 年 10 月 8 日
  • 筆記

上一篇文章:聊聊细节 – 你知道缓存的正确打开方式么?(1) 中介绍了读取缓存时的一些细节,有读就有写,本篇来聊聊,当我们需要更新缓存该怎么做?

当我们通过一些方式:如后台管理系统更新了相关的数据信息,或者用户在一些操作的时候更新了一些数据信息,如果这些信息正好也在缓存里,那一般也需要在更新数据库的时候,也更新缓存.

那更新的流程是什么呢?很多人可能觉的很简单,示例如下?

public function setData($data)  {        //更新数据库      $db->update($data);        //更新缓存      $redis->set($key, $data);        return true;  }

嗯,咋一看,没毛病,但真的是这样么?

场景一:如果更新数据库失败了?

结果可有两个

  1. 更新数据库抛异常了,中断,缓存也不影响
  2. 数据库不抛异常,缓存继续更新,结果会导致 数据库和缓存不一致
  3. 缓存更新失败,数据库和缓存不一致

如果是第二种情况,可能就会带来线上的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并不是原子性的,所以存在下面的时序可能性:

  1. db 更新了 data1
  2. db 更新了 data2
  3. cache缓存了data2
  4. 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 异常处理      }  }

这样的话,并发更新的问题就不存在了,如以下时序:

  1. db 更新了 data1
  2. db 更新了 data2
  3. cache删除了data2
  4. cache 删除了 data1

都是删除cache,都是会回源到db拉到最新数据

(另一个问题:如果先delete cache再update db, 会有什么问题,欢迎留言)

那这个方式是不是就完美了呢?并不了,还存在一些极端的问题,看如下场景:

  1. 请求1读取缓存
  2. 缓存失效,回源数据库
  3. 请求2 更新db
  4. 请求2 删除cache
  5. 请求1 设置cache

这样也导致cache是老数据,但这种场景概率还是很低的(需满足缓存失效,读取db比update db时间还要长)

优化二:异步更新

可以把缓存更新的放到一个异步对列里,进行异步更新,这种方式会带来几个问题

1、逻辑变得更重

2、又引入了一个新的队列依赖

如果不用消息队列,是否可行?

也是可行的,可以直接通过db的binlog进行更新

总结:

利用缓存,本身就要做好数据不一致的预期,但我们还是可以通过细节的把握,让数据不一致的情况尽可能减少。

最后用一张图对比一下:

(思考下最后一种方式带来什么更好的改进?)

下一篇我们来聊聊用redis做锁的一些细节