谨防 ActiveSupport::Cache::Store 缓存 nil 值
Rails 中的 active_support 组件主要基于 Rails 需要提供了很多非常有用的基础工具以及对 Ruby 内置类进行扩展。其中的 cache 模块主要提供了 Rails 中底层缓存的定义以及简单实现。今天要跟大家探讨的是之前在使用此模块所遇到的一个坑,有兴趣学习其基本用法的可以点击以下两个链接:
Rails Guides: ActiveSupport::Cache::Store
Rails API: ActiveSupport::Cache::Store
从 ActiveSupport::Cache::Store#fetch 聊起
之前在实现一个需要从外部服务请求数据的功能时,处于性能考虑,我在代码中使用了缓存,并且设置缓存失效时间为 7 天,示例代码如下:
def read_external_service(params)# 这段代码稍微解释下:# 当缓存命中时,则直接读取缓存,如果无期待缓存,则通过 HTTP 向外请求结果,并且将结果# 缓存下来,这样子,当下次继续调用时,则可直接返回缓存内容,而无需重复向外请求#Rails.cache.fetch 'example_cache_key_here', expires_in: 7.days doresponse = HTTParty.get 'https://example.com/example/request/path'JSON.parse(response.body)["data"]end
end
上面的代码其实不复杂,核心代码就是使用了 ActiveSupport::Cache::Store#fetch
方法。
一切都很正常地运行着,直到有一天,线上系统不断报警,出错原因就是这段代码总是返回 nil
,而调用者又因为没有判断 nil
值,就会出现 undefined method 'xxx' for nil:NilClass
错误。在 debug 时,我尝试了直接调用外部服务接口,发现请求都有正确返回数据,不可能返回 nil
啊,难道是缓存了 nil
值?下面就直接通过代码验证一下!
[1] pry(main)> require 'active_support'
=> true
[2] pry(main)> cache = ActiveSupport::Cache::MemoryStore.new
=> <#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}>
[3] pry(main)> cache.read :nil_value
=> nil
[4] pry(main)> cache.exist? :nil_value
=> false
[5] pry(main)> cache.fetch :nil_value do
[5] pry(main)* nil # this `nil` value will be cached
[5] pry(main)* end
=> nil
[6] pry(main)> cache.read :nil_value
=> nil
[7] pry(main)> cache.exist? :nil_value
=> true
看吧, fetch
方法确实会缓存 nil
值(通过 exist?
方法可以判断是否缓存了指定的 key ),所以系统出错原因就清晰了:在某次代码执行中,我的缓存刚好失效了,所以系统向外部发送了请求,恰巧这时候外部系统因为故障或者其他可能原因,没有返回期待数据,导致代码中最终缓存了 nil
值,在接下来的时间里,虽然外部系统可能恢复了正确服务,可是这时候因为我们的系统已经缓存了 nil
值,所以在每次调用时都返回缓存的 nil
,而不是重新请求正确结果,导致最后不停的报错告警。
这里插播一句,通过后来仔细查阅文档,才发现文档里已经注明:
Nil values can be cached.
╮(╯▽╰)╭ 怪我咯~
解决方案
意识到这个问题之后,解决思路简单粗暴,就是在可能返回 nil
值的地方放弃写入缓存:
def read_external_service(params)cache_key = 'example_cache_key_here'result = Rails.cache.read(cache_key)# 缓存命中,且内容不为 nil ,直接返回缓存内容return result if result.present?# 缓存失效,只能重新请求了~response = HTTParty.get 'https://example.com/example/request/path'result = JSON.parse(response.body)["data"]# 请求结果正确,写入缓存;否则,放弃之~~~Rails.cache.write(cache_key, result, expires_in: 7.days) if result.present?result
end
呃~~~虽然解决问题了,可是,就为了告诉系统不要相信 nil
,就写得这么繁琐,好么?好么?好么?
踏上阅读源码之路
我尝试搜索了 #fetch
方法是否有支持比如 reject_nil
这样的 option,可惜的是,没有!可是真的没有吗?我不信!看源码去!
首先还是拜访下 ActiveSupport::Cache::Store
这个类啦,它可是所有缓存实现类的抽象类,别问我抽象类是什么,就是它明明只说话不干活,但是其他干活的都得向它看齐!好啦,说人话,其实就是说,我们在调用 Rails.cache.read
、Rails.cache.fetch
等读写方法时,这些方法都是在 ActiveSupport::Cache::Store
中定义的,但是它只定义逻辑,而实际底层的读写实现,则都是交由其各种子类实现的,比如前面的 ActiveSupport::Cache::MemoryStore
。
首先让我们来看看 fetch
方法的全部内容:
def fetch(name, options = nil)if block_given?options = merged_options(options)key = namespaced_key(name, options)instrument(:read, name, options) do |payload|cached_entry = read_entry(key, options) unless options[:force]payload[:super_operation] = :fetch if payloadentry = handle_expired_entry(cached_entry, key, options)if entrypayload[:hit] = true if payloadget_entry_value(entry, name, options)elsepayload[:hit] = false if payloadsave_block_result_to_cache(name, options) { |_name| yield _name }endendelseread(name, options)end
从代码中可以看到,当 #fetch
方法调用时没有传递 block 的话,它本质上就是 read
方法的别名而已。而当调用时传递了 block 的话,即如我前面的示例代码,让我们把代码分开看下:
cached_entry = read_entry(key, options) unless options[:force]
payload[:super_operation] = :fetch if payload
entry = handle_expired_entry(cached_entry, key, options)
它首先判断是否设置了 force
选项,如果有,则不读取缓存,由此模拟缓存强制失效;如果未设置 force
选项或者该选项不等于 true value,则尝试读取缓存,并且调用 handle_expired_entry
判断缓存是否仍旧有效。
if entrypayload[:hit] = true if payloadget_entry_value(entry, name, options)
这三行代码,则是在缓存命中时,直接读取缓存内容并且返回。
elsepayload[:hit] = false if payloadsave_block_result_to_cache(name, options) { |_name| yield _name }
end
else
的代码则表示,在缓存无命中时, #fetch
代码直接调用 #save_block_result_to_cache
方法,并且向其传递了一个 block,这个 block 没有干别的事情,它只会执行我们传递给 #fetch
方法的 block,让我们接着往下看看相关的实现:
def save_block_result_to_cache(name, options)result = instrument(:generate, name, options) do |payload|yield(name)endwrite(name, result, options)result
end
可以看到,#save_block_result_to_cache
方法首先执行传递进来的代码块,实际上也就是我们期待在缓存失效时执行的代码,而在获得执行结果 result
后,方法通过调用 #write
方法将结果写入缓存,最后将 result
返回。
通过上面的源码分析,我们可以知道,当缓存失效时,#fetch
方法会直接将其代码块中的代码的返回值不加判断地写入缓存,并且返回该返回值。这里,或许我们可以做点什么,来实现我们想要支持 :reject_nil
的需求?
支持 :reject_nil
option
为了支持 :reject_nil
,我们只需要在写入缓存前判断是否真的需要 nil
值即可,于是我们只需要在 #save_block_result_to_cache
中加入 #write
的前置条件:
def save_block_result_to_cache(name, options)result = instrument(:generate, name, options) do |payload|yield(name)end# options[:reject_nil] && result.nil? 作为前置条件write(name, result, options) unless result.nil? && options[:reject_nil]result
end
话不多说,让我们来重新试验一番:
[1] pry(main)> require 'active_support'
=> true
[2] pry(main)> cache = ActiveSupport::Cache::MemoryStore.new
=> <#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}>
[3] pry(main)> cache.fetch :nil_key1 do
[3] pry(main)* nil
[3] pry(main)* end
=> nil
[4] pry(main)> cache.exist? :nil_key1
=> true
[5] pry(main)> cache.fetch :nil_key2, reject_nil: true do
[5] pry(main)* nil
[5] pry(main)* end
=> nil
[6] pry(main)> cache.exist? :nil_key2
=> false
可以看到,当我们调用 #fetch
方法时,如果没有传递 reject_nil: true
,则 #fetch
方法会默认缓存 nil
值;而如果我们设置 reject_nil: true
的话,则 #fetch
就会放弃写入 nil
值到缓存中。试验成功!!!
基于这样的实现,我的代码就又可以改为如下了:
def read_external_service(params)# 所有改动只是加了一个 `reject_nil: true`,多方便,妈妈再也不用担心我掉到坑里去了Rails.cache.fetch 'example_cache_key_here', expires_in: 7.days, reject_nil: true doresponse = HTTParty.get 'https://example.com/example/request/path'JSON.parse(response.body)["data"]end
end
待会去给 Rails 提交 Pull Request 去 O(∩_∩)O~~
总结
缓存是好个东西,用得好能够让应用性能表现突飞猛进
要注意缓存写入的边界条件,要注意避免缓存了空值,但也并非所有空值都不能缓存(比如有些接口确实就是有可能返回空值嘛),具体看业务,没有绝对的要与不要,反正
:reject_nil
给你了,看你要不要
谨防 ActiveSupport::Cache::Store 缓存 nil 值相关推荐
- System.Web.Caching.Cache类 缓存 各种缓存依赖
原文:System.Web.Caching.Cache类 缓存 各种缓存依赖 Cache类,是一个用于缓存常用信息的类.HttpRuntime.Cache以及HttpContext.Current.C ...
- 浏览器Disk Cache磁盘缓存及其协商缓存、及原生App和浏览器实现缓存的差异
浏览器Disk Cache磁盘缓存及其协商缓存.及原生App和浏览器实现缓存的差异 目录 浏览器Disk Cache磁盘缓存及其协商缓存.及原生App和浏览器实现缓存的差异 1.Memory Cach ...
- disk cache(磁盘缓存) 和 memory cache(内存缓存)的区别
disk cache(磁盘缓存) 和 memory cache(内存缓存)的区别 同: 都属于强缓存,现在浏览器缓存存储图像和网页等(主要在磁盘上),而你的操作系统缓存文件可能大部分在内存缓存中. 使 ...
- 【高并发基础】Cache Aside 缓存模式背后的思想
文章目录 1. 前言 2. 缓存污染 2.1 造成污染的时机 2.2 解决缓存污染 2.2.1 优点 3. Cache Aside 的实现 4. Cache Aside 的遗留问题 5. 一致性问题 ...
- vue获取商品数据接口_基于 request cache 请求缓存技术优化批量商品数据查询接口...
Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓存. 首先,有一个概念,叫做 Request Context 请求上下文,一般来说,在一个 we ...
- Hystrix面试 - 基于 request cache 请求缓存技术优化批量商品数据查询接口
Hystrix面试 - 基于 request cache 请求缓存技术优化批量商品数据查询接口 Hystrix command 执行时 8 大步骤第三步,就是检查 Request cache 是否有缓 ...
- Cache Server缓存服务器
Preferences偏好设置-Cache Server缓存服务器 5.Cache Server:缓存服务器,对缓存服务器进行设置,在选中"Use Cache Server"选项后 ...
- L1 Cache(一级缓存)
CPU缓存(Cache Memory)是位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多.缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU ...
- Sheet弹出视图在SwiftUI中“怪异”行为的解决(第一次弹出视图的可选属性总得到nil值)
问题现象 Xcode 13.3 + SwiftUI 3.0中,在sheet中弹出包含nil属性的子视图时,明明传递的是非nil值,但第一次子视图中获取的却是nil值:但随后传递非nil值的操作,子视图 ...
最新文章
- oracle全组件安装,Oracle text组件安装
- MIT谷歌大脑用AI破解失传的古代文字,被称“现代版罗塞塔石碑”丨ACL 2019
- 两只小熊队高级软件工程第七次作业敏捷冲刺7
- EOJ_1020_铁路调度
- bzoj1207: [HNOI2004]打鼹鼠
- PHP文件 字符集编码!
- jieba分词太慢,怎么办?找jieba_fast
- MYSQL函数应用----替换函数replace()用法
- 交换十六进制的的高低字节
- 那些让程序员炸毛的奇葩需求,说起来满满的都是泪!
- 高质量程序设计指南C++/C试题
- linux中mvn命令的下载与安装
- Javascript鼠标悬停显示子菜单的大型分类菜单
- 为村上隆直播做同传的火山翻译:成立仅3年,拿下5项世界冠军
- 用Matlab将坐标添加到地图上
- bootstrap 初学 1
- 二十、融会贯通之全流程操作
- restTemplate请求设置请求头信息
- 十秒钟入门一分钟搭建Discuz论坛
- 事业单位招聘计算机类面试自我介绍,事业单位面试自我介绍范文2分钟|2019事业单位面试自我介绍范文...
热门文章
- 高通把苹果逼急了?传苹果正大力研发调制解调器
- 拿签证威胁外籍博士后,实验室导师真的可以为所欲为?
- vue 父组件使用keep-alive和infinite-scroll导致在子组件触发父组件的infinite-scroll方法...
- Linux基本命令 帮助命令
- D2Admin - 基于vue的清新后台模板
- 十大最酷云计算应用程序创业企业
- Transaction rolled back because it has rollback-only
- Spring ORM+Hibernate?Out!换 Spring Data JPA 吧!
- DNS域名系统(二)
- java中常用的一些方法