Dalli

RubyMemcachedキャッシュライブラリRailsパフォーマンス分散キャッシュ

GitHub概要

petergoldstein/dalli

High performance memcached client for Ruby

スター3,107
ウォッチ55
フォーク460
作成日:2010年8月17日
言語:Ruby
ライセンス:MIT License

トピックス

なし

スター履歴

petergoldstein/dalli Star History
データ取得日時: 2025/10/22 08:07

キャッシュライブラリ

Dalli

概要

DalliはRuby用の高性能なMemcachedクライアントライブラリで、バイナリプロトコルを使用して優れたパフォーマンスを提供します。

詳細

Dalli(ダリ)は、Peter GoldsteinとMike Perhamによって開発されたRuby向けの高性能Memcachedクライアントライブラリです。Memcached 1.4以降で導入された新しいバイナリプロトコルを使用し、従来のmemcache-clientの代替として設計されています。Pure Rubyで実装されており、デフォルトでスレッドセーフな動作を提供します。単一接続またはコネクションプールを使用した並行処理に対応し、マルチスレッド環境でのボトルネック解消が可能です。SASL認証をサポートしており、Herokuなどのマネージドサービス環境での使用に適しています。Rails 4以降では標準のmemcache storeがDalliを使用するように更新されており、Ruby on Railsエコシステムにおいて重要な位置を占めています。圧縮、フェイルオーバー、タイムアウト設定などの高度な機能を提供し、プロダクション環境での信頼性を重視した設計となっています。

メリット・デメリット

メリット

  • 高性能: バイナリプロトコルによる効率的な通信
  • スレッドセーフ: デフォルトでマルチスレッド対応
  • Rails統合: Rails 3/4との優れた統合とcache store対応
  • コネクションプール: 並行処理でのパフォーマンス最適化
  • SASL認証: Herokuなどマネージド環境でのセキュリティ
  • フェイルオーバー: 適切な障害復旧とタイムアウト調整
  • 圧縮サポート: 大きなデータの効率的な格納

デメリット

  • Memcached依存: Memcachedサーバーの設定・運用が必要
  • メモリ制限: Memcachedのメモリベースストレージ特性
  • データ永続性: サーバー再起動時のデータ消失リスク
  • ネットワーク依存: 分散環境でのネットワーク遅延影響
  • Ruby限定: Ruby以外の言語では使用不可

主要リンク

書き方の例

基本的な使用方法

require 'dalli'

# Memcachedサーバーに接続
dc = Dalli::Client.new('localhost:11211')

# データの保存
dc.set('user:1', { name: 'Alice', age: 30 })
dc.set('count', 100, 3600)  # 1時間のTTL

# データの取得
user = dc.get('user:1')
puts user['name']  # => "Alice"

count = dc.get('count')
puts count  # => 100

# データの削除
dc.delete('user:1')

# データの存在確認
exists = dc.get('count')
puts exists.nil?  # => false または true

複数サーバー構成

require 'dalli'

# 複数のMemcachedサーバーを指定
servers = ['192.168.1.10:11211', '192.168.1.11:11211', '192.168.1.12:11211']
dc = Dalli::Client.new(servers, {
  compression: true,
  expires_in: 3600,
  namespace: 'myapp'
})

# データの分散保存
dc.set('user:1', { name: 'Alice', email: '[email protected]' })
dc.set('user:2', { name: 'Bob', email: '[email protected]' })

# バッチ取得
users = dc.get_multi('user:1', 'user:2')
users.each do |key, value|
  puts "#{key}: #{value['name']}"
end

Rails での設定

# config/environments/production.rb
Rails.application.configure do
  # Dalliを使用したmemcache store設定
  config.cache_store = :mem_cache_store, 
                       'cache1.example.com:11211',
                       'cache2.example.com:11211',
                       {
                         namespace: 'myapp',
                         expires_in: 1.day,
                         compress: true,
                         compression_min_size: 1024
                       }
end

# アプリケーションでの使用
class User < ApplicationRecord
  def self.find_cached(id)
    Rails.cache.fetch("user:#{id}", expires_in: 1.hour) do
      find(id)
    end
  end
  
  def cached_posts
    Rails.cache.fetch("user:#{id}:posts", expires_in: 30.minutes) do
      posts.includes(:comments).to_a
    end
  end
end

# 使用例
user = User.find_cached(1)
posts = user.cached_posts

コネクションプールの設定

require 'dalli'
require 'connection_pool'

# コネクションプールを使用
CACHE_POOL = ConnectionPool.new(size: 5, timeout: 5) do
  Dalli::Client.new('localhost:11211', {
    socket_timeout: 0.5,
    socket_max_failures: 2,
    keepalive: true
  })
end

class UserCache
  def self.get_user(user_id)
    CACHE_POOL.with do |cache|
      cache.get("user:#{user_id}")
    end
  end
  
  def self.set_user(user_id, user_data)
    CACHE_POOL.with do |cache|
      cache.set("user:#{user_id}", user_data, 3600)
    end
  end
  
  def self.delete_user(user_id)
    CACHE_POOL.with do |cache|
      cache.delete("user:#{user_id}")
    end
  end
end

# 使用例
user_data = UserCache.get_user(123)
UserCache.set_user(123, { name: 'Charlie', role: 'admin' })

高度な操作とエラーハンドリング

require 'dalli'

class CacheManager
  def initialize(servers, options = {})
    @client = Dalli::Client.new(servers, options)
  end
  
  def increment_counter(key, amount = 1, initial = 0)
    # カウンターのインクリメント(存在しない場合は初期値設定)
    @client.incr(key, amount) || begin
      @client.set(key, initial)
      initial
    end
  end
  
  def decrement_counter(key, amount = 1)
    # カウンターのデクリメント
    @client.decr(key, amount)
  end
  
  def fetch_with_fallback(key, fallback_proc, ttl = 3600)
    # キャッシュから取得、存在しない場合はfallback実行
    cached_value = @client.get(key)
    return cached_value unless cached_value.nil?
    
    begin
      fresh_value = fallback_proc.call
      @client.set(key, fresh_value, ttl)
      fresh_value
    rescue => e
      Rails.logger.error "Cache fallback failed for key #{key}: #{e.message}"
      nil
    end
  end
  
  def atomic_update(key, ttl = 3600)
    # Compare and Swap (CAS) を使用した原子的更新
    loop do
      value, cas = @client.gets(key)
      
      # 値が存在しない場合は新規作成
      if value.nil?
        new_value = yield(nil)
        success = @client.add(key, new_value, ttl)
        return new_value if success
        next  # 競合が発生した場合は再試行
      end
      
      # 既存値を更新
      new_value = yield(value)
      success = @client.cas(key, new_value, cas, ttl)
      return new_value if success
      # CAS失敗時は再試行
    end
  end
end

# 使用例
cache = CacheManager.new(['localhost:11211'])

# カウンターの操作
current_count = cache.increment_counter('page_views', 1, 0)
puts "Page views: #{current_count}"

# フォールバック付きキャッシュ
user_data = cache.fetch_with_fallback('user:123', 
  -> { User.find(123).to_hash }, 
  1800
)

# 原子的更新
updated_settings = cache.atomic_update('app_settings') do |current_settings|
  current_settings ||= {}
  current_settings['last_updated'] = Time.now.to_i
  current_settings
end

セッションストアとしての使用

# config/initializers/session_store.rb
require 'action_dispatch/middleware/session/dalli_store'

Rails.application.config.session_store :dalli_store, {
  memcache_server: ['session1.example.com:11211', 'session2.example.com:11211'],
  namespace: 'sessions',
  key: '_myapp_session',
  expire_after: 2.weeks,
  compress: true,
  pool_size: 10
}

# セッション管理クラス
class SessionManager
  def self.cache_client
    @cache_client ||= Dalli::Client.new(
      Rails.application.config.session_store[1][:memcache_server],
      namespace: 'custom_sessions'
    )
  end
  
  def self.store_user_session(session_id, user_data)
    cache_client.set(session_id, user_data, 86400)  # 24時間
  end
  
  def self.get_user_session(session_id)
    cache_client.get(session_id)
  end
  
  def self.invalidate_user_session(session_id)
    cache_client.delete(session_id)
  end
  
  def self.extend_session(session_id, additional_time = 3600)
    data = cache_client.get(session_id)
    return false unless data
    
    cache_client.set(session_id, data, additional_time)
    true
  end
end

パフォーマンス監視とデバッグ

require 'dalli'

class MonitoredCache
  def initialize(servers, options = {})
    @client = Dalli::Client.new(servers, options)
    @stats = { hits: 0, misses: 0, sets: 0, deletes: 0 }
  end
  
  def get(key)
    start_time = Time.now
    value = @client.get(key)
    duration = Time.now - start_time
    
    if value.nil?
      @stats[:misses] += 1
      Rails.logger.debug "Cache MISS for key: #{key} (#{duration * 1000}ms)"
    else
      @stats[:hits] += 1
      Rails.logger.debug "Cache HIT for key: #{key} (#{duration * 1000}ms)"
    end
    
    value
  end
  
  def set(key, value, ttl = nil)
    start_time = Time.now
    result = @client.set(key, value, ttl)
    duration = Time.now - start_time
    
    @stats[:sets] += 1
    Rails.logger.debug "Cache SET for key: #{key} (#{duration * 1000}ms)"
    
    result
  end
  
  def delete(key)
    result = @client.delete(key)
    @stats[:deletes] += 1
    Rails.logger.debug "Cache DELETE for key: #{key}"
    result
  end
  
  def stats
    server_stats = @client.stats
    {
      local: @stats,
      hit_rate: (@stats[:hits].to_f / [@stats[:hits] + @stats[:misses], 1].max * 100).round(2),
      servers: server_stats
    }
  end
  
  def reset_stats
    @stats = { hits: 0, misses: 0, sets: 0, deletes: 0 }
  end
end

# 使用例
monitored_cache = MonitoredCache.new(['localhost:11211'])

# 通常の使用
monitored_cache.set('test_key', 'test_value')
value = monitored_cache.get('test_key')

# 統計情報の確認
puts monitored_cache.stats