2

Rails でのタイムライン クエリを高速化するためのキャッシングの適切な使用方法についてアドバイスを得たいと思っています。背景は次のとおりです。

Rails バックエンドで iPhone アプリを開発しています。これはソーシャル アプリであり、他のソーシャル アプリと同様に、その主なビューはメッセージのタイムライン (ニュースフィード) です。これは、タイムラインがユーザーとそのフォロワーのメッセージで構成される Twitter と非常によく似ています。タイムラインを取得するための API リクエストの主なクエリは次のとおりです。

@messages = Message.where("user_id in (?) OR user_id = ?", current_user.followed_users.map(&:id), current_user)

現在、このクエリは、特に大規模な場合に非常に非効率になるため、キャッシュを検討しています。やろうとしていることは以下の2点です。

1) Redis を使用してタイムラインをメッセージ ID のリストとしてキャッシュする

このクエリのコストが非常に高くなる理由の 1 つは、オンザフライで表示するメッセージを判断することです。ここでの私の計画は、各ユーザーのメッセージ ID の Redis リストを作成し続けることです。Timeline API リクエストが届いたときにこれを正しくビルドすると仮定すると、Redis を呼び出して、表示するメッセージの ID の前処理済みの順序付きリストを取得できます。たとえば、「[21, 18, 15, 14, 8, 5]」のような結果が得られる場合があります。

2) Memcached を使用して個々のメッセージ オブジェクトをキャッシュする

最初のポイントは大いに役立つと思いますが、データベースから個々のメッセージ オブジェクトを取得するという潜在的な問題がまだ残っています。メッセージ オブジェクトは非常に大きくなる可能性があります。それらを使用して、コメント、いいね、ユーザーなどの関連オブジェクトを返します。理想的には、これらの個々のメッセージ オブジェクトもキャッシュします。これは私が混乱しているところです。

キャッシュを使用しない場合は、次のようなクエリ呼び出しを実行してメッセージ オブジェクトを取得します。

@messages = Message.where("id in (?)", ids_from_redis)

次に、タイムラインを返します。

respond_with(:messages => @messages.as_json) # includes related likes, comments, user, etc.

ここで、Memcache を使用して個々のメッセージ オブジェクトを取得したいので、一度に 1 つずつメッセージを取得する必要があるようです。疑似コードを使用して、次のようなことを考えています。

ids_from_redis.each do |m|
  message = Rails.cache.fetch("message_#{m}") do
     Message.find(m).as_json
  end
  @messages << message
end

これが私の2つの具体的な質問です(長いビルドで申し訳ありません):

1) このアプローチは一般的に理にかなっていますか (リストの場合は redis、オブジェクトの場合は memcached)?

2)具体的には、以下の疑似コードでは、これがこれを行う唯一の方法ですか? メッセージを 1 つずつ取得するのは効率が悪いように感じますが、オブジェクト レベルのキャッシュを実行するつもりであるため、他にどのように実行すればよいかわかりません。

このようなことを試みるのはこれが初めてなので、フィードバックに感謝します。

4

1 に答える 1

2

一見すると、これは合理的に思えます。Redis は、リストなどの保存に適しています。永続化することもできます。memcached は、そのように順番に呼び出しても、個々のメッセージを非常に高速に取得できます。

ここでの問題は、メッセージが投稿されるたびにその redis キャッシュをクリア/補足する必要があることです。この状況でキャッシュをクリアするだけでは少し無駄に思えます。なぜなら、メッセージのすべての受信者を特定するという手間がすでにかかっているからです。

間違った質問に答えるつもりはありませんが、各メッセージが投稿されたときに、メッセージの可視性をデータベース (または redis) に「レンダリング」することについて考えたことはありますか? このようなもの:

class Message
  belongs_to :sender
  has_many   :visibilities

  before_create :render_visibility
    sender.followers.each do |follower|
      visibilities.build(:user => follower)
    end
  def 
end

次に、メッセージのリストを非常に簡単にレンダリングできます。

class User
  has_many :visibilities
  has_many :messages, :through => :visibilities
end

# in your timeline view:
<%= current_user.messages.each { |message| render message } %>

次に、次のような個々のメッセージを追加します。

# In your message partial, caching individual rendered messages:
<%= cache(message) do %>
  <!-- render your message here -->
<% end %>

次に、次のようにタイムライン全体のキャッシュも追加します。

# In your timeline view
<%= cache("timeline-for-#{current_user}-#{current_user.messages.last.cache_key}") do %>
  <%= current_user.messages.each { |message| render message } %>
<% end %>

これ達成できること (私はまだテストしていません) は、新しいメッセージが投稿されるまで、タイムラインの HTML 全体がキャッシュされることです。その場合、タイムラインは再レンダリングされますが、個々のメッセージはすべて再レンダリングされるのではなく、キャッシュから取得されます (ただし、他のユーザーが表示していない新しいメッセージは例外となる可能性があります!)。

これは、メッセージのレンダリングがすべてのユーザーで同じであることを前提としていることに注意してください。そうでない場合は、ユーザーごとにメッセージをキャッシュする必要もありますが、これは少し残念なことです。可能な場合はこれを行わないようにしてください。

FWIW、これは漠然と(そして漠然と意味しますが)Twitterが行うことだと思います。ただし、彼らはそれに対して「ビッグデータ」アプローチを採用しており、ツイートは爆発的に展開され、マシンの大規模なクラスター全体でフォロワーのタイムラインに挿入されます。ここで説明したことは、多くのフォロワーがいる書き込みの多い環境でスケーリングするのに苦労しますが、resque などを使用することで多少改善できます。

PS ここのコードは少し怠け者でした。これをリファクタリングして、たとえばタイムライン キャッシュ キーの生成をヘルパーや人物モデルに移動することを検討する必要があります。

于 2013-01-08T01:08:21.213 に答える