缓存的正确打开方式(二)

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

 

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

 

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

 

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做锁的一些细节

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章