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

并发情况下写入缓存

  •  
  •   rocky114 · 2021-02-02 08:43:30 +08:00 · 4935 次点击
    这是一个创建于 1389 天前的主题,其中的信息可能已经有所发展或是发生改变。
    比如在 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  
       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   ·   实用小工具   ·   5571 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 56ms · UTC 03:05 · PVG 11:05 · LAX 19:05 · JFK 22:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.