写点什么

使用缓存的 9 大误区(下)

2012 年 5 月 30 日

编者按:由 InfoQ 主办的全球架构师峰会将于 2012 年 8 月 10 日 -12 日在深圳举行,为了更好地诠释架构的意义、方法和实践,InfoQ 中文站近期会集中发布一批与架构相关的文章,本篇即为其中之一。InfoQ 也欢迎读者亲身参与到本次全球架构师峰会中,与来自国内外的顶尖架构师进行面对面的交流。报名参会请点击这里

本篇文章在上篇的基础上继续讨论了使用缓存的几个误区,包括:缓存大量的数据集合,而读取其中一部分;缓存大量具有图结构的对象导致内存浪费;缓存应用程序的配置信息;使用很多不同的键指向相同的缓存项;没有及时的更新或者删除再缓存中已经过期或者失效的数据。

缓存大量的数据集合,而读取其中一部分

在很多时候,我们往往会缓存一个对象的集合,但是,我们在读取的时候,只是每次读取其中一部分。 我们举个例子来说明这个问题(例子可能不是很恰当,但是足以说明问题)。

在购物站点中,常见的操作就是查询一些产品的信息,这个时候,如果用户输入了“25 寸电视机”,然后查找相关的产品。这个时候,在后台,我们可以查询数据库,找到几百条这样的数据,然后,我们将这几百条数据作为一个缓存项缓存起来,代码的代码如下:

同时,我们对找出的产品进行分页的显示,每次展示 10 条。其实在每次分页的时候,我们都是根据缓存的键去获取数据,然后选择下一个 10 条数据,然后显示。

如果是使用本地内存缓存,那么这可能不是什么问题,如果是采用分布式缓存,问题就来了。下图可以清楚的说明这个过程,如图所示:

相信大家看完这个图,然后结合之前的讲述应该很清楚了问题所在了:每次都按照缓存键获取全部数据,然后在应用服务器那里反序列化全部数据,但是只是取其中 10 条。

这里可以将数据集合再次拆分,分为例如 25-0-10-products,25-11-20-products 等的缓存项,如下图所示:

当然,查询和缓存的方式有很多,拆分的方式也有很多,这里这是给出一些常见的问题!

缓存大量具有图结构的对象导致内存浪费

为了更好的说明这个问题,我们首先看到下面的一个类结构图,如图:

如果我们要把一些 Customer 数据缓存起来,这里就可以可能出现两个问题:

  1. 由于使用.NET 的默认序列化机制,或者没有适当的加入相应 Attribute(属性),使得缓存了一些原本不需要缓存的数据。
  2. 将 Customer 缓存的时候,同时,为了更快的获取 Customer 的 Order 信息,将 Order 信息缓存在了另外一个缓存项中,导致同一份数据被缓存两次。

下面,我们就分别来看看这两个问题。

首先看到第一个。如果我们使用分布式缓存来缓存一些 Customer 的信息的时候,如果我们没有自己重新 Customer 的序列化机制,而是采用的默认的,那么序列化机制在序列化 Customer 的时候,会将 Customer 所引用的对象也序列化,然后在序列化被序列化对象中的其他引用对象,最后的结果就是:Customer 被序列化,Customer 的 Order 信息被序列化,Order 引用的 OrderItem 被序列化,最后 OrderItem 引用的 Product 也会序列化。

整个对象图全部被序列化了,如果这种情况是我们想要的,那么没有问题;如果不是的,那么,我们就浪费了很多的资源了,解决的方法有两个:第一,自己实现序列化,自己完全控制哪些对象需要序列化,我们前面已经讲过了;第二,如果使用默认的序列化机制,那么在不要需要序列化的对象上面加上 [NonSerialized] 标记。

下面,我们看到第二个问题。这个问题主要是由于第一个问题引起的:原本在缓存 Customer 的时候,已经将 Customer 的其他信息,例如 Order,Product 已经缓存了。但是很多的技术人员不清楚这一点,然后又把 Customer 的 Order 信息去缓存在其他的缓存项,使用的使用就根据 Customer 的标识,例如 ID 去缓存中获取 Order 信息,如下代码所示:

解决这个问题的方法也比较明显,参看第一个问题的解决方案就可以了!

缓存应用程序的配置信息

因为缓存是有一套数据失效检测周期的(之前说过,要么是固定时间失效,要么是相对时间失效),所以,很多的技术人员喜欢把一些动态变化的信息保存在缓存中,以充分利用缓存机制的这种特性,其中,缓存程序的配置信息就是其中一个例子。

因为在应用的中的一些配置,可能会发生变化,最简单的就是数据库连接字符串了,如下代码:

当这样设置之后,每隔一段时间缓存失效之后,就去重新读取配置文件,这时候,可能此时的配置就和之前不一样了,并且其他的地方都可以读取缓存从而进行更新,特别是在多台服务器上面部署同一个站点的时候,有时候,我们没有及时的去修改每个服务器上面的站点的配置文件里面的信息,这个时候如何使用分布式缓存缓存配置信息,只要更新一个站点的配置文件,其他站点就全部修改了,技术人员皆大欢喜。OK,这确实看起来是个不错的方法(在必要的时候可以采用一下),但是,不是所有的配置信息都要保持一样的,而且还要考虑怎样一个情况:如果缓存服务器出了问题,宕机了,那么我们所有使用这个配置信息的站点可能都会出问题。

建议对于这些配置文件的信息,采用监控的机制,例如文件监控,每次文件发生变化,就重新加载配置信息。

使用很多不同的键指向相同的缓存项

我们有时候会遇到这样的一个情况:我们把一个对象缓存起来,用一个键作为缓存键来获取这个数据,之后,我们又通过一个索引作为缓存键来获取这个数据,如下代码所示:

我们之所以这样写,主要因为我们会以多种方式来从缓存中读取数据,例如在进行循环遍历的时候,需要通过索引来获取数据,例如 index++ 等,而有些情况,我们可能需要通过其他的方式,例如,产品名来获取产品的信息。

如果遇到这样的情况,那么就建议将这些多个键组合起来,形成如下的形式:

另外一个常见的问题就是:相同的数据被缓存在不同的缓存项中,例如,如果用户查询尺寸为 36 寸的彩电,那么可能有可能一个编号为 100 的电视产品就在结果中,此时,我们将结果缓存。另外,用户在查找一个生产厂家为 TCL 的电视,如果编号为 100 的电视产品又出现在结果中,我们把结果又缓存在另外一个缓存项中。这个时候,很显然,出现了内存的浪费。

对于这样的情况,之前笔者采用的方法就是,在缓存中创建了一个索引列表,如图所示:

当然,这其中有很多的细节和问题需要解决,这里就不一一述说,要看各自的应用和情况而定! 也非常欢迎大家提供更好的方法。

没有及时的更新或者删除再缓存中已经过期或者失效的数据

这种情况应该是使用缓存最常见的问题,例如,如果我们现在获取了一个 Customer 的所有没有处理的订单的信息,然后缓存起来,类似的代码如下:

之后,用户的一个订单被处理了,但是缓存还没有更新,那么这个时候,缓存中的数据就已经有问题!当然,我这里只是列举的最简单的场景,大家可以联想自己应用中的其他产品,很有可能会出现缓存中的数据和实际数据库中的不一样。

现在很多的时候,我们已经容忍了这种短时间的不一致的情况。其实对于这种情况,没有非常完美的解决方案,如果要做,倒是可以实现,例如每次修改或者删除一个数据,就去遍历缓存中的所有数据,然后进行操作,但是这样往往得不偿失。另外一个折中的方法就是,判断数据的变化周期,然后尽可能的将缓存的时间变短一点。

关于作者

汪洋,现任惠普架构师、信息分析师《NET 应用架构设计:模式、原则与实践》作者。上海益思研发管理咨询有限公司首席软件架构专家,软件咨询组副组长。


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012 年 5 月 30 日 00:0011462

评论

发布
暂无评论
发现更多内容

架构师训练营结业作业

superman

2020年7月云主机性能评测报告

BonreeAPM

云计算 测试 公有云 评测 排行榜

在线游戏,如何把握住1个亿以上的DAU?

BonreeAPM

运维 监控 AIOPS 系统 用户体验

Java 基础语法

InfoQ_34a83d636158

直播倒计时|30分钟带你解锁“技术写作”新技能

小红豆

技术 写作 直播 技术创作 RTC征文大赛

字节高级工程师告诉我,想越过开发5年的“分水岭”这样做最适合

周老师

Java 编程 程序员 架构 面试

python——自定义序列类

菜鸟小sailor 🐕

Spring Cloud 微服务实践(0) - 开篇闲话

xiaoboey

微服务 Spring Cloud Spring Boot Spring Framework

活着

GongTeng95

Spring Cloud 微服务实践(2) - Gateway重试机制

xiaoboey

maven Spring Cloud Gateway modules 重试

如何进步神速

Sean

学习 个人成长

关于手机里的IP地址,你不得不知道的“秘密”

脑极体

一次旅途

GongTeng95

Spring Cloud 微服务实践(1) - 用Initializr初始化

xiaoboey

Spring Cloud Eureka Gateway Hoxton

一次压缩引发堆外内存过高的教训

AI乔治

Java kafka JVM

危与机并存 保险业如何走好线上线下业务并举转型之路?

BonreeAPM

运维 监控 保险 AIOPS 系统

不想搞Java了,4年经验去面试10分钟结束,现在Java面试为何这么难

Java架构师迁哥

血的教训!千万别在生产使用这些 redis 指令

云流

redis 学习 编程 程序员

高速路二维码报警定位系统开发,二维码报警定位功能

13530558032

2020年8月北京BGP机房网络质量评测报告

BonreeAPM

测试 机房 评测 排行榜 IDC

Electronjs

Neil

Java Electron 前端框架 前端教程 客户端开发

架构师训练营大作业

叮叮董董

大数据下单集群如何做到2万+规模?

华为云开发者社区

大数据 集群

算法大赛评委亲授通关秘籍,报名倒计时!

易观大数据

CentOS 7 安装 Python 3.7

wong

Python

UML练习1-食堂就餐卡系统设计

博古通今小虾米

UML

LeetCode题解:622. 设计循环队列,使用数组,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

Python基础知识(二)

Python基础

网上赌博输了怎么办?上岸戒赌是唯一的选择

geeker

网上赌博输了怎么办 网上赌博玩快三输了怎办 网上玩快三输了怎么回血 网赌输了怎么戒赌

数字货币量化交易,量化对冲搬砖系统开发

13530558032

实践案例丨云连接CC实现跨区域多VPC与线下IDC Server互联

华为云开发者社区

云服务 IDC

InfoQ 极客传媒开发者生态共创计划线上发布会

InfoQ 极客传媒开发者生态共创计划线上发布会

使用缓存的9大误区(下)-InfoQ