我正在开发一个成熟的 Rails 应用程序来加速我们的主索引页面,该页面的渲染时间为 5-6 秒。我最近注意到一些 N+1 没有被 Bullet 拾取,因此我在查询上实现了新的
strict_loading
方法,并相应地包含了关联。它现在做了相当多的急切加载:
[{ thumb_image: [:image_votes, :license, :projects, :user] }
:location, :name,
{ titles: :votes },
:projects, :logs, :user]
这对页面加载时间有所帮助。但最大的改进是当我开始在每个对象的部分上实现片段缓存时。这将加载时间缩短至 3-4 秒。像这样的东西:
Views: 62.4ms | ActiveRecord: 3356.4ms | Allocations: 592258
但是页面还是很慢。被查询的对象不仅关联性很强,而且对象也会不断地添加或更新。而且用户访问频率很高。
因此,即使大多数访问者获得 90% 的缓存命中率,我们也无法缓存索引查询以保持页面新鲜。现在我们遇到的情况是,查询时间占页面平均渲染时间的 95% 以上。这是因为它会急切加载所有这些对象和关联,即使它不需要其中的 90%。
所以我正在重新考虑急切加载。我想象更好的是先进行一个轻查询来获取最新的对象 ID,然后检查每个对象部分缓存键上的
fragment_exist?
。然后,无论缺少什么,batch加载缓存尚不存在的所有对象的关联。像这样的东西:
objects = Object.where(id: ids.reject { |id| fragment_exist?(cache_key_for_obj_id) }).
includes(blah blah blah)
问题是缓存键。我已经查看了有关此问题的旧问答,但他们假设手动缓存键,手动无效。使用 Rails 5 中缓存片段键的“较新”自动生成的摘要(我使用 Rails 7):
skip_digest
在生成缓存密钥时,似乎我必须手动使缓存过期 - 不,谢谢,这看起来太脆弱了这里要走什么路?我可以以某种方式从控制器检查
fragment_exist?
,包括正确的摘要,还是应该恢复到延迟加载关联?
如果延迟加载,我是否应该调用一个方法来从对象部分模板本身预先加载它们(每个对象)?这不仅没有将它们批处理在一起,而且似乎严重违反了模板和控制器之间的关注点。但部分模板似乎是唯一单独处理每个对象的地方。
正如一位之前的提问者所说,“片段缓存和急切加载似乎有些矛盾”。
有一个中间地带称为批量加载。预加载仅在缓存失效时发生,但它有点像设置:
https://github.com/exAspArk/batch-loader
$ bundle add batch-loader
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
def user_lazy
# i'm not even gonna try to explain this, you'll have to really read the readme
BatchLoader.for(user_id).batch do |user_ids, loader|
User.where(id: user_ids).each { |user| loader.call(user.id, user) }
end
end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# this one is for example purposes, there is a middleware for this
BatchLoader::Executor.clear_current
@posts = Post.all
# no db query is executed yet
@users = @posts.each_with_object({}) do |post, h|
h[post] = post.user_lazy
end
end
end
# app/views/posts/index.html.erb
# collection cache also works, this is just to see it easier in the log
<% @posts.each do |post| %>
<% cache post do %>
<%= render post %>
<% end %>
<% end %>
# app/views/posts/_post.html.erb
<%= tag.div id: dom_id(post) do %>
# when any of the `cache post` is invalidated, this line will execute
# and eager load all of the users. when all posts are cached there is
# no eager loading of users
<%= @users[post] %>
<% end %>
第一次加载时,帖子会被加载,用户会立即加载并且帖子会被缓存,第二次访问时只需要帖子:
Processing by PostsController#index as HTML
Post Load (0.1ms) SELECT "posts".* FROM "posts"
↳ app/controllers/posts_controller.rb:10:in `each_with_object'
Rendering layout layouts/application.html.erb
Rendering posts/index.html.erb within layouts/application
Read fragment views/posts/index:54ef1a9698061f894cb00c459440f8a6/posts/2-20240122190140985467 (0.1ms)
Read fragment views/posts/index:54ef1a9698061f894cb00c459440f8a6/posts/3-20240122185810608401 (0.1ms)
Read fragment views/posts/index:54ef1a9698061f894cb00c459440f8a6/posts/4-20240122185810608401 (0.1ms)
Read fragment views/posts/index:54ef1a9698061f894cb00c459440f8a6/posts/5-20240122185810608401 (0.1ms)
如果一个缓存失效
Post.first.touch
,用户会再次急切加载:
Processing by PostsController#index as HTML
Post Load (0.1ms) SELECT "posts".* FROM "posts"
↳ app/controllers/posts_controller.rb:10:in `each_with_object'
Rendering layout layouts/application.html.erb
Rendering posts/index.html.erb within layouts/application
Read fragment views/posts/index:1687a2170041b25d00fcb7bd15638f9e/posts/2-20240122192532103648 (0.2ms)
User Load (0.2ms) SELECT "users".* FROM "users" WHERE ("users"."id" IN (?, ?, ?) OR "users"."id" IS NULL) [["id", 17], ["id", 19], ["id", 55]]
↳ app/models/post.rb:6:in `block in user_lazy'
Rendered posts/_post.html.erb (Duration: 2.7ms | Allocations: 2761)
Write fragment views/posts/index:1687a2170041b25d00fcb7bd15638f9e/posts/2-20240122192532103648 (0.2ms)
Read fragment views/posts/index:1687a2170041b25d00fcb7bd15638f9e/posts/3-20240122185810608401 (0.1ms)
Read fragment views/posts/index:1687a2170041b25d00fcb7bd15638f9e/posts/4-20240122185810608401 (0.1ms)
Read fragment views/posts/index:1687a2170041b25d00fcb7bd15638f9e/posts/5-20240122185810608401 (0.1ms)