V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mitu9527
V2EX  ›  程序员

数据库与缓存的一致性问题的两个疑问

  •  1
     
  •   mitu9527 · 2022-08-19 17:57:34 +08:00 · 2728 次点击
    这是一个创建于 835 天前的主题,其中的信息可能已经有所发展或是发生改变。
    首先,这里不讨论 Binlog 方案。

    其次,基于我对该问题的理解,准备用以下实现:
    1 )先更新数据库,再删除缓存。
    2 )通过延时双删解决不一致问题,这里借助消息队列实现了异步的延时双删,以加快吞吐量。
    3 )通过消息队列实现重试,以解决第二步 删除缓存失败 的问题。

    最后,我有两个想问的问题:
    1 )除了侵入了业务代码和引入消息队列会引发的问题以外,上面的方案有啥问题么?
    2 )下列两种伪代码实现,哪种是对的?错在哪里?


    // 伪代码 1
    // 更新数据库
    updateDB();
    // 第一次删除缓存,同步
    result = deleteCache(key);
    if (false == result) {
    // 失败
    sendMessageToMQToDeleteCacheWithoutDelay(key);
    }
    // 第二次删除缓存,异步且延迟
    sendMessageToMQToDeleteKeyWithDelay(key, delay);


    // 伪代码 2
    // 更新数据库
    updateDB();
    // 第一次删除缓存,同步
    deleteCache(key);
    // 即使上面的第一次删除缓存操作失败了,也什么都不做,继续向下执行
    // 第二次删除缓存,异步且延迟
    sendMessageToMQToDeleteCacheWithDelay(key, delay);
    26 条回复    2022-08-20 10:44:24 +08:00
    zjj19950716
        1
    zjj19950716  
       2022-08-19 18:09:40 +08:00   ❤️ 1
    mitu9527
        2
    mitu9527  
    OP
       2022-08-19 18:26:16 +08:00
    @zjj19950716 额,文章看完了,但是不太理解你引入这篇文章想表达什么,能明示么?
    lmshl
        3
    lmshl  
       2022-08-19 18:41:34 +08:00   ❤️ 4
    一般对于缓存问题,我都会先问一句,缓存服务是必须引入的组件吗?
    你的业务量在可预见的未来,会增长到单机数据库难以支撑的程度吗?

    然后问第二句,你是否真的理解 CAP 。不要试图去挑战 CAP ,这里面水太深,你把握不住。

    据我观察除了头部公司几个流量业务外,99% 项目的并发量直到项目死掉都没有超出单机数据库的承载范围,引入 Redis 对你们是否真的有必要?

    如果真的有必要,请熟读 1 楼帖子
    javaisthebest
        4
    javaisthebest  
       2022-08-19 18:44:56 +08:00   ❤️ 1
    个人看法

    我会选第一种
    愿意如下
    1. 服务正常的时候远远多于异常的时候, 代码 1 可以覆盖这种场景
    2. 代码 2 的话无非就是视为每次操作都是异常场景。再次都用 mq 去确保最终一致性

    至于取舍的话,我感觉好像也没啥大的区别。看个人习惯吧
    siweipancc
        5
    siweipancc  
       2022-08-19 18:55:55 +08:00 via iPhone
    证书授权上缓存测试组一年提了上百个 bug ,仅供参考。
    mitu9527
        6
    mitu9527  
    OP
       2022-08-19 19:08:05 +08:00
    第一,都在讨论这个问题了,当然缓存服务是必然的,不然我也不会提问了。
    第二,那个帖子我看过,也看完了,我在讨论 Cache Aside Pattern ,应该不会有人觉得我在讨论另外两种方案吧。
    最后,你都说了这么多了,感觉如果我的理解有问题,你也应该能轻易指出来,就别只说到一半了,能明示么?不胜感激。
    mitu9527
        7
    mitu9527  
    OP
       2022-08-19 19:08:32 +08:00
    @lmshl 第一,都在讨论这个问题了,当然缓存服务是必然的,不然我也不会提问了。
    第二,那个帖子我看过,也看完了,我在讨论 Cache Aside Pattern ,应该不会有人觉得我在讨论另外两种方案吧。
    最后,你都说了这么多了,感觉如果我的理解有问题,你也应该能轻易指出来,就别只说到一半了,能明示么?不胜感激。
    mitu9527
        8
    mitu9527  
    OP
       2022-08-19 19:10:23 +08:00
    @siweipancc 怎么说,有啥经验教训可以传授么?
    LeegoYih
        9
    LeegoYih  
       2022-08-19 20:10:39 +08:00   ❤️ 1
    以前我也纠结过这个问题,始终没有一个完美的方案可以覆盖所有场景,针对不同场景用不同实现比较好。

    普通场景允许短时间内缓存不一致的话,一般用 Cache-Aside pattern 。
    如果缓存不一致可能带来生产问题,比如,可能造成资损,建议还是用 事务 /分布式锁 方式保证强一致。

    Cache-Aside pattern 实现简单,性能也是最好的,很多大厂都在用: https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
    kidlj
        10
    kidlj  
       2022-08-19 21:09:47 +08:00   ❤️ 1
    Friends don't let friends do dual writes.

    更新完数据库紧接着更新缓存或者写入消息队列,这就是双写或者多写,总会有第一步成功了下一步失败的概率(比如网络抖动等原因),这时候就会造成数据不一致。

    我个人的项目实践了一种事件驱动的异步架构,也就是 CDC ( change data capture )架构,选型上使用的是 Debezium + Kafka connector ,当然 Java 生态的也可以用 Canal 替换 Debezium 。简单来说 Debezium 的工作就是监听数据库的写入变更( pg 的 wal log 或 mysql 的 bin log ),为每条变更记录生成一条 Kafka message (包含变更记录的主键 id 等其他字段信息),通过 Kafka connector 自动写入到一个数据库表对应的 Kafka Topic 。采用这种架构处理缓存过期就很简单了。业务端只要更新数据库就可以,(避免了双写的问题),更新缓存的逻辑起一个线程或 goroutine 监听这个表对应的 Kafka topic ,拿到消息以后解析出主键 id ,然后 purge 掉对应记录的缓存。如果消费了这条消息以后 purge 缓存失败怎么办?有这种可能的。Kafka 的 message 有 commit 机制,purge 失败可以一直重试,当成功了以后再 commit 消息。虽然这种架构是异步的,不过得益于 Kafka 的良好吞吐性能,几乎可以做到 real-time 的使用体验。
    mitu9527
        11
    mitu9527  
    OP
       2022-08-19 21:23:45 +08:00
    @kidlj 我开头说的 binlog 指的就是你的这种方案,当然你的方案是完整的。不过就算用你的方案,也应该要在应用程序中更新完数据库后立即删除一次吧。只做更新,不先尝试立即删除一次,然后直接交给其他程序异步删除兜底,这样做可以么?
    kidlj
        12
    kidlj  
       2022-08-19 21:31:06 +08:00 via iPhone
    @mitu9527 不需要立即删除,可以完全避免掉双写的。异步消息几乎是实时的,而且是可重试的,采用 kafka consumer group 还可以多重订阅一个消息。

    当然,采用这种方案会带来架构的改变和额外的维护成本。不过我个人的实践来看,这种架构非常灵活,省去了一些传统上需要用分布式事务或者双写带来的复杂性,维护一套 Kafka + connector 还是值得的。
    mitu9527
        13
    mitu9527  
    OP
       2022-08-19 21:43:41 +08:00
    @kidlj 那应用程序中更新完数据库后,再立即删除缓存一次(哪怕失败也无所谓),然后再用你的方案呢?是不是更好?感觉你的方案依赖性太强了,一旦出事就是灾难性的,所有缓存都不删除了。
    kidlj
        14
    kidlj  
       2022-08-19 21:49:23 +08:00
    @mitu9527 多立即删除这一次就像是心理安慰,没大用处。Kafka 集群挂了,未消费并 commit 的消息是持久化的,集群恢复以后消息还在,有什么好怕的。
    kidlj
        15
    kidlj  
       2022-08-19 21:52:40 +08:00
    如果你指的“灾难性”是指 Kafka 挂掉以后缓存过期的实效性的话,可以适当缩短缓存的存活时间,让它自动快速过期。
    mitu9527
        16
    mitu9527  
    OP
       2022-08-19 21:54:44 +08:00
    @kidlj 我不担心消息队列丢数据,我担心的是实时性。我在研究研究吧,看看是不是可以省去这一次删除操作,目前我认为省去就会删除不及时。
    tairan2006
        17
    tairan2006  
       2022-08-19 22:16:07 +08:00   ❤️ 1
    updateDB 成功了,然后服务挂了
    mitu9527
        18
    mitu9527  
    OP
       2022-08-19 22:22:35 +08:00
    @tairan2006 这种就得用监控 binlog 的方式吧,只要数据库更新成功了,就会事件产生去执行删除缓存操作。是一种情况,但不是我想讨论的问题。
    tairan2006
        19
    tairan2006  
       2022-08-19 22:25:53 +08:00
    @mitu9527 如果你不想做 binlog 方案,另一种方案是给缓存设置过期时间,然后通过过期触发自动更新缓存,这样也可以达到最终一致。
    mitu9527
        20
    mitu9527  
    OP
       2022-08-19 22:29:02 +08:00
    @tairan2006 deleteCache 其实不就是等价于立即过期么。就算设置为立即过期,不还是有问题么?比如:
    ( 1 )缓存刚好失效
    ( 2 )请求 A 查询数据库,得一个旧值
    ( 3 )请求 B 将新值写入数据库
    ( 4 )请求 B 删除缓存(或者设置为立即过期)
    ( 5 )请求 A 将查到的旧值写入缓存
    sy20030260
        21
    sy20030260  
       2022-08-19 22:36:51 +08:00   ❤️ 1
    关乎业务代码的问题,脱离了具体业务场景来做技术选型,无异于纸上谈兵

    回归实际业务场景:引入 cache 解决什么问题?引入 cache 之后期望穿透到 DB 的压力有多少?能否接受短时间内的数据不一致?可以的话最长能容忍多久的不一致?如果确实出现不一致会导致什么问题(用户体验下降 or 资损)...具体业务中还会有更多细节。不回答这些问题,实在无法做技术选型

    在我遇到的大多数业务场景中,如果已经采用 cache-aside ,大多数情况下就是个逻辑简单、高并发、RT 敏感的读接口。这种业务场景对数据一致性并没有那么强的需求,设置个短一点的缓存过期时间就是了,甚至都不需要引入队列(徒增架构复杂度

    在使用 cache-aside 的情况下,还需要纠结两种队列用法之间细微差异的业务场景,可能是我见识太少,实在没见过。如果是真实业务场景的话还希望 OP 分享下,让我也长长见识
    mitu9527
        22
    mitu9527  
    OP
       2022-08-19 22:46:47 +08:00
    @sy20030260 我平时也不会引入消息队列,只是简单更新以下数据库,然后删除缓存。虽说讨论有些空乏,但是并不代表问题不存在啊,所以才拉出来讨论啊,不用实现强一致性,但是不是还是应该尽可能更完善一点呢。
    Jooooooooo
        23
    Jooooooooo  
       2022-08-19 22:55:54 +08:00   ❤️ 1
    你把每一步网络交互都假想会失败再看方案

    比如第一种方案里, updateDB 你这边超时了, 但是数据库成功了, 还要不要往下走? 如何去补偿.

    另外还有并发的问题

    比如第一种方案里, 线程 1 updateDB1 成功之后, 线程 2 紧接着去执行 updateDB2, 和 deleteCache2, 然后线程 1 deleteCache1 才执行, 会不会造成数据错乱呢?
    tairan2006
        24
    tairan2006  
       2022-08-19 23:00:06 +08:00
    @mitu9527 你说的这个并不影响最终一致,只要缓存过期时间设置的不太长就没关系。
    sy20030260
        25
    sy20030260  
       2022-08-19 23:30:23 +08:00
    @mitu9527 这个问题当然是存在的,一个更「完善」的方案当然也是值得追求的。但可能我没表达清楚,核心问题在于:没有具体业务场景作为前提,是没法衡量哪种方案更完善的。方案 A 可能在某场景下是完善的,但在其他场景下可能就远远不如方案 B ,而且甚至起到反效果。这种实际业务中是很常见的。

    啥叫前提啊?你读取一个数,前提是你要知道进制;你问现在几点,前提是你得告诉我啥时区;你求个坐标,前提是你得知道参考坐标系。不给你参考坐标系你会求坐标吗,不会的话就别绕开业务场景谈什么更完善的技术方案
    RedBeanIce
        26
    RedBeanIce  
       2022-08-20 10:44:24 +08:00   ❤️ 1
    如同 21 楼说的那样,关乎业务代码的问题,脱离了具体业务场景来做技术选型,无异于纸上谈兵

    我目前所写到的项目,基本上 binlog 的方案可以解决一致性的问题,
    如果你想更进一步,,我没有想过,我去看代码去了,溜溜。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2612 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 11:01 · PVG 19:01 · LAX 03:01 · JFK 06:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.