并发情况下写入缓存 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
rocky114
V2EX    编程

并发情况下写入缓存

  •  
  •   rocky114 2021-02-02 08:43:30 +08:00 5409 次点击
    这是一个创建于 1715 天前的主题,其中的信息可能已经有所发展或是发生改变。
    比如在 1000 并发的状态下,缓存过期了,这个时候需要读取数据库重新写入缓存,只有获取锁的线程才能读取数据库,其它没有拿到锁的线程如何处理呢?
    第一种方案:sleep(200) 睡眠 200 毫秒,重新到缓存中取数据,取到返回给客户端
    第二种方案:直接返回空数据给客户端,提示稍后重试
    46 条回复    2021-02-03 10:35:11 +08:00
    Dabaicong
        1
    Dabaicong  
       2021-02-02 08:47:53 +08:00
    看程序怎么对这个缓存数据的利用了,如果要求准确数据,那就得等缓存重建完成;要求不高可以直接用过期的缓存数据
    rocky114
        2
    rocky114  
    OP
       2021-02-02 08:50:07 +08:00
    @Dabaicong 过期的缓存已经没数据了,这个时候要直接返回空吗?
    imdong
        3
    imdong  
       2021-02-02 08:53:02 +08:00 via iPhone   1
    快要过期的时候,就更新缓存。
    有一个线程锁定去读即可,其他的锁不住就直接返回缓存。

    典型的缓存击穿,缓存血崩案例。
    yty2012g
        4
    yty2012g  
       2021-02-02 08:53:50 +08:00
    一般套路不是缓存过期就去读库,然后发送回源消息,另一个应用接收回源消息读库写缓存么。这样做保持最终一致性是不需要加锁的。另外看你的数据重要程度吧,重要的数据一般是不允许返回空的
    JKeita
        5
    JKeita  
       2021-02-02 08:54:20 +08:00
    这看你对数据容忍度吧,可以接受返回空,就返回空。
    JKeita
        6
    JKeita  
       2021-02-02 08:56:29 +08:00
    即使是正常情况下都可能出现网络异常导致客户端请求失败的情况,所以重试机制这种应该客户端去判断。
    netnr
        7
    netnr  
       2021-02-02 09:00:39 +08:00 via Android
    要过期前就调更新缓存,保证缓存数据始终有效,避免多次调更新可以加锁
    netnr
        8
    netnr  
       2021-02-02 09:04:13 +08:00 via Android
    异步更新
    artikle
        9
    artikle  
       2021-02-02 09:04:55 +08:00
    可以加个缓存标识,这个缓存标识时间比原缓存时间小,要是缓存标识过期,就直接读取缓存返回同时后台读取数据库数据更新缓存。
    rocky114
        10
    rocky114  
    OP
       2021-02-02 09:23:31 +08:00
    @netnr 缓存太多,定期更新不可维护吧?要是说每天的凌晨执行一次缓存热更新这个还能接受
    ksco
        11
    ksco  
       2021-02-02 09:32:14 +08:00   1
    wqhui
        12
    wqhui  
       2021-02-02 09:45:38 +08:00
    如果是不会经常变的数据直接设置不过期,然后自己维护。对于过期的缓存,其它也要读这个数据的线程可以阻塞掉,然后其它线程获取到锁后,再尝试去缓存获取数据,有点类似双检锁。
    ksco
        13
    ksco  
       2021-02-02 09:46:51 +08:00
    补充一下,假设有三个线程同时读取一个过期的 key,singleflight 可以保证只有一个线程读库更新缓存,其他的线程会等待此线程执行完成,然后拿到和此线程相同的返回值。

    实现上也比较简单,可以看看上面贴的源码。用其他语言改写应该也问题不大。
    darkleave
        14
    darkleave  
    &bsp;  2021-02-02 09:56:16 +08:00
    建议了解下缓存更新策略,你这种情况按照 cache aside 或者 read through 的方式去处理就行了
    bingoshe
        15
    bingoshe  
       2021-02-02 09:59:40 +08:00
    我有点不明白,这里去数据库取数据的时候,为什么需要加锁?这个数据是独占性资源吗?是的话为什么 1000 个并发要维护各自的缓存?
    将 1000 个并发生成不 expireTime+random 数,这样就不会在一瞬间都过期了;
    任务扫描更新缓存;
    ksco
        16
    ksco  
       2021-02-02 10:10:02 +08:00
    @darkleave #14 cache aside 或者 read through 只是解决了正确性,并没有解决并发读的缓存击穿问题。
    pangleon
        17
    pangleon  
       2021-02-02 10:31:19 +08:00
    楼上说的好,为啥非得等查不到采取更新,如果真的是特别热点的数据快过期就去更新
    rrfeng
        18
    rrfeng  
       2021-02-02 10:46:35 +08:00 via Android
    前段时间才了解到 go 官方 sync 包里有个叫 singleflight 的玩意儿,专做这个。
    admol
        19
    admol  
       2021-02-02 10:49:45 +08:00
    @pangleon 一种兜底策略吧,要是去提前更新的线程出现问题了呢?
    wy315700
        20
    wy315700  
       2021-02-02 10:53:49 +08:00
    缓存过期这个词有点歧义。

    两层含义:
    1 缓存存在,但是里面的数值或者时间戳过期了,这种情况下可以先返回过期数据,然后另开一个线程去更新缓存。
    2 缓存不存在了,最好避免这种情况以免数据库被击穿,可以另开一个循环线程去定期更新缓存。
    xwander
        21
    xwander  
       2021-02-02 11:01:46 +08:00
    缩小锁粒度吧,既然是读取数据后写入缓存,读没必要锁,锁的是缓存区,这个锁是整个缓存区的全局锁?
    enihcam
        22
    enihcam  
       2021-02-02 11:41:53 +08:00 via Android
    布隆过滤器锁,布尔改整数,取 2 代表此 entry 正在访问实体。原子化操作这个表。
    enihcam
        23
    enihcam  
       2021-02-02 11:43:35 +08:00 via Android
    顺便可以做成 circuit breaker,一石二鸟。
    rocky114
        24
    rocky114  
    OP
       2021-02-02 13:28:49 +08:00
    @wy315700 这里的缓存过期是指缓存不存在了
    rocky114
        25
    rocky114  
    OP
       2021-02-02 13:34:03 +08:00
    @pangleon 定期更新比较难维护,要是缓存 key 比较少的还好,要是有几百个类型的缓存都要定期维护就有点麻烦了
    pangleon
        26
    pangleon  
       2021-02-02 13:52:21 +08:00
    @rocky114 我意思取数的时候不光取数据,也包括 TTL,发现要过期了就更新
    rocky114
        27
    rocky114  
    OP
       2021-02-02 13:56:29 +08:00
    @ksco 这个支持分布式吗
    rocky114
        28
    rocky114  
    OP
       2021-02-02 13:57:24 +08:00
    @pangleon 你这个方案好,感谢啊
    justforlook44444
        29
    justforlook44444  
       2021-02-02 14:13:20 +08:00
    缓存击穿
    Varobjs
        30
    Varobjs  
       2021-02-02 14:45:55 +08:00
    @pangleon 我意思取数的时候不光取数据,也包括 TTL,发现要过期了就更新
    ----------

    也需要加锁的吧,比如 1000 并发请求,都发现快要过期了(例如 ttl<120 ),都去更新读数据库,效果其实和获取不到缓存数据的时候再更新是一样的。
    axbx
        31
    axbx  
       2021-02-02 15:57:07 +08:00
    写缓存的时候加一个更新间隔时间,比缓存失效时间短,每次读取的时候去判断一下是否已经过了这个间隔时间,过了的话异步去更新缓存。
    cassyfar
        32
    cassyfar  
       2021-02-02 16:12:45 +08:00
    一般直接开一个单独线程更新 cache 。当前所有的 cache miss 全部去读数据库。你不停查看 TTL 然后更新只会让你代码特别肿胀,而且如果更新 cache 失败,不还是会失效然后会遇到老问题吗?
    xxy973211
        33
    xxy973211  
       2021-02-02 16:15:28 +08:00
    @pangleon 这种数据为啥不直接设置成不过期呢?即使数据库有更新,刷新缓存就行了吧
    petercui
        34
    petercui  
       2021-02-02 16:15:57 +08:00
    过期或者修改了数据,只需要让缓存失效就行了,然后下次读取的时候再写入缓存。
    keepeye
        35
    keepeye  
       2021-02-02 16:25:35 +08:00
    1.sleep 不是不行,就怕雪崩,具体要看并发量和持续时间以及刷新缓存耗时
    2.直接返回错误给客户端,让客户端自己重试,这个是可行的,但只适用普通场景
    3.若要始终保证缓存有效,那只能单独一个线程,在缓存快要过期前,提前更新缓存
    pangleon
        36
    pangleon  
       2021-02-02 16:31:45 +08:00
    @xxy973211 如果你们数据量少,可以这么干。
    但是假如你们有 1000W 数据,REDIS 占用的内存有多少考虑过么?可以通过这个网站计算 http://www.redis.cn/redis_memory/
    所以全部数据不过期适合全部数据量小的情况。
    也可以只设置热点数据永不过期,前提是你要知道哪些是热点数据以及热点数据量小的情况。相应的有了 REDIS 缓存预热的说法。

    大部分场景下热点数据其实就那么多,大部分是冷数据。所以目前有很多冷热数据的解决方案。这是另一个问题就不在这里讨论了。

    楼主的问题是,业内常见的处理他不想用,正常查不到缓存就返回空前端处理一下,就留一个获取到锁的线程去更新。
    楼主不想返回空,那么那么多线程在那里轮询类似自旋,就比较烦躁了。
    还有一种方案就是 2 套 REDIS,一套过期时间长一些作为备份缓存,过期时间短的查不到去查这个备份的。
    问题是 REDIS 在云服务商那不便宜啊,如果数据量一大成本是个问题。
    luzhh
        37
    luzhh  
       2021-02-02 17:52:18 +08:00
    Java 的话,用 FutureTask,1000 个请求过来,只有一个请求实际区读取数据库,其他的请求等待第一个请求拿到结果之后返回结果即可。
    Foredoomed
        38
    Foredoomed  
       2021-02-02 17:58:25 +08:00
    都不是,没拿到锁的线程等待
    imjamespond
        39
    imjamespond  
       2021-02-02 19:08:35 +08:00 via Android
    react 模式加队列即可
    rocky114
        40
    rocky114  
    OP
       2021-02-02 20:11:31 +08:00
    @Foredoomed php+redis 分布式锁没法实现阻塞等待吧,php+mysql 实现的锁倒是可以阻塞等待
    vindurriel
        41
    vindurriel  
       2021-02-02 20:41:36 +08:00 via iPhone
    两个方案刚好是 CAP 定理中选 C 还是 A 的问题 方案一选 C 问题是 200ms 不一定够 还得加随机数削峰 方案二选 A 增加了客户端 /使用者的负担
    hxndg
        42
    hxndg  
       2021-02-02 21:18:12 +08:00
    我没实现过分布式,不过设计过单机的线程缓存操作之类的。。。。。提个自己的想法
    创建一个缓存的队列,命名为缓冲垫,表示没命中,目前正在从数据库拿数据。

    如果工作者线程发现缓存没命中,这个数据也没在缓冲垫里,直接去数据库那数据就完了,然后一次性更新数量多一些的数据,如果有局部性可言。

    如果工作者线程发现缓存没命中,这个数据在缓冲垫里,那就直接返回,先去做别的事务,等待已经去数据库取数据的工作者线程把数据取回来,再继续执行。

    总之就是减少忙等,除非忙等的时间特别短。
    rocky114
        43
    rocky114  
    OP
       2021-02-03 08:44:41 +08:00
    @hxndg 这里是不允许多个线程直接读取数据库的,因为流量都直接到数据库容易把数据库搞奔溃了,所以需要增加一把锁,我这里的疑问点是其它拿不到锁的线程应该怎么处理?前排给出的方案是存储的缓存值增加 ttl 信息,这样每次读取缓存时判断下快过期的就重新设置一下缓存,这样就保证了缓存不会过期。可能有人会说一段时间没有访问缓存失效了,一下子并发上来还是会遇到问题,我认为一段时间没有访问的缓存不属于热点缓存,访问量应该不大。最后配合上凌晨缓存热更新应该能基本解决这些问题
    sujin190
        44
    sujin190  
       2021-02-03 10:29:41 +08:00
    加锁就是了,搞个超高性能的锁服务,如果锁服务也挂了就返回让客户端重试,而且只需要在无缓存的时候才加锁从数据库加载,指单纯用于加锁的话,设计好搞个十倍 redis 性能的,妥妥的
    sujin190
        45
    sujin190  
       2021-02-03 10:32:07 +08:00
    @xxy973211 #33 不过期有个极大问题是一致性维护太难了,写错了就麻烦死了,内存管理也很麻烦,缓存的话过了缓存时间就会从数据库加载,等同于系统有自动修复能力,维护会容易太多了
    hxndg
        46
    hxndg  
       2021-02-03 10:35:11 +08:00
    @rocky114
    如果单纯从缓存热点的角度来考虑,你使用 ttl 是可以做的,但是具体 ttl 的值哪种更合适这个只能测出来,因为不同的流量,ttl 的值可能需要动态变化。

    一段时间没被访问的缓存走数据库是没问题的,但是要防备热点更新,这种比方说很多人每天早上醒过来的时候会看新闻这种。

    你提的实际上就是 lru 算法的各种实现,搜搜看?
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     918 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 28ms UTC 22:00 PVG 06:00 LAX 15:00 JFK 18:00
    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