榜单模型(四):查询接口的缓存方案和可用性分析

一、查询接口的缓存方案

现在假设我们需要暴露一个查询接口:返回前 100 榜单的文章列表

这个查询接口就是一个高并发并且高可用的接口。你可以预期,类似于微博、小红书之类的,基本上任何用户打开网站或者 APP,都需要调用这个接口

所以,要保证该查询接口的性能,要引入 本地缓存 。

引入本地缓存有两种思路:

  • 缓存提供一个 Redis 实现,提供一个本地实现,同时有一个装饰器同时操作这两个实现。
  • 在 Repository 上直接操作 Redis 实现和本地缓存实现。(本文选择该思路)

如果 Repository 本身还有一些很复杂的逻辑,那么选用方案一,否则直接使用方案二(开发起来快一点)

二、Redis 和 本地缓存 的实现

2.1 方案一:仅用 Redis 实现

(1)repo 层

 

golang

代码解读

复制代码

type CachedRankingRepository struct { // 方案一:仅用 redis 缓存 topN cache cache.RankingCache } func NewCachedRankingRepository(cache cache.RankingCache) RankingRepository { return &CachedRankingRepository{cache: cache} } func (c *CachedRankingRepository) ReplaceTopN(ctx context.Context, articles []domain.Article) error { return c.cache.Set(ctx, articles) } func (c *CachedRankingRepository) GetTopN(ctx context.Context) ([]domain.Article, error) { return c.cache.Get(ctx) }

(2)cache 层

整理了一份Java面试题。这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

golang

代码解读

复制代码

type RankingRedisCache struct { client redis.Cmdable key string expiration time.Duration } func NewRankingRedisCache(client redis.Cmdable) RankingCache { return &RankingRedisCache{client: client, key: "ranking:top_n", expiration: time.Minute * 3} } func (r *RankingRedisCache) Set(ctx context.Context, articles []domain.Article) error { // note 缓存榜单的 top100 文章时,不要缓存文章内容 for _, article := range articles { article.Content = article.Abstract() } // 直接序列化 []article ,存入 redis res, err := json.Marshal(articles) if err != nil { return err } return r.client.Set(ctx, r.key, res, r.expiration).Err() } func (r *RankingRedisCache) Get(ctx context.Context) ([]domain.Article, error) { var res []domain.Article val, err := r.client.Get(ctx, r.key).Bytes() if err != nil { return nil, err } return res, json.Unmarshal(val, &res) }

2.2 方案二:Redis + 本地缓存实现

(1)repo 层 —— 用于组装 Redis 和 本地缓存

 

golang

代码解读

复制代码

type CachedRankingRepository struct { // 方案一:仅用 redis 缓存 topN cache cache.RankingCache // 方案二:用 本地缓存 + redis 缓存 topN // note 传入的是 结构体指针 localCache *cache.RankingLocalCache redisCache *cache.RankingRedisCache } func NewCachedRankingRepositoryV1(localCache *cache.RankingLocalCache, redisCache *cache.RankingRedisCache) *CachedRankingRepository { return &CachedRankingRepository{localCache: localCache, redisCache: redisCache} } // ReplaceTopNV1 本地 + Redis 缓存的 // note 更新:先更新本地缓存,再更新 redis func (c *CachedRankingRepository) ReplaceTopNV1(ctx context.Context, articles []domain.Article) error { // 更新本地缓存 _ = c.localCache.Set(ctx, articles) return c.redisCache.Set(ctx, articles) } // GetTopNV1 本地 + Redis 缓存的 // note 查询:先查本地,再查 redis,最后回写本地 func (c *CachedRankingRepository) GetTopNV1(ctx context.Context) ([]domain.Article, error) { articles, err := c.localCache.Get(ctx) if err == nil { return articles, nil } res, err := c.redisCache.Get(ctx) if err != nil { return c.localCache.ForceGet(ctx) } return res, c.localCache.Set(ctx, res) }

注意:GetTopNV1() 方法中所调用的 ForceGet() 是不考虑本地缓存过期时间强制取出缓存数据的(为了提高可用性,下文会提到)。

(2)cache 层

类似于我们这种本地缓存的实现,是可以直接使用原子操作来实现的,因为本质上我们这里并不需要 一个 key-value 的结构

 

golang

代码解读

复制代码

package cache import ( "context" "errors" "refactor-webook/webook/internal/domain" "sync/atomic" "time" ) // RankingLocalCache 榜单这种本地缓存的实现,可以直接用原子操作,本质上是因为我们不需要一个key-value的结构(所以就不需要类似lru.Cache那样线程安全的kv本地缓存库) type RankingLocalCache struct { topN atomic.Value ddl atomic.Value expiration time.Duration } func (r *RankingLocalCache) Set(ctx context.Context, articles []domain.Article) error { r.topN.Store(articles) r.ddl.Store(time.Now().Add(r.expiration)) return nil } func (r *RankingLocalCache) Get(ctx context.Context) ([]domain.Article, error) { ddl := r.ddl.Load().(time.Time) arts := r.topN.Load().([]domain.Article) if arts == nil || ddl.Before(time.Now()) { return nil, errors.New("本地缓存失效(不存在或过期)") } return arts, nil } // ForceGet 忽略本地缓存的过期时间ddl,从而强制返回数据 func (r *RankingLocalCache) ForceGet(ctx context.Context) ([]domain.Article, error) { arts := r.topN.Load().([]domain.Article) if arts == nil { return nil, errors.New("本地缓存失效(不存在)") } return arts, nil }

三、可用性问题

整个榜单依赖于 数据库 和 Redis 的可用性,那么问题就在于万一这两个东西崩溃了呢

  • 如果 MySQL 崩溃了,那么无法更新榜单了,因为此时的定时任务必然失败,只能查询缓存。
  • 如果 Redis 崩溃了,后果就是一旦节点本身的本地缓存也失效了,那么查询接口就会失败。

最简单的做法就是:

  • 防止 mysql 崩溃:设置 Redis 缓存过期时间为一个比较大的值,甚至永不过期。所以只有在定时任务运行的时候,才会更新这个缓存。

  • 防止 Redis 崩溃:给本地缓存设置一个“兜底”。即正常情况下,我们的会从本地缓存里面获取,获取不到就会去 Redis 里面获取。但是我们可以在 Redis 崩溃的时候,再次尝试从本地缓存获取。此时不会检查本地缓存是否已经过期了。 也就是上面代码中的 ForceGet() 方法的实现。

但是如果一个节点本身没有本地缓存,此时 Redis 又崩溃了,那么这里依旧拿不到榜单数据?

这种情况下,可以考虑走一个 failover(容错)策略,让前端在加载不到热榜数据的情况下,重新发一个请求。这样一来,除非全部后端节点都没有本地数据,Redis 又崩溃了,否则必然可以加载出来一个榜单数据。

四、其他高性能高并发思路

思路一:计算了热榜之后,直接生成一个静态页面,放到 OSS 上,然后走 CDN 这条路。类似的思路还有将数据(一般组装成 JS 文件)上传到 OSS,再走 CDN 这条路。

思路二:直接放到 nginx 上。

思路三:如果是本地 APP,那么可以定时去后面拉数据,拉的热榜数据会缓存在 APP 本地。

这些需要控制住页面或者数据的 CDN 过期时间和前端资源过期时间

五、总结

本地缓存 + Redis 缓存 + 数据库

在大多数时候,追求极致性能的缓存方案,差不多就是本地缓存 + Redis 缓存 + 数据库

查找:先查找本地缓存,再查找 Redis,最后查找数据库。

更新:先更新数据库,再更新本地缓存,最后更新(或者删除)Redis。核心在于一点,本地缓存的操作几乎不可能失败

  • 14
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
#import <Foundation/Foundation.h> typedef void(^SuccessBlock)(NSDictionary *returnDic, NSString *msg); typedef void(^FailureBlock)(NSString *errorInfo); typedef void(^LoadProgress)(float progress); @interface CBCacheHttpRequet : NSObject /** * Get请求 不对数据进行缓存 * * @param urlStr url * @param success 成功的回调 * @param failure 失败的回调 */ +(void)getRequestUrlStr:(NSString *)urlStr success:(SuccessBlock)success failure:(FailureBlock)failure; /** * Get请求 对数据进行缓存 * * @param urlStr url * @param success 成功的回调 * @param failure 失败的回调 */ +(void)getRequestCacheUrlStr:(NSString *)urlStr success:(SuccessBlock)success failure:(FailureBlock)failure ; /** * Post请求 不对数据进行缓存 * * @param urlStr url * @param parameters post参数 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)postRequestUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters success:(SuccessBlock )success failure:(FailureBlock)failure; /** * Post请求 对数据进行缓存 * * @param urlStr url * @param parameters post参数 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)postRequestCacheUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters success:(SuccessBlock )success failure:(FailureBlock)failure; /** * 上传单个文件 * * @param urlStr 服务器地址 * @param parameters 参数 * @param imageStr 上传的key * @param imageData 上传的问件 * @param loadProgress 上传的进度 * @param success 成功的回调 * @param failure 失败的回调 */ +(void)upLoadDataWithUrlStr:(NSString *)urlStr withDic:(NSDictionary *)parameters imageKey:(NSString *)imageStr withImageData:(NSData *)imageData upLoadProgress:(LoadProgress)loadProgress success:(SuccessBlock)success failure:(FailureBlock)failure; //清除接口数据缓存 +(void)deleteCache; @end
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值