图解面试--Redis和多级缓存

科技   2024-11-25 17:56   北京  

提到缓存,大家首先想到是Redis,但是很多访问量大的项目还会设置多级缓存--即根据数据更新特点把一些数据放到本地缓存中,让它们帮Redis抗一波访问,本文我们聊一聊本地缓存和远程缓存的事儿。


我们知道,查询内存的速度比查询磁盘要快, mysql 数据主要存放在磁盘里,如果能将 mysql 里的数据放内存里,查询完全不走磁盘,那必然能大大提升查询性能。

我们很容易想到,可以在商品服务的内存中,申请一个字典,在 python 里叫 dict,在 java 里叫 map。

key 是商品 ID,value 是商品数据。通过商品 ID, 就能查到商品数据。

发生查询时,优先去查内存字典,没结果再跑到 mysql 数据库里查询,再将结果顺手放内存字典里,下次就又能从内存里查出来啦。

像这样,放在服务内部的缓存,就是所谓的本地缓存
有了本地缓存的加持,真正打到 mysql 的查询量就跟你喜欢的女生回你的消息字数一样少,将查询请求干到 1w qps 是很轻松的事情。

远程缓存

但问题又来了,为了保证系统高可用,商品服务经常不止一个实例,如果每个实例都重复缓存一份本地内存,那就有些浪费内存条了。
所以更好的解决方案是将这部分字典内存抽出来,单独做成一个服务。它就是所谓的远程缓存服务

但这就引入另外一个问题,多个商品服务通过网络去读写同一份远程缓存,会存在并发问题。怎么办呢?
很简单!对外不管有多少有个网络连接,收到读写命令后,都统一塞到一个线程上,在一个线程上对字典进行读写,什么并发问题线程切换开销,完全不存在!

这个远程缓存服务足以满足大部分场景,但它属实过于简陋,我们来看下怎么优化它。

多种数据类型支持

现在缓存服务里,只有一个字典类型。key 和 value 都是字符串。但我们平时写代码的时候,还会用到很多其他内存里的数据结构,是不是也可以在缓存服务里提供类似的数据结构?
于是我们对字段的 value 进行扩展,除了 字符串, 还支持先进先出的队列 List 和用于去重的 Set 类型,再加入可以做排行榜的 ZSet,现在缓存服务就更强了。

内存过期策略

缓存服务支持的数据结构变多了之后,塞到内存里的数据就越来越多了,内存又小又贵,迟早扛不住。
怎么办呢?我们可以给缓存里的数据加个过期时间,一旦数据过期,就从内存里删掉,可以很大程度缓解掉内存增长速度。
但问题又又来了,我怎么知道哪些数据该设置多长过期时间呢?
完全没办法,只能交给调用方去做判断,让用户通过 expire 命令的形式来指定哪些数据多久过期

缓存淘汰

但你不能指望每个调用方都是老实人,如果都不设置过期时间,那内存还是得炸。
有解法吗?
有!在内存接近上限的时候,根据一些策略删除掉一些内存。比如可以将最近最少使用的内存删掉,也就是所谓的 LRU,这样不仅解决了内存过大的问题,还让 redis 里的数据全是热点数据。真是一箭双雕。

持久化

现在内存过大的问题是解决了。但还有个问题,mysql 之所以过得那么舒服,那是因为前面有个缓存服务挡住了大部分流量。一旦缓存服务重启,那内存就全丢了,这时候流量会全都打到 mysql 身上,疼得它嗷嗷叫。

所以我们还需要给 redis 加入最大程度的持久化保证。确保服务重启后不至于什么数据都没有。
于是可以在缓存服务里加个异步线程,定期将全量内存数据定期持久化到磁盘文件里,而这种将内存数据生成快照保存到文件的方式,就是所谓的 RDBRedis Database Backup

它可以每隔几分钟记录下缓存服务 的全量数据,类似于游戏的"存档"。
这样就算进程挂了,重启的时候,通过加载快照文件,就能复原大部分数据。
之所以说是大部分,是因为"存档"之后写入的数据可能会丢。
那还有其他方式可以保留更多数据吗?有!
全量数据备份当然耗时,那我们化整为零,在每次写入数据时,顺手将数据记录到文件缓存中,并每将文件缓存刷入磁盘,这种持久化机制叫 AOFAppend Only File,服务启动时跟着文件重新执行一遍就能将大部分数据还原,最大程度保证了数据持久化。

那问题就来了,AOF 文件会不会很大?没事,定期重写压缩就行,比如 a 被依次赋值 a=1,a=2,最终保留 a=2 就够了。

简化网络协议

刚刚提到远程缓存服务对外提供读写能力,那是对外提供的 HTTP 接口吗?
当然不是!
我们知道, HTTP 是基于 TCP 做的通信,实现了很多笨重的特性。
既然当初是为了性能,特地上的缓存服务,那就索性彻底点,抛弃 HTTP,直接基于 TCP 做传输就好!传输协议也设计得简单点,比如只要通过 TCP 传入 SET key value,就能完成写入。传入"GET key" 就能获得对应的 value。非常简洁。
那传输协议的解析需要我们自己写代码去实现吗?
完全不需要,redis 官方提供了一个命令行工具,redis-cli,通过它,我们可以输入一些命令,读写 Redis 服务器里的各种内存数据。

不想用命令行也没关系,各路大神已经用各种语言将 redis-cli 支持的命令实现了一遍,完全不需要自己手写。

Redis 是什么?

好了,到这里,当初那个简陋的远程缓存服务,就成了一个高性能支持多种数据类型和各种缓存淘汰策略,并提供一定持久化能力的超强缓存服务,没错,它就是我们常说的 Redis,全称 Remote Dictionary Server,这名字就很精辟了,说白了 redis 就是个远程的字典服务。

redis 作为架构中最常用的提速神器,是万金油一般的存在,将它放在 mysql 面前挡一道查询只是最基础的用法。
通过扩展插件,还能实现各种高阶玩法。
比如 RedisJSON 支持复杂的 JSON 查询和更新,说白了就是内存版本的 MongoDB


结尾推荐一下我的新专栏《Go项目搭建和整洁开发实战》,本专栏力主实战技能,配备一个完整的实战项目,扫下方二维码可查看已更新章节

 订阅专栏后,可加入专栏配套的实战项目,获得完整实战教程,同时也有专属的读者群,欢迎加入一起学习

专栏分为五大部分,目前已经更新30多节教程,主要内容架构如下:

  • 第一部分介绍让框架变得好用的诸多实战技巧,比如通过自定义日志门面让项目日志更简单易用、支持自动记录请求的追踪信息和程序位置信息、通过自定义Error在实现Go error接口的同时支持给给错误添加错误链,方便追溯错误源头。
  • 第二部分:讲解项目分层架构的设计和划分业务模块的方法和标准,让你以后无论遇到什么项目都能按这套标准自己划分出模块和逻辑分层。后面几个部分均是该部分所讲内容的实践。
  • 第三部分:设计实现一个套支持多平台登录,Token泄露检测、同平台多设备登录互踢功能的用户认证体系,这套用户认证体系既可以在你未来开发产品时直接应用
  • 第四部分:商城app C端接口功能的实现,强化分层架构实现的讲解,这里还会讲解用责任链、策略和模版等设计模式去解决订单结算促销、支付方式支付场景等多种多样的实际问题。
  • 第五部分:单元测试、项目Docker镜像、K8s部署和服务保障相关的一些基础内容和注意事项

扫描上方的海报二维码或者访问 https://xiaobot.net/p/golang 即刻订阅

点击阅读原文可跳转。

网管叨bi叨
分享软件开发和系统架构设计基础、Go 语言和Kubernetes。
 最新文章