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

Rails Hidden Gems:ActiveSupport Cache Tăng và giảm

Rails là một framework lớn với rất nhiều công cụ tiện dụng được tích hợp sẵn cho các tình huống cụ thể. Trong loạt bài này, chúng ta sẽ xem xét một số công cụ ít được biết đến ẩn trong cơ sở mã lớn của Rails.

Trong bài viết này, chúng tôi sẽ giải thích incrementdecrement các phương thức trong Rails.cache .

Trình trợ giúp Rails.cache

Rails.cache là lối vào để tương tác với bộ đệm trong ứng dụng của bạn. Nó cũng là một sự trừu tượng hóa, cung cấp cho bạn một API chung để gọi bất kể "kho lưu trữ" bộ nhớ cache thực sự đang được sử dụng dưới mui xe. Ngoài ra, Rails hỗ trợ những điều sau:

  • FileStore
  • MemoryStore
  • MemCacheStore
  • NullStore
  • RedisCacheStore

Kiểm tra Rails.cache sẽ hiển thị bạn đang chạy cái nào:

> Rails.cache
=> <#ActiveSupport::Cache::RedisCacheStore options={:namespace=>nil, ...

Chúng tôi sẽ không xem xét tất cả chúng một cách chi tiết mà chỉ là một bản tóm tắt nhanh:

  • NullStore không lưu trữ bất kỳ thứ gì; đọc lại từ này sẽ luôn trả về nil . Đây là cài đặt mặc định cho một ứng dụng Rails mới.
  • FileStore lưu trữ bộ nhớ cache dưới dạng các tệp trên ổ cứng, vì vậy chúng vẫn tồn tại ngay cả khi bạn khởi động lại máy chủ Rails của mình.
  • MemoryStore giữ bộ nhớ đệm trong RAM, vì vậy nếu bạn dừng máy chủ Rails, bạn cũng sẽ xóa bộ nhớ cache.
  • MemCacheStore và RedisCacheStore sử dụng các chương trình bên ngoài (tương ứng là MemCache và Redis) để duy trì bộ nhớ cache.

Vì nhiều lý do khác nhau, ba phần đầu tiên ở đây được sử dụng thường xuyên nhất để phát triển / thử nghiệm. Trong quá trình sản xuất, bạn có thể sẽ sử dụng Redis hoặc MemCache.

Bởi vì Rails.cache Tóm tắt sự khác biệt giữa các dịch vụ này, dễ dàng chạy các phiên bản khác nhau trong các môi trường khác nhau mà không cần thay đổi mã; ví dụ:bạn có thể sử dụng NullStore đang phát triển, MemoryStore trong thử nghiệm và RedisCacheStore trong sản xuất.

Dữ liệu được lưu trong bộ nhớ cache

Thông qua Rails 'Rails.cache.read , Rails.cache.writeRails.cache.fetch , chúng tôi có một cách dễ dàng để lưu trữ và truy xuất bất kỳ dữ liệu tùy ý nào từ bộ nhớ đệm. Một bài báo trước đó đã đề cập chi tiết hơn về những điều này; điều quan trọng cần lưu ý đối với bài viết này là không có sự an toàn luồng tích hợp trên các phương pháp này. Giả sử chúng tôi đang cập nhật dữ liệu đã lưu trong bộ nhớ đệm từ nhiều chuỗi để duy trì số lượng đang hoạt động; chúng ta sẽ cần phải bọc hoạt động đọc / ghi trong một số loại khóa để tránh các điều kiện đua. Hãy xem xét ví dụ này, giả sử chúng ta đã thiết lập mọi thứ để sử dụng kho lưu trữ bộ nhớ cache của Redis:

threads = []
# Set initial counter
Rails.cache.write(:test_counter, 0)

4.times do
  threads << Thread.new do
    100.times do 
      current_count = Rails.cache.read(:test_counter)
      current_count += 1
      Rails.cache.write(:test_counter, current_count)
    end
  end
end

threads.map(&:join)

puts Rails.cache.read(:test_counter)

Ở đây chúng ta có bốn luồng, mỗi luồng tăng giá trị được lưu trong bộ nhớ cache của chúng ta một trăm lần. Kết quả phải là 400, nhưng hầu hết thời gian, nó sẽ ít hơn nhiều - 269 trong lần chạy thử nghiệm của tôi. Những gì đang xảy ra ở đây là một điều kiện chủng tộc. Tôi đã trình bày chi tiết hơn về những vấn đề này trong bài viết trước, nhưng như một bản tóm tắt nhanh, vì các luồng đều hoạt động trên cùng một dữ liệu "được chia sẻ", chúng có thể không đồng bộ với nhau. Ví dụ:một luồng có thể đọc giá trị, sau đó một luồng khác tiếp nhận và đọc giá trị, tăng và lưu trữ giá trị đó. Sau đó, chuỗi đầu tiên tiếp tục lại, sử dụng giá trị hiện đã lỗi thời của nó.

Cách phổ biến để giải quyết vấn đề này là bao quanh mã bằng một khóa loại trừ lẫn nhau (hoặc Mutex) để chỉ một luồng có thể thực thi mã trong khóa tại một thời điểm. Tuy nhiên, trong trường hợp của chúng tôi, Rails.cache có một số phương pháp để xử lý tình huống này.

Tăng và giảm bộ nhớ cache của Rails

Rails.cache đối tượng có cả incrementdecrement các phương pháp để hành động trực tiếp trên dữ liệu đã lưu trong bộ nhớ cache như kịch bản truy cập của chúng tôi:

  threads = []
  # Set initial counter
  Rails.cache.write(:test_counter, 0, raw: true)

  4.times do
    threads << Thread.new do
      100.times do 
        Rails.cache.increment(:test_counter)
        # repeating the increment just to highlight the thread safety
        Rails.cache.decrement(:test_counter)
        Rails.cache.increment(:test_counter)
      end
    end
  end

  threads.map(&:join)

  puts Rails.cache.read(:test_counter, raw: true)

Để sử dụng incrementdecrement , chúng tôi phải cho bộ nhớ cache lưu trữ đó là một giá trị 'thô' (thông qua raw: true ). Bạn cũng phải làm điều này khi đọc lại giá trị; nếu không, bạn sẽ gặp lỗi. Về cơ bản, chúng tôi đang nói với bộ đệm rằng chúng tôi muốn giá trị này được lưu trữ dưới dạng số nguyên trống để chúng tôi có thể gọi tăng / giảm trên đó, nhưng bạn vẫn có thể sử dụng expires_in và các cờ bộ nhớ cache khác cùng một lúc.

Chìa khóa ở đây là incrementdecrement sử dụng các phép toán nguyên tử (ít nhất là cho Redis và MemCache), nghĩa là chúng an toàn cho luồng; không có cách nào để một chuỗi tạm dừng trong một hoạt động nguyên tử.

Điều đáng chú ý là, mặc dù tôi chưa sử dụng nó trong ví dụ của mình ở đây, nhưng cả hai phương thức đều trả về giá trị mới. Do đó, nếu bạn cần sử dụng giá trị bộ đếm mới ngoài việc cập nhật nó, bạn cũng có thể làm như vậy mà không cần thêm read cuộc gọi.

Các ứng dụng trong thế giới thực

Về mặt của nó, những increment này và decrement các phương thức có vẻ giống như các phương thức trợ giúp cấp thấp, loại mà bạn có thể chỉ quan tâm nếu bạn đang triển khai hoặc duy trì một cái gì đó như một viên ngọc xử lý công việc nền. Tuy nhiên, khi bạn đã biết về chúng, bạn có thể ngạc nhiên vì chúng có thể hữu ích ở đâu.

Tôi đã sử dụng chúng trong một ứng dụng sản xuất để tránh trùng lặp các công việc nền đã lên lịch chạy đồng thời. Trong trường hợp của chúng tôi, chúng tôi có nhiều công việc đã được lên lịch để cập nhật chỉ số tìm kiếm, đánh dấu các xe hàng bị bỏ rơi, v.v. Nói chung, điều này hoạt động tốt; vấn đề là một số công việc (đặc biệt là chỉ số tìm kiếm) tiêu tốn rất nhiều bộ nhớ - đủ đến mức, nếu hai công việc chạy cùng nhau, nó sẽ vượt quá giới hạn Heroku dyno của chúng tôi và nhân viên sẽ bị giết. Bởi vì chúng tôi có một số công việc trong số này, nó không đơn giản như đánh dấu chúng không cần thử lại hoặc bắt buộc các công việc duy nhất; hai công việc khác nhau (và do đó là duy nhất) có thể cố gắng chạy cùng một lúc và làm hỏng nhân viên.

Để ngăn chặn điều này, chúng tôi đã tạo một lớp cơ sở cho các công việc đã lên lịch để lưu giữ một bộ đếm số lượng hiện đang chạy. Nếu số lượng quá cao, công việc chỉ cần tự xếp hàng lại và đợi.

Một ví dụ khác là trong một dự án phụ của tôi, nơi một công việc nền (hoặc nhiều công việc) thực hiện một số xử lý trong khi người dùng phải đợi. Điều này dẫn đến vấn đề phổ biến là thông báo tiến trình hiện tại cho người dùng của công việc nền. Mặc dù có nhiều cách để giải quyết vấn đề này, nhưng để thử nghiệm, tôi đã thử sử dụng Rails.cache.increment để cập nhật một bộ đếm có sẵn trên toàn cầu. Cấu trúc như sau:

  1. Đầu tiên, tôi đã thêm một lớp mới trong /app/models để loại bỏ thực tế rằng bộ đếm nằm trong bộ nhớ cache. Đây là nơi mà tất cả quyền truy cập vào giá trị sẽ chảy qua. Một phần của việc này là tạo khóa bộ nhớ cache duy nhất liên quan đến công việc.
  2. Sau đó, công việc tạo ra một phiên bản của mô hình này và cập nhật nó khi các mục được xử lý.
  3. Một điểm cuối JSON đơn giản tạo một phiên bản của mô hình này để lấy giá trị hiện tại.
  4. Giao diện người dùng sẽ thăm dò điểm cuối này sau mỗi vài giây để cập nhật giao diện người dùng. Tất nhiên, bạn có thể làm cho bộ phim này trở nên độc đáo hơn với một cái gì đó như ActionCable và tung ra các bản cập nhật.

Kết luận

Thành thật mà nói, Rails.cache.increment không phải là công cụ mà tôi muốn sử dụng thường xuyên, vì tôi không thường xuyên muốn cập nhật dữ liệu được lưu trữ trong bộ nhớ cache (về bản chất, có phần tạm thời). Như đã lưu ý ở trên, thời gian tôi đạt được nó thường liên quan đến các công việc nền vì các công việc đã lưu trữ dữ liệu của chúng trong Redis (ít nhất là trong hầu hết các ứng dụng tôi đã làm việc) và nói chung cũng là tạm thời. Trong những trường hợp như vậy, việc lưu trữ dữ liệu liên quan (chẳng hạn như tỷ lệ phần trăm hoàn thành) ở cùng một nơi với mức độ ổn định ngắn hạn tương tự dường như là điều hiển nhiên.

Như với tất cả những thứ "đi tắt đón đầu", bạn nên cảnh giác với việc đưa những thứ như thế này vào cơ sở mã của mình. Ít nhất tôi cũng khuyên bạn nên thêm một số nhận xét để giải thích cho các nhà phát triển tương lai tại sao bạn lại sử dụng phương pháp này mà họ có thể không quen thuộc.