Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rails Hidden Gems:ActiveSupportキャッシュのインクリメントとデクリメント

Railsは、特定の状況に対応する多くの便利なツールが組み込まれた大規模なフレームワークです。このシリーズでは、Railsの大規模なコードベースに隠されているあまり知られていないツールのいくつかを見ていきます。

この記事では、incrementについて説明します。 およびdecrement Rails.cacheのメソッド 。

Rails.cacheヘルパー

Rails.cache アプリケーションのキャッシュと対話するための入り口です。これは抽象化でもあり、内部で使用されている実際のキャッシュ「ストア」に関係なく呼び出すための共通のAPIを提供します。箱から出して、Railsは以下をサポートします:

  • ファイルストア
  • MemoryStore
  • MemCacheStore
  • NullStore
  • RedisCacheStore

Rails.cacheを検査しています 実行しているものが表示されます:

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

それらすべてを詳細に検討することはしませんが、簡単な要約として:

  • NullStoreは何も保存しません。これから読み返すと、常にnilが返されます 。これは、新しいRailsアプリのデフォルトです。
  • FileStoreはキャッシュをファイルとしてハードドライブに保存するため、Railsサーバーを再起動してもキャッシュは保持されます。
  • MemoryStoreはキャッシュをRAMに保持するため、Railsサーバーを停止すると、キャッシュも消去されます。
  • MemCacheStoreとRedisCacheStoreは、外部プログラム(それぞれ、MemCacheとRedis)を使用してキャッシュを維持します。

さまざまな理由から、ここでの最初の3つは、開発/テストに最も頻繁に使用されます。本番環境では、RedisまたはMemCacheを使用する可能性があります。

Rails.cache これらのサービス間の違いを抽象化するため、コードを変更せずに、さまざまな環境でさまざまなバージョンを簡単に実行できます。たとえば、NullStoreを使用できます 開発中、MemoryStore テスト中、およびRedisCacheStore 生産中です。

キャッシュデータ

RailsのRails.cache.readを介して 、Rails.cache.write 、およびRails.cache.fetch 、キャッシュから任意のデータを保存および取得する簡単な方法があります。以前の記事でこれらについて詳しく説明しました。この記事で注意すべき重要なことは、これらのメソッドには組み込みのスレッドセーフがないということです。実行カウントを維持するために、複数のスレッドからキャッシュされたデータを更新するとします。競合状態を回避するために、読み取り/書き込み操作をある種のロックでラップする必要があります。この例を考えてみましょう。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)

ここには4つのスレッドがあり、それぞれがキャッシュされた値を100回インクリメントします。結果は400になるはずですが、ほとんどの場合、テスト実行では269とはるかに少なくなります。ここで起こっているのは競合状態です。これらについては前の記事で詳しく説明しましたが、簡単にまとめると、スレッドはすべて同じ「共有」データで動作しているため、互いに同期しなくなる可能性があります。たとえば、あるスレッドが値を読み取り、次に別のスレッドが値を引き継いで読み取り、それをインクリメントして保存する場合があります。その後、最初のスレッドは、現在は古い値を使用して再開します。

この問題を解決する一般的な方法は、コードを相互に排他的なロック(またはMutex)で囲み、一度に1つのスレッドのみがロック内でコードを実行できるようにすることです。ただし、この場合、Rails.cacheにはこのシナリオを処理するためのメソッドがいくつかあります。

Railsキャッシュのインクリメントとデクリメント

Rails.cache オブジェクトには両方のincrementがあります およびdecrement カウンターシナリオのように、キャッシュされたデータに直接作用する方法:

  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)

incrementを使用するには およびdecrement 、キャッシュストアに「raw」値であることを通知する必要があります(raw: true経由) )。値を読み戻すときにもこれを行う必要があります。そうしないと、エラーが発生します。基本的に、インクリメント/デクリメントを呼び出すことができるように、この値を裸の整数として格納するようにキャッシュに指示していますが、expires_inは引き続き使用できます。 と他のキャッシュフラグを同時に。

ここで重要なのは、incrementです。 およびdecrement アトミック操作(少なくともRedisとMemCacheの場合)を使用します。これは、スレッドセーフであることを意味します。アトミック操作中にスレッドを一時停止する方法はありません。

ここでの例では使用していませんが、どちらのメソッドも新しい値を返すことに注意してください。したがって、更新するだけでなく新しいカウンター値を使用する必要がある場合は、追加のreadなしで使用することもできます。 電話してください。

実世界のアプリケーション

表面的には、これらのincrement およびdecrement メソッドは低レベルのヘルパーメソッドのように見えます。これは、バックグラウンドのジョブ処理gemのようなものを実装または保守している場合にのみ気になる種類です。ただし、それらについて知ってしまえば、どこで役立つのか驚くかもしれません。

これらを本番アプリケーションで使用して、スケジュールされたバックグラウンドジョブの重複が同時に実行されないようにしました。私たちの場合、検索インデックスの更新、放棄されたカートのマーク付けなど、さまざまなスケジュールされたジョブがあります。通常、これは正常に機能します。キャッチは、一部のジョブ(特に検索インデックス)が大量のメモリを消費することです。2つを一緒に実行すると、Heroku dynoの制限を超え、ワーカーが強制終了されます。これらのジョブがいくつかあるため、再試行なしのマークを付けたり、固有のジョブを強制したりするほど簡単ではありません。 2つの異なる(したがって一意の)ジョブを同時に実行して、ワーカーをダウンさせようとする可能性があります。

これを防ぐために、現在実行中の数のカウンターを保持するスケジュールされたジョブの基本クラスを作成しました。カウントが多すぎる場合、ジョブは単に自分自身を再エンキューして待機します。

別の例は、ユーザーが待たなければならない間にバックグラウンドジョブ(または複数のジョブ)が何らかの処理を行う私のサイドプロジェクトでした。これにより、現在の進捗状況をバックグラウンドジョブのユーザーに伝えるという一般的な問題が発生します。これを解決する方法はたくさんありますが、実験として、Rails.cache.incrementを使用してみました グローバルに利用可能なカウンターを更新します。構造は次のとおりです。

  1. 最初に、/app/modelsに新しいクラスを追加しました カウンターがキャッシュに存在するという事実を抽象化するため。これは、値へのすべてのアクセスが流れる場所です。この一部は、ジョブに関連する一意のキャッシュキーを生成することです。
  2. 次に、ジョブはこのモデルのインスタンスを作成し、アイテムが処理されるときにそれを更新します。
  3. 単純なJSONエンドポイントは、現在の値を取得するためにこのモデルのインスタンスを作成します。
  4. フロントエンドは、UIを更新するために、このエンドポイントを数秒ごとにポーリングします。もちろん、ActionCableのようなものでこのファンシーを作り、アップデートをプッシュすることもできます。
結論

正直なところ、Rails.cache.increment キャッシュに保存されているデータを更新することはめったにないので、これは私が頻繁に使用するツールではありません(これは本質的に一時的なものです)。上記のように、ジョブはすでにRedisにデータを保存しており(少なくとも私が取り組んだほとんどのアプリでは)、一般的に一時的なものであるため、私が到達する時間は一般にバックグラウンドジョブに関連しています。このような場合、関連するデータ(たとえば、完了率)を同じ場所に同じレベルの短期間の永続性で保存するのが自然なようです。

「殴られた道から外れた」すべてのものと同様に、このようなものをコードベースに導入することには注意する必要があります。少なくとも、将来の開発者に、おそらく慣れていないこの方法を使用している理由を説明するコメントを追加することをお勧めします。


  1. RailsとCarrierwaveを使用したアップロード

    これは、「Railsによるアップロード」シリーズの別の記事です。今日は、Railsで最も人気のあるファイルアップロードソリューションの1つであるCarrierwaveに会います。 Carrierwaveが好きなのは、使い始めが簡単で、すぐに使える機能がたくさんあり、コミュニティのメンバーが書いた「ハウツー」記事がたくさんあるので、迷子にならないからです。 この記事では、次の方法を学習します。 CarrierwaveをRailsアプリに統合する 検証を追加する リクエスト間でファイルを永続化する ファイルを削除する サムネイルを生成する 離れた場所からファイルをアップロードする 複数のファイ

  2. 知っておくべきWindows 11のヒントと隠された宝石

    Windows 11 は、互換性のあるデバイスの無料アップグレードとして利用でき、多くの新機能と改善が含まれています。新しく再設計されたスタート メニュー タスクバー、Andriod アプリをサポートする改良された Microsoft ストア、統合された Microsoft チーム、スナップ レイアウト、ウィジェットなどがあります。しかし、レドモンドの巨人によって公式に発表された大きなニュースと機能に加えて、Windows 11 には多くの小さな変更が含まれており、一見すると明らかではないかもしれません。この投稿では、Windows 11 の非表示の機能をいくつか紹介しました。 について知って