Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Thành thạo Rails Cache: Bí mật Increment & Decrement trong ActiveSupport

Rails là framework lớn với nhiều công cụ tích hợp sẵn cho tình huống cụ thể. Trong loạt bài này, chúng ta xem xét những công cụ ít người biết ẩn giấu trong codebase lớn của Rails.

Trong bài này, chúng ta giải thích phương thức increment và decrement trong Rails.cache.

Helper Rails.cache

Rails.cache là cổng vào để tương tác với cache trong ứng dụng. Nó cũng là abstraction, cung cấp API chung để gọi bất kể "store" cache thực tế đang dùng. Mặc định, Rails hỗ trợ:

  • FileStore
  • MemoryStore
  • MemCacheStore
  • NullStore
  • RedisCacheStore

Kiểm tra Rails.cache cho thấy store nào đang chạy:

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

Chúng ta không xem chi tiết tất cả, nhưng tóm tắt nhanh:

  • NullStore không lưu gì; đọc lại luôn trả nil. Mặc định cho app Rails mới.
  • FileStore lưu cache dạng file trên ổ cứng, tồn tại ngay cả khi restart Rails server.
  • MemoryStore giữ cache trong RAM, nên dừng Rails server cũng xóa cache.
  • MemCacheStore và RedisCacheStore dùng chương trình bên ngoài (MemCache và Redis) để duy trì cache.

Với nhiều lý do, ba cái đầu thường dùng cho development/testing. Production thường dùng Redis hoặc MemCache.

Vì Rails.cache trừu tượng hóa sự khác biệt giữa các service, dễ dàng chạy store khác nhau ở môi trường khác nhau mà không đổi code; ví dụ: NullStore development, MemoryStore testing, RedisCacheStore production.

Dữ liệu Cache

Quá Rails.cache.read, Rails.cache.write, và Rails.cache.fetch, ta có cách dễ dàng lưu và lấy bất kỳ dữ liệu tùy ý từ cache. Bài trước đi sâu hơn; quan trọng cho bài này là không có thread-safety built-in trên các method này. Giả sử cập nhật cached data từ nhiều thread để giữ running count; cần wrap read/write trong lock để tránh race condition. Xem ví dụ, giả sử setup dùng Redis cache store:

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 có 4 thread, mỗi thread increment cached value 100 lần. Kết quả nên là 400, nhưng hầu như lúc nào cũng ít hơn nhiều - 269 trên test run. Điều xảy ra là race condition. Tôi đề cập chi tiết bài trước, nhưng tóm tắt: vì các thread đều hoạt động trên cùng dữ liệu "shared", chúng có thể out-of-sync. Ví dụ: thread A đọc giá trị, thread B tiếp quản đọc giá trị, increment, lưu. Thread A tiếp tục, dùng giá trị cũ.

Cách phổ biến giải quyết là bao code bằng mutually exclusive lock (Mutex) để chỉ một thread thực thi code trong lock tại một thời điểm. Trong trường hợp chúng ta, Rails.cache có method xử lý kịch bản này.

Rails Cache Increment và Decrement

Object Rails.cache có cả method increment và decrement tác động trực tiếp lên cached data như kịch bản counter:

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)
      # lặp increment để nhấn mạnh 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)

Để dùng increment và decrement, phải báo cho cache store là giá trị 'raw' (qua raw: true). Phải làm khi đọc giá trị ngược lại sẽ lỗi. Cơ bản, báo cho cache lưu giá trị dạng bare integer để gọi increment/decrement, nhưng vẫn dùng expires_in và flag cache khác đồng thời.

Điểm then chốt: increment và decrement dùng atomic operations (ít nhất cho Redis và MemCache), nghĩa là thread-safe; không cách nào thread pause trong atomic operation.

Có giá trị lưu ý, dù không dùng trong ví dụ, cả hai method cũng trả về giá trị mới. Do đó, nếu cần dùng giá trị counter mới ngoài việc update, có thể làm mà không cần thêm read.

Ứng dụng thực tế

Bề ngoài, các method increment và decrement giống helper low-level, loại bạn chỉ quan tâm nếu implement hoặc maintain gem xử lý background job. Khi biết chúng, bạn có thể ngạc nhiên nơi chúng hữu ích.

Tôi dùng trong production app tránh duplicate scheduled background jobs chạy đồng thời. Trường hợp chúng ta, có các scheduled job update search indices, mark abandoned carts, v.v. Thông thường ổn; vấn đề là một số job (search indices đặc biệt) tiêu tốn nhiều memory - đủ để nếu hai chạy cùng lúc, vượt limit Heroku dyno, worker bị kill. Vì có vài job kiểu này, không đơn giản mark no-retries hoặc force unique jobs; hai job khác nhau (và do đó unique) có thể chạy cùng lúc và kill worker.

Để ngăn chặn, tạo base class cho scheduled jobs giữ counter số job đang chạy. Nếu count quá cao, job re-enqueue và chờ.

Ví dụ khác side-project nơi background job (hoặc nhiều job) xử lý khi user chờ. Nảy sinh vấn đề phổ biến: communicate tiến độ cho user của background job. Có nhiều cách giải quyết, nhưng thử dùng Rails.cache.increment update counter globally available. Cấu trúc:

  1. Thêm class mới trong /app/models abstract việc counter sống trong cache. Nơi mọi truy cập giá trị đi qua. Phần sinh unique cache key liên quan job.
  2. Job tạo instance model này và update khi item processed.
  3. JSON endpoint đơn giản tạo instance model lấy giá trị hiện tại.
  4. Front-end poll endpoint vài giây cập nhật UI. Có thể làm fancy với ActionCable push updates.

Kết luận

Thực lòng, Rails.cache.increment không phải tool thường dùng, vì không thường muốn update data trong cache (theo bản chất, tạm thời). Như lưu ý, lúc dùng thường liên quan background jobs vì jobs đã lưu data trong Redis (ít nhất trong app tôi làm) và cũng tạm thời. Trong trường hợp đó, lưu data liên quan (phần trăm hoàn thành) cùng nơi với mức persistence ngắn hạn tương tự là tự nhiên.

Với mọi thứ "off the beaten path", nên cẩn thận đưa vào codebase. Khuyên thêm comment giải thích cho developer tương lai tại sao dùng method này mà họ có thể không quen.