数据库与缓存的一致性问题的两个疑问 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在答技术问题时复制粘贴 AI 生成的内容
mitu9527
V2EX    程序员

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

  •  1
     
  •   mitu9527 2022-08-19 17:57:34 +08:00 3215 次点击
    这是一个创建于 1152 天前的主题,其中的信息可能已经有所发展或是发生改变。
    首先,这里不讨论 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     Solana     2978 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 13:42 PVG 21:42 LAX 06:42 JFK 09:42
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86