Computer >> Máy Tính >  >> Lập trình >> Ruby

Cách ActiveRecord sử dụng bộ nhớ đệm để tránh các chuyến đi không cần thiết đến cơ sở dữ liệu

Một cách chung để mô tả bộ nhớ đệm là lưu trữ kết quả của một số mã để chúng ta có thể nhanh chóng truy xuất nó sau này. Trong một số trường hợp, điều này có nghĩa là lưu trữ một giá trị đã tính toán để tránh phải tính toán lại sau này. Tuy nhiên, chúng tôi cũng có thể lưu dữ liệu vào bộ nhớ cache chỉ bằng cách giữ nó trong bộ nhớ mà không cần thực hiện bất kỳ tính toán nào, để tránh phải đọc từ ổ cứng hoặc thực hiện yêu cầu mạng.

Hình thức thứ hai này đặc biệt thích hợp cho ActiveRecord, nơi cơ sở dữ liệu thường chạy trên một máy chủ riêng biệt. Do đó, tất cả các yêu cầu đều phải chịu phí lưu lượng mạng, chưa kể đến tải được đặt trên máy chủ cơ sở dữ liệu khi truy vấn được thực hiện lại.

May mắn thay, đối với các nhà phát triển Rails, bản thân ActiveRecord đã xử lý rất nhiều việc này cho chúng tôi, có lẽ chúng tôi không hề hay biết về nó. Điều này rất tốt cho năng suất, nhưng đôi khi, điều quan trọng là phải biết những gì đang được lưu vào bộ nhớ cache phía sau hậu trường. Ví dụ:khi bạn biết (hoặc mong đợi) một giá trị đang bị thay đổi bởi một quy trình khác, hoặc bạn nhất thiết phải có giá trị cập nhật nhất. Trong những trường hợp như thế này, ActiveRecord cung cấp một số 'cửa thoát hiểm' để buộc đọc dữ liệu chưa được lưu trữ.

Đánh giá Lười biếng của ActiveRecord

Đánh giá lười biếng của ActiveRecord không phải là bộ nhớ đệm, nhưng chúng ta sẽ gặp nó trong các ví dụ mã sau này, vì vậy chúng tôi sẽ cung cấp một cái nhìn tổng quan ngắn gọn. Khi bạn tạo một truy vấn ActiveRecord, trong nhiều trường hợp, mã không đưa ra lệnh gọi ngay lập tức đến cơ sở dữ liệu. Đây là những gì cho phép chúng tôi chuỗi nhiều .where các mệnh đề mà không cần phải truy cập vào cơ sở dữ liệu mỗi lần:

@posts = Post.where(published: true)
# no DB hit yet
@posts = @posts.where(publied_at: Date.today)
# still nothing
@posts.count
# SELECT COUNT(*) FROM "posts" WHERE...

Có một số ngoại lệ cho điều này. Ví dụ:khi sử dụng .find , .find_by , .pluck , .to_a hoặc .first , không thể xâu chuỗi các mệnh đề bổ sung. Trong hầu hết các ví dụ dưới đây, tôi sẽ sử dụng .to_a như một cách đơn giản để buộc một cuộc gọi DB.

Lưu ý rằng nếu bạn đang thử nghiệm điều này trong bảng điều khiển Rails, bạn sẽ cần phải tắt chế độ 'tiếng vọng'. Nếu không, bảng điều khiển (irb hoặc pry) gọi .inspect trên đối tượng sau khi bạn nhấn 'enter', buộc phải truy vấn DB. Để tắt chế độ tiếng vọng, bạn có thể sử dụng mã sau:

conf.echo = false # for irb
pry_instance.config.print = proc {} # for pry

Mối quan hệ ActiveRecord

Phần đầu tiên của bộ nhớ đệm tích hợp của ActiveRecord mà chúng ta sẽ xem xét là các mối quan hệ. Ví dụ:chúng tôi có một User-Posts điển hình mối quan hệ:

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
end

Điều này cung cấp cho chúng tôi user.posts hữu ích và post.user phương pháp thực hiện truy vấn cơ sở dữ liệu để tìm (các) bản ghi liên quan. Giả sử chúng tôi đang sử dụng chúng trong một bộ điều khiển và chế độ xem:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @user = User.find(params[:user_id])
    @posts = @user.posts
  end
...

# app/views/posts/index.html.erb
...
<%= render 'shared/sidebar' %>
<% @posts.each do |post| %>
  <%= render post %>
<% end %>

# app/views/shared/_sidebar.html.erb
...
<% @posts.each do |post| %>
  <li><%= post.title %></li>
<% end %>

Chúng tôi có một index cơ bản hành động lấy @user.posts . Tương tự như phần trước, truy vấn cơ sở dữ liệu vẫn chưa được chạy tại thời điểm này. Sau đó, các đường ray hiển thị index của chúng tôi xem, đến lượt nó, hiển thị thanh bên. Thanh bên gọi @posts.each ... và tại thời điểm này, ActiveRecord kích hoạt truy vấn cơ sở dữ liệu để lấy dữ liệu.

Sau đó, chúng tôi quay lại phần còn lại của index của chúng tôi mẫu, nơi chúng tôi có khác @posts.each; tuy nhiên, lần này, không có lệnh gọi cơ sở dữ liệu. Điều đang xảy ra là ActiveRecord đang lưu vào bộ nhớ đệm tất cả các bài đăng này cho chúng tôi và không buồn cố gắng đọc lại từ cơ sở dữ liệu.

Escape Hatch

Đôi khi chúng ta có thể muốn buộc ActiveRecord tìm nạp lại các bản ghi được liên kết; có lẽ, chúng tôi biết nó đang được thay đổi bởi một quy trình khác (ví dụ:một công việc nền). Một tình huống phổ biến khác là trong các thử nghiệm tự động, nơi chúng tôi muốn nhận giá trị mới nhất trong cơ sở dữ liệu để xác thực rằng mã đã cập nhật chính xác.

Có hai cách phổ biến để làm điều này, tùy thuộc vào tình huống. Tôi nghĩ cách phổ biến nhất chỉ đơn giản là gọi .reload trên liên kết, điều này cho ActiveRecord biết rằng chúng tôi muốn bỏ qua bất kỳ thứ gì nó đã lưu trong bộ nhớ cache và tải phiên bản mới nhất từ ​​cơ sở dữ liệu:

@user = User.find(1)
@user.posts # DB Call
@user.posts # Cached, no DB call
@user.posts.reload # DB call
@user.posts # Cached new version, no DB call

Một tùy chọn khác là chỉ cần lấy một phiên bản mới của mô hình ActiveRecord (ví dụ:bằng cách gọi find một lần nữa):

@user = User.find(1)
@user.posts # DB Call
@user.posts # Cached, no DB call
@user = User.find(1) # @user is now a new instance of User
@user.posts # DB Call, no cache in this instance

Mối quan hệ vào bộ nhớ đệm là tốt, nhưng chúng ta thường kết thúc với .where(...) phức tạp truy vấn ngoài tra cứu mối quan hệ đơn giản. Đây là nơi chứa bộ nhớ cache SQL của ActiveRecord.

Bộ nhớ cache SQL của ActiveRecord

ActiveRecord giữ một bộ nhớ cache nội bộ của các truy vấn mà nó đã thực hiện để tăng tốc hiệu suất. Tuy nhiên, lưu ý rằng bộ nhớ cache này được gắn với hành động cụ thể; nó được tạo ra khi bắt đầu hành động và bị phá hủy khi kết thúc hành động. Điều này có nghĩa là bạn sẽ chỉ thấy điều này nếu bạn thực hiện cùng một truy vấn hai lần trong một hành động của bộ điều khiển. Nó cũng có nghĩa là bộ nhớ cache không được sử dụng trong bảng điều khiển Rails. Lần truy cập bộ nhớ cache được hiển thị trong nhật ký Rails với một CACHE . Ví dụ:

class PostsController < ApplicationController
  def index
    ...
    Post.all.to_a # to_a to force DB query

    ...
    Post.all.to_a # to_a to force DB query

tạo ra đầu ra nhật ký sau:

  Post Load (2.1ms)  SELECT "posts".* FROM "posts"
  ↳ app/controllers/posts_controller.rb:11:in `index'
  CACHE Post Load (0.0ms)  SELECT "posts".* FROM "posts"
  ↳ app/controllers/posts_controller.rb:13:in `index'

Bạn thực sự có thể xem qua những gì bên trong bộ đệm để thực hiện một hành động bằng cách in ra ActiveRecord::Base.connection.query_cache (hoặc ActiveRecord::Base.connection.query_cache.keys chỉ cho truy vấn SQL).

Escape Hatch

Có thể không có nhiều lý do khiến bạn cần phải bỏ qua SQL Cache, nhưng tuy nhiên, bạn có thể buộc ActiveRecord bỏ qua bộ đệm SQL của nó bằng cách sử dụng uncached phương thức trên ActiveRecord::Base :

class PostsController < ApplicationController
  def index
    ...
    Post.all.to_a # to_a to force DB query

    ...
    ActiveRecord::Base.uncached do
      Post.all.to_a # to_a to force DB query
    end

Vì nó là một phương thức trên ActiveRecord::Base , bạn cũng có thể gọi nó qua một trong các lớp mô hình của mình nếu nó cải thiện khả năng đọc; ví dụ,

  Post.uncached do
    Post.all.to_a
  end

Bộ nhớ cache của Bộ đếm

Khá phổ biến trong các ứng dụng web khi muốn đếm các bản ghi trong một mối quan hệ (ví dụ:một người dùng có X bài đăng hoặc một tài khoản nhóm có Y người dùng). Do mức độ phổ biến của nó, ActiveRecord bao gồm một cách để tự động cập nhật bộ đếm để bạn không có nhiều .count cuộc gọi sử dụng hết tài nguyên cơ sở dữ liệu. Chỉ mất một vài bước để kích hoạt nó. Đầu tiên, chúng tôi thêm counter_cache vào mối quan hệ để ActiveRecord biết để lưu vào bộ nhớ cache số lượng cho chúng tôi:

class Post < ApplicationRecord
  belongs_to :user, counter_cache: true
end

Chúng tôi cũng cần thêm một cột mới vào User , nơi số lượng sẽ được lưu trữ. Trong ví dụ của chúng tôi, đây sẽ là User.posts_count . Bạn có thể chuyển một ký hiệu vào counter_cache để chỉ định tên cột nếu cần.

rails generate migration AddPostsCountToUsers posts_count:integer
rails db:migrate

Bộ đếm bây giờ sẽ được đặt thành 0 (mặc định). Nếu ứng dụng của bạn đã có một số bài đăng, bạn cần cập nhật chúng. ActiveRecord cung cấp một reset_counters để xử lý các chi tiết nitty-gritty, vì vậy bạn chỉ cần chuyển ID của nó và cho nó biết bộ đếm nào cần cập nhật:

User.all.each do |user|
  User.reset_counters(user.id, :posts)
end

Cuối cùng, chúng ta phải kiểm tra những nơi mà số đếm này đang được sử dụng. Điều này là do gọi .count sẽ bỏ qua bộ đếm và sẽ luôn chạy COUNT() Truy vấn SQL. Thay vào đó, chúng ta có thể sử dụng .size , biết sử dụng bộ nhớ đệm bộ đếm nếu nó tồn tại. Ngoài ra, bạn có thể muốn đặt mặc định là sử dụng .size ở mọi nơi, vì nó cũng không tải lại các liên kết nếu chúng đã có mặt, có khả năng lưu một chuyến đi vào cơ sở dữ liệu.

Kết luận

Đối với hầu hết các phần, bộ nhớ đệm nội bộ của ActiveRecord "chỉ hoạt động". Tôi không thể nói rằng tôi đã gặp nhiều trường hợp cần phải vượt qua nó, nhưng như với tất cả mọi thứ, biết những gì đang diễn ra "bí mật" có thể giúp bạn tiết kiệm thời gian và sự đau đớn khi bạn rơi vào tình huống cần một cái gì đó. khác thường.

Tất nhiên, cơ sở dữ liệu không phải là nơi duy nhất mà Rails thực hiện một số bộ nhớ đệm hậu trường cho chúng ta. Đặc tả HTTP bao gồm các tiêu đề có thể được gửi giữa máy khách và máy chủ để tránh phải gửi lại dữ liệu không thay đổi. Trong bài viết tiếp theo của loạt bài về bộ nhớ đệm này, chúng ta sẽ xem xét 304 (Not Modified) Mã trạng thái HTTP, cách Rails xử lý nó cho bạn và cách bạn có thể điều chỉnh việc xử lý này.