![]() | 1 Dabaicong 2021-02-02 08:47:53 +08:00 看程序怎么对这个缓存数据的利用了,如果要求准确数据,那就得等缓存重建完成;要求不高可以直接用过期的缓存数据 |
![]() | 3 imdong 2021-02-02 08:53:02 +08:00 via iPhone ![]() 快要过期的时候,就更新缓存。 有一个线程锁定去读即可,其他的锁不住就直接返回缓存。 典型的缓存击穿,缓存血崩案例。 |
4 yty2012g 2021-02-02 08:53:50 +08:00 一般套路不是缓存过期就去读库,然后发送回源消息,另一个应用接收回源消息读库写缓存么。这样做保持最终一致性是不需要加锁的。另外看你的数据重要程度吧,重要的数据一般是不允许返回空的 |
![]() | 5 JKeita 2021-02-02 08:54:20 +08:00 这看你对数据容忍度吧,可以接受返回空,就返回空。 |
![]() | 6 JKeita 2021-02-02 08:56:29 +08:00 即使是正常情况下都可能出现网络异常导致客户端请求失败的情况,所以重试机制这种应该客户端去判断。 |
![]() | 7 netnr 2021-02-02 09:00:39 +08:00 via Android 要过期前就调更新缓存,保证缓存数据始终有效,避免多次调更新可以加锁 |
![]() | 8 netnr 2021-02-02 09:04:13 +08:00 via Android 异步更新 |
9 artikle 2021-02-02 09:04:55 +08:00 可以加个缓存标识,这个缓存标识时间比原缓存时间小,要是缓存标识过期,就直接读取缓存返回同时后台读取数据库数据更新缓存。 |
![]() | 11 ksco 2021-02-02 09:32:14 +08:00 ![]() |
![]() | 12 wqhui 2021-02-02 09:45:38 +08:00 如果是不会经常变的数据直接设置不过期,然后自己维护。对于过期的缓存,其它也要读这个数据的线程可以阻塞掉,然后其它线程获取到锁后,再尝试去缓存获取数据,有点类似双检锁。 |
![]() | 13 ksco 2021-02-02 09:46:51 +08:00 补充一下,假设有三个线程同时读取一个过期的 key,singleflight 可以保证只有一个线程读库更新缓存,其他的线程会等待此线程执行完成,然后拿到和此线程相同的返回值。 实现上也比较简单,可以看看上面贴的源码。用其他语言改写应该也问题不大。 |
![]() | 14 darkleave &bsp; 2021-02-02 09:56:16 +08:00 建议了解下缓存更新策略,你这种情况按照 cache aside 或者 read through 的方式去处理就行了 |
![]() | 15 bingoshe 2021-02-02 09:59:40 +08:00 我有点不明白,这里去数据库取数据的时候,为什么需要加锁?这个数据是独占性资源吗?是的话为什么 1000 个并发要维护各自的缓存? 将 1000 个并发生成不 expireTime+random 数,这样就不会在一瞬间都过期了; 任务扫描更新缓存; |
![]() | 16 ksco 2021-02-02 10:10:02 +08:00 @darkleave #14 cache aside 或者 read through 只是解决了正确性,并没有解决并发读的缓存击穿问题。 |
17 pangleon 2021-02-02 10:31:19 +08:00 楼上说的好,为啥非得等查不到采取更新,如果真的是特别热点的数据快过期就去更新 |
![]() | 18 rrfeng 2021-02-02 10:46:35 +08:00 via Android 前段时间才了解到 go 官方 sync 包里有个叫 singleflight 的玩意儿,专做这个。 |
![]() | 20 wy315700 2021-02-02 10:53:49 +08:00 缓存过期这个词有点歧义。 两层含义: 1 缓存存在,但是里面的数值或者时间戳过期了,这种情况下可以先返回过期数据,然后另开一个线程去更新缓存。 2 缓存不存在了,最好避免这种情况以免数据库被击穿,可以另开一个循环线程去定期更新缓存。 |
![]() | 21 xwander 2021-02-02 11:01:46 +08:00 缩小锁粒度吧,既然是读取数据后写入缓存,读没必要锁,锁的是缓存区,这个锁是整个缓存区的全局锁? |
22 enihcam 2021-02-02 11:41:53 +08:00 via Android 布隆过滤器锁,布尔改整数,取 2 代表此 entry 正在访问实体。原子化操作这个表。 |
23 enihcam 2021-02-02 11:43:35 +08:00 via Android 顺便可以做成 circuit breaker,一石二鸟。 |
25 rocky114 OP @pangleon 定期更新比较难维护,要是缓存 key 比较少的还好,要是有几百个类型的缓存都要定期维护就有点麻烦了 |
29 justforlook44444 2021-02-02 14:13:20 +08:00 缓存击穿 |
![]() | 30 Varobjs 2021-02-02 14:45:55 +08:00 @pangleon 我意思取数的时候不光取数据,也包括 TTL,发现要过期了就更新 ---------- 也需要加锁的吧,比如 1000 并发请求,都发现快要过期了(例如 ttl<120 ),都去更新读数据库,效果其实和获取不到缓存数据的时候再更新是一样的。 |
31 axbx 2021-02-02 15:57:07 +08:00 写缓存的时候加一个更新间隔时间,比缓存失效时间短,每次读取的时候去判断一下是否已经过了这个间隔时间,过了的话异步去更新缓存。 |
![]() | 32 cassyfar 2021-02-02 16:12:45 +08:00 一般直接开一个单独线程更新 cache 。当前所有的 cache miss 全部去读数据库。你不停查看 TTL 然后更新只会让你代码特别肿胀,而且如果更新 cache 失败,不还是会失效然后会遇到老问题吗? |
34 petercui 2021-02-02 16:15:57 +08:00 过期或者修改了数据,只需要让缓存失效就行了,然后下次读取的时候再写入缓存。 |
![]() | 35 keepeye 2021-02-02 16:25:35 +08:00 1.sleep 不是不行,就怕雪崩,具体要看并发量和持续时间以及刷新缓存耗时 2.直接返回错误给客户端,让客户端自己重试,这个是可行的,但只适用普通场景 3.若要始终保证缓存有效,那只能单独一个线程,在缓存快要过期前,提前更新缓存 |
36 pangleon 2021-02-02 16:31:45 +08:00 @xxy973211 如果你们数据量少,可以这么干。 但是假如你们有 1000W 数据,REDIS 占用的内存有多少考虑过么?可以通过这个网站计算 http://www.redis.cn/redis_memory/ 所以全部数据不过期适合全部数据量小的情况。 也可以只设置热点数据永不过期,前提是你要知道哪些是热点数据以及热点数据量小的情况。相应的有了 REDIS 缓存预热的说法。 大部分场景下热点数据其实就那么多,大部分是冷数据。所以目前有很多冷热数据的解决方案。这是另一个问题就不在这里讨论了。 楼主的问题是,业内常见的处理他不想用,正常查不到缓存就返回空前端处理一下,就留一个获取到锁的线程去更新。 楼主不想返回空,那么那么多线程在那里轮询类似自旋,就比较烦躁了。 还有一种方案就是 2 套 REDIS,一套过期时间长一些作为备份缓存,过期时间短的查不到去查这个备份的。 问题是 REDIS 在云服务商那不便宜啊,如果数据量一大成本是个问题。 |
![]() | 37 luzhh 2021-02-02 17:52:18 +08:00 Java 的话,用 FutureTask,1000 个请求过来,只有一个请求实际区读取数据库,其他的请求等待第一个请求拿到结果之后返回结果即可。 |
![]() | 38 Foredoomed 2021-02-02 17:58:25 +08:00 都不是,没拿到锁的线程等待 |
39 imjamespond 2021-02-02 19:08:35 +08:00 via Android react 模式加队列即可 |
40 rocky114 OP @Foredoomed php+redis 分布式锁没法实现阻塞等待吧,php+mysql 实现的锁倒是可以阻塞等待 |
![]() | 41 vindurriel 2021-02-02 20:41:36 +08:00 via iPhone 两个方案刚好是 CAP 定理中选 C 还是 A 的问题 方案一选 C 问题是 200ms 不一定够 还得加随机数削峰 方案二选 A 增加了客户端 /使用者的负担 |
![]() | 42 hxndg 2021-02-02 21:18:12 +08:00 我没实现过分布式,不过设计过单机的线程缓存操作之类的。。。。。提个自己的想法 创建一个缓存的队列,命名为缓冲垫,表示没命中,目前正在从数据库拿数据。 如果工作者线程发现缓存没命中,这个数据也没在缓冲垫里,直接去数据库那数据就完了,然后一次性更新数量多一些的数据,如果有局部性可言。 如果工作者线程发现缓存没命中,这个数据在缓冲垫里,那就直接返回,先去做别的事务,等待已经去数据库取数据的工作者线程把数据取回来,再继续执行。 总之就是减少忙等,除非忙等的时间特别短。 |
43 rocky114 OP @hxndg 这里是不允许多个线程直接读取数据库的,因为流量都直接到数据库容易把数据库搞奔溃了,所以需要增加一把锁,我这里的疑问点是其它拿不到锁的线程应该怎么处理?前排给出的方案是存储的缓存值增加 ttl 信息,这样每次读取缓存时判断下快过期的就重新设置一下缓存,这样就保证了缓存不会过期。可能有人会说一段时间没有访问缓存失效了,一下子并发上来还是会遇到问题,我认为一段时间没有访问的缓存不属于热点缓存,访问量应该不大。最后配合上凌晨缓存热更新应该能基本解决这些问题 |
![]() | 44 sujin190 2021-02-03 10:29:41 +08:00 加锁就是了,搞个超高性能的锁服务,如果锁服务也挂了就返回让客户端重试,而且只需要在无缓存的时候才加锁从数据库加载,指单纯用于加锁的话,设计好搞个十倍 redis 性能的,妥妥的 |