一种基于“哨兵”的分布式缓存设计

14年双11大促缓存方案,今天有点闲暇时间,回顾一下当时的思路。

场景介绍:

大促活动下,对于某些产品进行整点秒杀活动。预计流量是平时峰值5+倍

商品计算逻辑比较复杂:某个最终展示的商品属性和价格,可能需要上亿次动态条件计算获得,动态条件每时每刻都在变化,并且商品的库存属性属于行业共有库存,每时每刻都在变化。

计算模型:前端机并发去后端获取实时计算数据,然后合并结果,根据用户信息给商品打属性,排序。

头脑风暴

针对这种场景,有很多方案可以尝试,不过总结起来,大概俩个方向:扩容缓存

扩容

扩容是最容易想到的方式,而且每年大促,根据压测和运营活动预期,都可能有相应扩容。扩容从某种程度上说,也是最简单的方式,如果应用规划足够好,没有状态,那么基本不用开发介入就能完成。

但是如果应用涉及状态信息,那么扩容就没有说的那么轻巧,扩容涉及到增加集群状态;活动结束后,机器下线涉及集群减少状态,这一增一减,增加了运维的成本和系统稳定性。

扩容还有一个不好的地方就是活动结束后,系统水位下降,闲下来4倍的机器,比较浪费。

缓存

相对扩容,缓存是一种从应用角度出发,优化系统的方案。缓存的方案可细分不同粒度,分别适用不同场景。

静态化

静态化能最大限度降低最大限度降低后端压力,一般静态内容可以定时或者通过数据更像触发生成,然后推送到CDN节点。静态化适用于1)读多写少的数据,或者2)能够容忍数据变化延迟的场景。对于本文介绍的场景,并不适合,原因在于商品不满足前面说的两点,并且每个登陆用户看到的产品价格和属性是不同的。

缓存中间结果

静态化这种”一刀切”模式,不能满足针对每个用户的个性化展示需求,如果把每个用户看到的数据都静态化,那缓存的命中率有会很低,基本每个用户请求一两次就不会再来了。而且缓存数据量巨大。

由此想到把缓存粒度缩小,把缓存从展示层后推到前端机上。因为前端机负责汇总后端结算结果,并根据用户信息给商品打属性,排序。

缓存方案尝试

经过头脑风暴,最终确定采用缓存中间结果方式。接下来讨论一下方案细节。

简单粗暴方式

如果缓存有数据,取缓存数据,如果没有,请求后端并把结果更新到缓存。这是一种最简单的缓存模式,但不幸的是不适合秒杀场景,因为秒杀开始的时候,缓存很能没有数据,请求会穿透到后端。

实时缓存,异步更新方式

实时请求数据来自缓存,缓存数据定时异步更新。粗看起来,这个方案不存在缓存穿透的情况,因为数据不会实时从后端计算获取,而是从缓存获取,如果缓存数据存在,直接获取即可。缓存更新可以把用户请求汇总后去重,定时更新。

上面讨论的两种方式都一个共性问题:第一批请求问题:如果第一批请求缓存没有数据怎么办?

简单粗暴的方式会让这样的请求穿透缓存,后端去处理并更新缓存。这样会给后端计算带来压力,秒杀开始那一刹那,很可能支撑不住。

实时缓存的牺牲了这样的请求,因为这些请求根本看不到数据,所以请求失败。这两种方式在本文的应用场景都不合适。

为何不提前初始化缓存?

的确,上面两个方案如果能在第一批请求到达前初始化好缓存,那基本上可以满足本文的应用场景的。而且看起来也很容易做到,提前模拟一次请求或者提前往缓存放一份数据不就可以了吗?

不幸的是,本文场景因为涉及数据范围巨大,不能在较短时间内遍历缓存key,初始化好缓存,即使采取并发方式。而且,初始化缓存请求过多,也将给后端机器造成压力。

缓存失效又该怎么办?

根据需求,两种方案的缓存不会永久有效,如果缓存失败了怎么办?

对于简单粗暴方式,如果缓存失效,又会遇到第一批请求问题,一批请求发现缓存失效,怎么办?看来即使解决了缓存初始化问题,还有可能导致缓存穿透。

实时缓存模式也有类似问题,如果异步更新前数据已经失效了,那么将牺牲一批数据失效后到更新前这批用户。因为没有人去更新数据。

缓存更新问题

不管哪种方式,分布式缓存更新都存在并发问题,尤其在整点秒杀场景更为突出。对于简单粗暴方式,可以采用分布式锁解决:如果缓存穿透的一批请求只有一个会真正打到后端是不是就可以解决了?

实时缓存也有同样的问题,只不过异步请求可以把一段时间内的重复请求合并成一个,从侧面避免了并发问题。

更好的缓存方式

把上面的讨论结合,可以得到一种更优雅的缓存方案,既不牺牲第一批请求,也不存在缓存穿透问题,同时避免并发更新问题。

哨兵

想象有这样一个哨兵线程,只有它能去后端请求实时数据,并更新缓存。

第一批请求场景:

第一批请求中,选取最早的那个请求为哨兵,这个线程不会去读缓存,直接去后端获取计算结果并更新缓存。其他普通线程则自旋+sleep等待,直到哨兵更新缓存后,能拿到数据为止。

缓存失效场景:

哨兵的作用是让缓存永不失效。哨兵线程提前苏醒,去后端获取计算数据并更新缓存。这样,其他普通线程根本不会感知到缓存已经失效,他们能一直拿到最新的缓存。

例如,某个key的缓存失效时间的12:00:00,那么哨兵可能在11:59:55的时候苏醒,请求后端并于11:59:57的时候完成缓存数据更新,后续请求线程感知不到数据的更新,一直能取到非过期的数据。

实现细节

哨兵:其实哨兵也是一个普通请求。可以用原子计数器(redis或tair)实现,一个数据有两个key:原子计数器key和数据缓存key,二者缓存时间一致,但是计数器key失效时间比数据key的要早(至少提前一个后端请求RT时间,这样能保证哨兵更新缓存后,不被其他线程感知到)。当请求线程发现缓存没有数据的时候,每个线程去更新计数器,更新后,得到计数器为1的线程,被设置成哨兵线程,其他线程则等待哨兵。

普通请求没有获取到数据的时候,自旋+sleep应该有个超时时间,防止意外情况。如果超时了,根据业务场景选择请求后端数据还是处理失败。

https://blog.csdn.net/jiao_fuyou/article/details/46604697

原文地址:https://www.cnblogs.com/dayandday/p/10999331.html

时间: 2024-08-01 16:25:36

一种基于“哨兵”的分布式缓存设计的相关文章

实现一个基于WCF的分布式缓存系统

前言: 用到分布式的东西很多了,一直想做一个简单的分布式小项目练练手学习下.后来决定来一个简单的分布式缓存的系统. 在企业应用开发中缓存的用例不胜枚举,但是每次更多的是单机的部署与使用,没有对应的需求是一个原因,另一个原因总是好高骛远做过的总是不想再进行修正. 这次的分布式就从最简单的分布式缓存开始.说简单是因为没有实现分布式缓存高深的寻址,或者对备份处理的牛X实现.只是实现了“分布”这个目的,不足之处还请大家指导. 分布的实现方式有哪些? 既然做“分布”,当然要看看主流的“分布”实现方式.小弟

一种基于Orleans的分布式Id生成方案

基于Orleans的分布式Id生成方案,因Orleans的单实例.单线程模型,让这种实现变的简单,贴出一种实现,欢迎大家提出意见 public interface ISequenceNoGenerator : Orleans.IGrainWithIntegerKey { Task<Immutable<string>> GetNext(); } public class SequenceNoGenerator : Orleans.Grain, ISequenceNoGenerator

一种基于zookeeper的分布式队列的设计与实现

package com.ysl.zkclient.queue; import com.ysl.zkclient.ZKClient; import com.ysl.zkclient.exception.ZKNoNodeException; import com.ysl.zkclient.utils.ExceptionUtil; import org.apache.zookeeper.CreateMode; import org.slf4j.Logger; import org.slf4j.Logg

日志系统之基于Zookeeper的分布式协同设计

最近这段时间在设计和实现日志系统,在整个日志系统系统中Zookeeper的作用非常重要--它用于协调各个分布式组件并提供必要的配置信息和元数据.这篇文章主要分享一下Zookeeper的使用场景.这里主要涉及到Zookeeper在日志系统中的使用,但其实它在我们的消息总线和搜索模块中也同样非常重要. 日志元数据 日志的类型和日志的字段这里我们统称为日志的元数据.我们构建日志系统的目的最终主要是为了:日志搜索,日志分析.这两大块我们很大程度上依赖于--ElasticSearch(关于什么是Elast

基于python+mysql+redis缓存设计与数据库关联数据处理

1.添加表 CREATE TABLE tb_signin( id INT, user_name VARCHAR(10), signin_num INT , signin_time DATETIME , gold_coin INT ); INSERT INTO tb_signin VALUES(1, 'ma', 0, NULL, 0), (2, 'he', 0, NULL, 0), (3, 'yu', 0, NULL, 0), (4, 'hai', 0, NULL, 0), (5, 'fang',

EhCache 分布式缓存/缓存集群

开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发依赖库: JDK6. JavaEE5.ehcache-core-2.5.2.jar Email:[email protected] Blog:http://blog.csdn.net/IBM_hoojo http://hoojo.cnblogs.com/ http://hoojo.blogjava.

基于 Scrapy-redis 的分布式爬虫详细设计

基于 Scrapy-redis 的分布式爬虫设计 目录 前言 安装 环境 Debian / Ubuntu / Deepin 下安装 Windows 下安装 基本使用 初始化项目 创建爬虫 运行爬虫 爬取结果 进阶使用 分布式爬虫 anti-anti-spider URL Filter 总结 相关资料 前言 在本篇中,我假定您已经熟悉并安装了 Python3. 如若不然,请参考 Python 入门指南. 关于 Scrapy Scrapy 是一个为了爬取网站数据,提取结构性数据而编写的应用框架. 可

.NET Core应用中使用分布式缓存及内存缓存

.NET Core针对缓存提供了很好的支持 ,我们不仅可以选择将数据缓存在应用进程自身的内存中,还可以采用分布式的形式将缓存数据存储在一个“中心数据库”中.对于分布式缓存,.NET Core提供了针对Redis和SQL Server的原生支持.除了这个独立的缓存系统之外,ASP.NET Core还借助一个中间件实现了“响应缓存”,它会按照HTTP缓存规范对整个响应实施缓存.ASP.NET Core 支持多种不同的缓存. 常见缓存响应的四种方式 1.内存缓存 顾名思义,缓存在内存中,生命周期默认伴

缓存设计(cache-design)

分布式缓存设计 目前常见的缓存方案都是分层缓存,通常可以分为以下几层: 1.1NG本地缓存,命中的话直接返回 1.2 NG没有命中时则需要查询分布式缓存,如redis 1.3 如果分布式缓存没有命中则需要回源到Tomcat在本地堆进行查询,命中之后异步写回redis 1.4以上都没有命中那就只有从DB或者是数据源进行查询,并写回到redis 缓存更新原子性 在写回到redis的时候如果是Tomcat集群, 多个进程同时写那很有可能出现脏数据,这时就会出现更新原子性的问题, 可以有以下解决方案: