本文概述
在现代Web开发中, 缓存是加快处理速度的一种快速而强大的方法。正确完成缓存后, 可以显着改善应用程序的整体性能。如果做错了, 那肯定会在灾难中结束。
你可能知道, 缓存失效是计算机科学中最困难的三个问题之一, 另外两个是命名问题和一个错误。一种简单的解决方法是, 每当发生更改时, 使左右所有内容失效。但这违反了缓存的目的。你只想在绝对必要时使缓存无效。
如果你想充分利用缓存, 则需要特别注意使无效的内容, 并避免应用程序浪费重复工作中的宝贵资源。
在此博客文章中, 你将学习一种技术, 可以更好地控制Rails缓存的行为:特别是实现字段级缓存失效。该技术依赖于Rails ActiveRecord和ActiveSupport :: Concern以及对触摸方法行为的操纵。
这篇博客文章基于我最近在一个项目中的经验, 在该项目中, 我们发现在实现字段级缓存无效后性能得到了显着改善。它有助于减少不必要的缓存失效和重复渲染模板。
Rails, Ruby和性能
Ruby不是最快的语言, 但总的来说, 它是考虑到开发速度的合适选择。此外, 其元编程和内置的特定领域语言(DSL)功能为开发人员提供了极大的灵活性。
像雅各布·尼尔森(Jakob Nielsen)的研究这样的研究表明, 如果一项任务花费10秒钟以上, 我们将失去注意力。重新获得我们的关注需要时间。因此, 这可能会出乎意料地昂贵。
不幸的是, 在Ruby on Rails中, 通过模板生成超过10秒的阈值非常容易。你不会在任何” hello world”应用程序或小型宠物项目中看到这种情况, 但是在真实世界的项目中, 很多东西都加载到单个页面上, 相信我, 模板生成很容易开始拖延。
而且, 这正是我在项目中必须解决的问题。
简单优化
但是, 你如何精确地加快速度?
答案:基准测试和优化。
在我的项目中, 两个非常有效的优化步骤是:
- 消除N + 1个查询
- 介绍模板的良好缓存技术
N + 1个查询
解决N + 1个查询很容易。你可以做的是检查日志文件, 每当你在日志中看到多个如下所示的SQL查询时, 都可以用急切的加载替换它们以消除它们:
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
Learning Load (0.3ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
Learning Load (0.4ms) SELECT 'learnings'.* FROM 'learnings' WHERE 'project'.'id' = ?
为此有一个宝石, 称为子弹, 以帮助检测这种低效率。你还可以遍历每个用例, 同时, 通过按照上述模式检查日志来检查日志。通过消除所有N + 1效率低下的情况, 你可以确信自己不会超载数据库, 并且花费在ActiveRecord上的时间将大大减少。
进行这些更改后, 我的项目已经运行得更加轻松。但是我决定将其提升到一个新的水平, 看看是否可以进一步降低加载时间。模板中仍有相当多的不必要的渲染, 最终, 片段缓存帮助了这一点。
片段缓存
片段缓存通常有助于显着减少模板生成时间。但是默认的Rails缓存行为并未减少我的项目。
Rails片段缓存背后的想法很棒。它提供了一种超级简单有效的缓存机制。
Ruby On Rails的作者在Signal v。Noise中写了一篇很好的文章, 介绍片段缓存的工作原理。
假设你有一些用户界面, 其中显示了实体的某些字段。
- 在页面加载时, Rails根据实体的类和updated_at字段计算cache_key。
- 使用该cache_key, 它检查高速缓存中是否有与该键关联的任何内容。
- 如果缓存中没有任何内容, 则会为视图呈现该片段的HTML代码(新呈现的内容存储在缓存中)。
- 如果使用该键在缓存中存在任何现有内容, 则将使用缓存的内容呈现视图。
这意味着缓存永远不需要显式地失效。每当我们更改实体并重新加载页面时, 都会为该实体呈现新的缓存内容。
默认情况下, Rails还提供了使子实体发生更改时使父实体的缓存无效的功能:
belongs_to :parent_entity, touch: true
当包含在模型中时, 将在触摸子级时自动触摸父级。你可以在此处了解有关触摸的更多信息。这样, Rails为我们提供了一种简单有效的方法来同时使父实体的缓存和子实体的缓存无效。
在Rails中缓存
但是, 在Rails中创建缓存是为了服务于用户界面, 其中代表父实体的HTML片段包含仅代表父子实体的HTML片段。换句话说, 在此范例中表示子实体的HTML片段不能包含来自父实体的字段。
但这不是现实世界中发生的事情。你可能非常需要在Rails应用程序中执行违反此条件的操作。
你将如何处理用户界面在表示子实体的HTML片段内显示父实体的字段的情况?
如果子级包含父级实体的字段, 则说明Rails的默认缓存失效行为存在问题。
每次修改从父实体显示的字段时, 你都需要触摸属于该父实体的所有子实体。例如, 如果修改了Parent1, 则需要确保Child1和Child2视图的缓存均无效。
显然, 这可能会导致巨大的性能瓶颈。每当父母更改时, 触摸每个子实体将导致很多数据库查询, 这没有充分的理由。
另一种类似的情况是当与has_and_belongs_to关联的实体出现在列表中, 并且修改这些实体时, 会通过关联链启动一系列的缓存失效。
class Event < ActiveRecord::Base
has_many :participants
has_many :users, through: :participants
end
class Participant < ActiveRecord::Base
belongs_to :event
belongs_to :user
end
class User < ActiveRecord::Base
has_many :participants
has_many :events, through :participants
end
因此, 对于上述用户界面, 在用户位置更改时触摸参与者或事件将是不合逻辑的。但是, 当用户名更改时, 我们应该同时触摸事件和参与者, 不是吗?
因此, 如上所述, “信号与噪声”一文中的技术对于某些UI / UX实例效率不高。
尽管Rails对于简单的事情非常有效, 但实际项目却有其自身的复杂性。
现场级Rails缓存无效
在我的项目中, 我一直在使用小型Ruby DSL来处理上述情况。它使你可以声明性地指定将通过关联触发缓存无效的字段。
让我们看一下它真正有帮助的几个例子:
范例1:
class Event < ActiveRecord::Base
include Touchable
...
has_many :tasks
...
touch :tasks, in_case_of_modified_fields: [:name]
...
end
class Task < ActiveRecord::Base
belongs_to :event
end
该代码片段利用了Ruby的元编程能力和内部DSL功能。
更具体地说, 仅事件中的名称更改将使相关任务的片段缓存无效。更改事件的其他字段(例如目的或位置)不会使任务的片段缓存无效。我将其称为字段级细粒度缓存无效控件。
范例2:
让我们看一个示例, 该示例通过has_many关联链显示缓存失效。
下面显示的用户界面片段显示了任务及其所有者:
对于此用户界面, 仅当任务更改或所有者名称更改时, 代表任务的HTML片段才应无效。如果所有者的所有其他字段(如时区或首选项)都发生了更改, 则任务缓存应保持不变。
这是使用此处显示的DSL实现的:
class User < ActiveRecord::Base
include Touchable
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
...
end
class Task < ActiveRecord::Base
has_one owner, class_name: :User
end
DSL的实现
DSL的主要本质是触摸方法。它的第一个参数是关联, 而下一个参数是触发该关联的字段列表:
touch :tasks, in_case_of_modified_fields: [:first_name, :last_name]
此方法由Touchable模块提供:
module Touchable
extend ActiveSupport::Concern
included do
before_save :check_touchable_entities
after_save :touch_marked_entities
end
module ClassMethods
def touch association, options
@touchable_associations ||= {}
@touchable_associations[association] = options
end
end
end
在这段代码中, 重点是我们存储了触摸调用的参数。然后, 在保存实体之前, 如果指定的字段被修改, 我们将关联标记为脏。如果关联脏了, 我们在保存后触摸该关联中的实体。
然后, 关注点的私有部分是:
...
private
def klass_level_meta_info
self.class.instance_variable_get('@touchable_associations')
end
def meta_info
@meta_info ||= {}
end
def check_touchable_entities
return unless klass_level_meta_info.present?
klass_level_meta_info.each_pair do |association, change_triggering_fields|
if any_of_the_declared_field_changed?(change_triggering_fields)
meta_info[association] = true
end
end
end
def any_of_the_declared_field_changed?(options)
(options[:in_case_of_modified_fields] & changes.keys.map{|x|x.to_sym}).present?
end
…
在check_touchable_entities方法中, 我们检查声明的字段是否更改。如果是这样, 我们通过将meta_info [association]设置为true来将关联标记为脏。
然后, 在保存实体之后, 我们检查脏连接并在必要时触摸其中的实体:
…
def touch_marked_entities
return unless klass_level_meta_info.present?
klass_level_meta_info.each_key do |association_key|
if meta_info[association_key]
association = send(association_key)
association.update_all(updated_at: Time.zone.now)
meta_info[association_key] = false
end
end
end
…
而且, 就是这样!现在, 你可以使用简单的DSL在Rails中执行字段级缓存失效。
总结
Rails缓存可以相对轻松地提高应用程序的性能。但是, 现实世界中的应用程序可能很复杂, 并且常常带来独特的挑战。默认的Rails缓存行为在大多数情况下都可以很好地工作, 但是在某些情况下, 缓存失效的更多优化可能会走很长一段路。
既然你知道如何在Rails中实现字段级缓存失效, 那么就可以防止应用程序中不必要的缓存失效。
评论前必须登录!
注册