作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
在欧尔班的头像下

被测试的班

Botond是一名熟练的开发人员,他喜欢编写可读的代码. 13岁时,他在一台兼容ZX spectrum的俄罗斯个人电脑上学会了编程.

以前在

Epam
Share

在现代web开发中,缓存是一种快速而强大的加速方式. 如果处理得当, 缓存可以显著提高应用程序的整体性能. 如果做错了,它肯定会以灾难告终.

缓存失效, 你们可能知道, 是计算机科学中最难的三个问题之一——另外两个是命名问题和误码问题吗. 一种简单的解决方法是,无论何时发生变化,都将所有东西都无效. 但这违背了缓存的目的. 您只希望在绝对必要时才使缓存无效.

如果您想充分利用缓存, 您需要特别注意要使哪些内容无效,从而避免应用程序在重复工作上浪费宝贵的资源.

字段级Rails缓存无效

在这篇博文中, 您将学习一种技术,以便更好地控制Rails缓存的行为:具体来说, 实现字段级缓存失效. 这种技术依赖于 Rails ActiveRecord and ActiveSupport这样::关注 以及操纵的 touch 方法的行为.

这篇博文是基于我最近在一个项目中的经验,在这个项目中,我们看到了实现字段级缓存失效后性能的显著提高. 它有助于减少不必要的缓存失效和模板的重复呈现.

Rails, Ruby和性能

Ruby并不是最快的语言, but overall, 考虑到开发速度,这是一个合适的选择. 此外,它的 元编程 以及内置的领域特定语言(DSL)功能为开发人员提供了极大的灵活性.

有研究表明 Jakob Nielsen的研究 这告诉我们,如果一项任务耗时超过10秒,我们就会失去注意力. 重新集中注意力需要时间. 因此,这可能会出乎意料地昂贵.

不幸的是, 在Ruby on Rails中, 通过模板生成,很容易超过10秒的阈值. 你不会在任何“hello world”应用程序或小型宠物项目中看到这种情况, 但在现实世界的项目中,很多东西都被加载到一个页面上, believe me, 模板生成可以很容易地开始拖动.

这正是我在项目中要解决的问题.

简单的优化

但你要怎么加快速度?

答案是:基准测试和优化.

在我的项目中,两个非常有效的优化步骤是:

  • 消除N+1个查询
  • 介绍一种好的模板缓存技术

N+1 Queries

Fixing N+1 queries is easy. 您所能做的就是检查日志文件——每当您在日志中看到如下所示的多个SQL查询时, 通过用急切加载取代它们来消除它们:

学习负荷(0).4毫秒)选择“学习”.*源自“learning”WHERE“project”.'id' = ?
学习负荷(0).3毫秒)选择“学习”.*源自“learning”WHERE“project”.'id' = ?
学习负荷(0).4毫秒)选择“学习”.*源自“learning”WHERE“project”.'id' = ?

有一种宝石,叫做 bullet 来帮助检测这种低效率. 您还可以遍历每个用例和, 与此同时, 通过对照上述模式检查日志来检查日志. 通过消除所有N+1个低效率, 你可以有足够的信心,你不会超载你的数据库,你花在ActiveRecord上的时间将大大减少.

在进行了这些更改之后,我的项目已经运行得更加活跃了. 但我决定把它提升到一个新的水平,看看我是否能进一步降低加载时间. 在模板中仍然有一些不必要的渲染, 并最终, 这就是片段缓存的作用所在.

片段缓存

片段缓存通常有助于显著减少模板生成时间. 但是默认的Rails缓存行为并不适合我的项目.

Rails片段缓存背后的想法非常棒. 它提供了一种超级简单有效的缓存机制.

Ruby On Rails的作者在Signal v上写了一篇非常好的文章. Noise on how 片段缓存工作.

假设您有一个显示实体的一些字段的用户界面.

  • 在页面加载时,Rails计算 cache_key 基于实体的类和 updated_at field.
  • Using that cache_key,它会检查缓存中是否有任何与该键相关的内容.
  • 如果缓存里什么都没有的话, 然后为视图呈现该片段的HTML代码(并且新呈现的内容存储在缓存中).
  • 如果缓存中有任何具有该键的现有内容, 然后用缓存的内容呈现视图.

这意味着永远不需要显式地使缓存无效. 每当我们更改实体并重新加载页面时,都会为该实体呈现新的缓存内容.

Rails, by default, 还提供了在子实体更改时使父实体缓存无效的功能:

Belongs_to:parent_entity, touch: true

当包含在模型中时,将自动执行 touch 当孩子是父母的时候 touched. 你可以了解更多 touch here. With this, Rails为我们提供了一种简单而有效的方法,可以使父实体的缓存与子实体的缓存同时失效.

Rails中的缓存

However, 在Rails中创建缓存是为了服务用户界面,其中表示父实体的HTML片段包含仅表示父实体的子实体的HTML片段. 换句话说, 在这个范例中,表示子实体的HTML片段不能包含来自父实体的字段.

但在现实世界中并不是这样的. 您很可能需要在Rails应用程序中执行违反此条件的操作.

如果用户界面在表示子实体的HTML片段中显示父实体的字段,您将如何处理这种情况?

引用父实体字段的子实体的片段

如果子实体包含来自父实体的字段, 那么你就会遇到Rails默认缓存无效行为的麻烦.

每次从父实体呈现的字段被修改时, 您将需要访问属于该父实体的所有子实体. 例如,如果 Parent1 的缓存,则需要确保 Child1 and Child2 视图都无效.

显然,这会导致巨大的性能瓶颈. 每当父实体发生变化时,触摸每个子实体将导致大量无缘无故的数据库查询.

另一个类似的场景是与 has_and_belongs_to 在名单中列出了协会, 修改这些实体会通过关联链启动缓存失效级联.

“拥有和属于”协会

class Event < ActiveRecord::Base
  has_many:参与者
  Has_many:用户,通过::参与者
end
class Participant < ActiveRecord::Base
  belongs_to:事件
  belongs_to:用户
end
class User < ActiveRecord::Base
  has_many:参与者
  Has_many:事件,通过:参与者
end

So, 对于上述用户界面, 当用户的位置发生变化时,触摸参与者或事件是不合逻辑的. 但是当用户的名字改变时,我们应该同时触摸事件和参与者,不是吗?

信号v中的技巧. 噪声文章对于某些UI/UX实例是低效的,如上所述.

尽管Rails对于简单的事情非常有效,但实际项目有其自身的复杂性.

字段级Rails缓存无效

在我的项目中,我一直在使用一个小型Ruby DSL来处理上述情况. 它使您能够声明性地指定将通过关联触发缓存失效的字段.

让我们来看看它真正有用的几个例子:

Example 1:

class Event < ActiveRecord::Base
  包括可食用的
  ...
  has_many:任务
  ...
  Touch:tasks, in_case_of_modified_fields: [:name]
  ...
end
class Task < ActiveRecord::Base
  belongs_to:事件
end

这个代码片段利用了元编程能力和内部 Ruby的DSL功能.

更具体地说, 只有事件中的名称更改才会使其相关任务的片段缓存失效. 更改事件的其他字段(如目的或位置)不会使任务的片段缓存失效. 我称之为 字段级细粒度缓存失效控制.

仅包含name字段的事件实体的片段

Example 2:

让我们看一个示例,该示例通过 has_many 连锁协会.

下面显示的用户界面片段显示了一个任务及其所有者:

带有事件所有者名称的事件实体的片段

对于这个用户界面, 只有当任务改变或者所有者的名字改变时,表示任务的HTML片段才会失效. 如果所有者的所有其他字段(如时区或首选项)发生变化, 那么任务缓存应该保持完整.

这是使用如下所示的DSL实现的:

class User < ActiveRecord::Base
  包括可食用的
  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 method. 它的第一个参数是一个关联,下一个参数是触发 touch 关于这种联系:

Touch:tasks, in_case_of_modified_fields: [:first_name,:last_name]

方法提供了此方法 Touchable module:

模块可触的
  延长ActiveSupport这样::关注
  included do
    before_save: check_touchable_entities
    after_save: touch_marked_entities
  end
  模块类方法
    Def touch关联,选项
      @touchable_associations ||= {}
      @touchable_associations[association] = options
    end
  end
end

类的参数存储在此代码中,重点是存储类的参数 touch call. 然后,在保存实体之前,如果指定的字段被修改,我们将关联标记为dirty. 如果关联是脏的,我们在保存后触摸该关联中的实体.

那么,关注的私人部分是:

...
  private
  def klass_level_meta_info
    self.class.instance_variable_get(“@touchable_associations”)
  end
  def meta_info
    @meta_info ||= {}
  end
  def check_touchable_entities
    返回,除非klass_level_meta_info.present?
    klass_level_meta_info.Each_pair do |association, change_triggering_fields|
      如果any_of_the_declared_field_changed?(change_triggering_fields)
        Meta_info [association] = true
      end
    end
  end
  def any_of_the_declared_field_changed?(options)
    (选择[in_case_of_modified_fields): & changes.keys.map{|x|x.to_sym}).present?
  end
…

In the check_touchable_entities 方法,我们检查 如果声明的字段发生了变化. 如果是,我们将关联标记为dirty meta_info(协会) to true.

然后,在保存实体之后,检查我们的 肮脏的关联 并在必要时触摸其中的实体:

…
  def touch_marked_entities
    返回,除非klass_level_meta_info.present?
    klass_level_meta_info.Each_key do |association_key|
      如果meta_info association_key
        关联= send(association_key)
        association.update_all (updated_at:时间.zone.now)
        Meta_info [association_key] = false
      end
    end
  end
…

就是这样! 现在,您可以使用一个简单的DSL在Rails中执行字段级缓存失效.

Conclusion

Rails缓存可以相对轻松地提高应用程序的性能. 然而,现实世界的应用程序可能很复杂,并且经常会带来独特的挑战. 默认的Rails缓存行为适用于大多数场景, 但是在某些情况下,对缓存无效进行更多的优化可能会有很大的帮助.

现在您已经了解了如何在Rails中实现字段级缓存无效, 可以防止应用程序中不必要的缓存失效.

了解基本知识

  • DSL代表什么?

    DSL代表“领域特定语言”.”

  • ActiveRecord触摸方法是做什么的?

    touch方法将updated_at/on字段设置为当前时间并保存记录.

就这一主题咨询作者或专家.
预约电话
在欧尔班的头像下
被测试的班

Located in 罗马尼亚Harghita县的georgheni

成员自 2015年6月4日

作者简介

Botond是一名熟练的开发人员,他喜欢编写可读的代码. 13岁时,他在一台兼容ZX spectrum的俄罗斯个人电脑上学会了编程.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

以前在

Epam

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® community.