Dogpile Cache

PythonライブラリキャッシュSQLAlchemyパフォーマンスキャッシュスタンピード

GitHub概要

sqlalchemy/dogpile.cache

dogpile.cache is a Python caching API which provides a generic interface to caching backends of any variety

スター281
ウォッチ7
フォーク49
作成日:2018年11月24日
言語:Python
ライセンス:MIT License

トピックス

memcachedredis

スター履歴

sqlalchemy/dogpile.cache Star History
データ取得日時: 2025/10/22 08:07

ライブラリ

Dogpile Cache

概要

Dogpile Cacheは、あらゆる種類のキャッシュバックエンドへの汎用インターフェースを提供するPythonキャッシングAPIです。SQLAlchemyプロジェクトの一部として開発され、Beakerキャッシングシステムの後継として設計されています。

詳細

Dogpile Cache(dogpile.cache)は、SQLAlchemyプロジェクトが開発した先進的なキャッシングライブラリで、Beakerキャッシングシステムが抱えていた根本的な性能問題を解決します。特に「double-fetching」問題(値がキャッシュから頻繁に二重取得される問題)を修正し、大幅に簡素化、改良、テストされたdogpile lockエンジンを中核としています。豊富なキャッシュバックエンドをサポートし、memcached(python-memcached、pylibmc、bmemcached)、Redis、anydbm、辞書ベースなど複数の選択肢を提供します。キー生成メカニズムは完全にカスタマイズ可能で、プラグ可能なキージェネレーターとオプションのキーマングラー機能により、キャッシュキーとファンクション呼び出しの対応を細かく制御できます。分散ロック機能により、各バックエンドは独自の「分散」ロック版を実装し、バックエンドのストレージシステムに対応した分散処理を提供します。SQLAlchemyとの強力な統合により、ORMレベルでの効率的なキャッシングが可能です。

メリット・デメリット

メリット

  • パフォーマンス改善: Beakerの「double-fetching」問題を根本的に解決
  • SQLAlchemy統合: ORMキャッシュ、FromCache、RelationshipCacheなどの専用クラス
  • 柔軟なキー管理: プラグ可能なキージェネレーターとキーマングラー機能
  • 分散ロック: バックエンドごとの分散ロック実装でスタンピード回避
  • 豊富なバックエンド: memcached、Redis、anydbm、辞書など多様な選択肢
  • 高度なキャッシング戦略: 古い値の返却機能やバックグラウンド処理サポート
  • エラー処理: デシリアライゼーション失敗時の適切な処理機能

デメリット

  • 学習コスト: 高度な機能により初期の学習コストが高い
  • 設定の複雑さ: 細かい制御が可能な分、設定が複雑になる場合がある
  • SQLAlchemy依存: SQLAlchemy専用機能が多く、他のORMでは恩恵が少ない
  • バックエンド依存: 選択したバックエンドの制限や特性に依存
  • デバッグの困難さ: 分散ロックやキャッシュ階層により、デバッグが困難な場合がある
  • 最小Python要件: Python 3.8以上が必要(古いPython版では使用不可)

主要リンク

書き方の例

基本的な設定と使用方法

from dogpile.cache import make_region

# キャッシュリージョンの作成
cache_region = make_region().configure(
    'dogpile.cache.memory',  # メモリキャッシュバックエンド
    expiration_time=3600,    # 1時間の有効期限
)

# 基本的なキャッシュ操作
@cache_region.cache_on_arguments()
def get_user_data(user_id):
    """重い処理をキャッシュ"""
    # データベースアクセスや外部API呼び出しなど
    import time
    time.sleep(1)  # 重い処理をシミュレート
    return f"User data for ID: {user_id}"

# 関数呼び出し(初回は処理実行、2回目以降はキャッシュから取得)
user_data = get_user_data(123)
cached_data = get_user_data(123)  # キャッシュから高速取得

# 手動キャッシュ操作
cache_region.set('manual_key', 'manual_value')
value = cache_region.get('manual_key')

Redis バックエンドの設定

from dogpile.cache import make_region
import redis

# Redisを使用したキャッシュリージョン
redis_region = make_region().configure(
    'dogpile.cache.redis',
    arguments={
        'host': 'localhost',
        'port': 6379,
        'db': 0,
        'redis_expiration_time': 3600,
        'distributed_lock': True,  # 分散ロック有効化
    }
)

# または既存のRedis接続を使用
redis_client = redis.StrictRedis(host='localhost', port=6379, db=1)
redis_region_custom = make_region().configure(
    'dogpile.cache.redis',
    arguments={
        'connection': redis_client,
        'distributed_lock': True,
    }
)

@redis_region.cache_on_arguments(expiration_time=1800)
def expensive_computation(param1, param2):
    """重い計算処理"""
    result = param1 ** param2
    return result

# 使用例
result = expensive_computation(2, 100)  # 初回は計算実行
cached_result = expensive_computation(2, 100)  # キャッシュから取得

SQLAlchemy統合

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dogpile.cache import make_region
from dogpile.cache.api import CantDeserializeException

# データベース設定
engine = create_engine('sqlite:///example.db')
Base = declarative_base()
Session = sessionmaker(bind=engine)

# モデル定義
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    email = Column(String(100))

# キャッシュリージョン設定
cache_region = make_region().configure(
    'dogpile.cache.memory',
    expiration_time=1800
)

# SQLAlchemy統合キャッシュ
from dogpile.cache.ext.sqlalchemy import FromCache, RelationshipCache

def get_user_with_cache(session, user_id):
    """ユーザー取得(キャッシュ付き)"""
    # FromCacheオプションを使用してクエリをキャッシュ
    user = session.query(User)\
        .options(FromCache(cache_region, "user", User.id == user_id))\
        .filter(User.id == user_id)\
        .first()
    return user

# 使用例
session = Session()
user1 = get_user_with_cache(session, 1)  # DBアクセス
user2 = get_user_with_cache(session, 1)  # キャッシュから取得

# リレーションシップキャッシュ
class Post(Base):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    title = Column(String(200))

def get_user_with_posts(session, user_id):
    """ユーザーと投稿を取得(リレーションキャッシュ付き)"""
    user = session.query(User)\
        .options(
            FromCache(cache_region, "user", User.id == user_id),
            RelationshipCache(cache_region, "user_posts", User.posts)
        )\
        .filter(User.id == user_id)\
        .first()
    return user

キャッシュスタンピード対策

from dogpile.cache import make_region
from dogpile.cache.api import Lock
import threading
import time

# スタンピード対策付きキャッシュリージョン
stampede_region = make_region().configure(
    'dogpile.cache.memory',
    expiration_time=300,  # 5分間
)

@stampede_region.cache_on_arguments()
def slow_function(param):
    """重くて時間のかかる処理"""
    print(f"Executing slow function with param: {param}")
    time.sleep(2)  # 重い処理をシミュレート
    return f"Result for {param}"

# 古い値を返す戦略(スタンピード回避)
@stampede_region.cache_on_arguments(
    should_cache_fn=lambda value: value is not None
)
def cached_with_stale_fallback(param):
    """古い値を返してスタンピードを回避"""
    def creator():
        print(f"Creating fresh value for {param}")
        time.sleep(2)
        return f"Fresh value for {param}"
    
    # 古い値が利用可能な場合、即座に返す
    return creator()

# バックグラウンド処理によるスタンピード対策
def background_refresh(key, creator_func):
    """バックグラウンドでキャッシュを更新"""
    def background_worker():
        fresh_value = creator_func()
        stampede_region.set(key, fresh_value)
    
    thread = threading.Thread(target=background_worker)
    thread.daemon = True
    thread.start()

@stampede_region.cache_on_arguments(
    function_multi_key_generator=lambda namespace, func, **kwargs: 
        f"{namespace}:{func.__name__}:{':'.join(str(v) for v in kwargs.values())}"
)
def smart_cached_function(param):
    """スマートキャッシング戦略"""
    def creator():
        time.sleep(1)
        return f"Smart result for {param}"
    
    # キー生成
    cache_key = f"smart_function:{param}"
    
    # 既存の値を確認
    existing_value = stampede_region.get(cache_key)
    
    # 値が存在し、期限が近い場合はバックグラウンド更新
    if existing_value and should_refresh_background():
        background_refresh(cache_key, creator)
        return existing_value
    
    return creator()

def should_refresh_background():
    """バックグラウンド更新が必要かどうかの判定"""
    # 30%の確率でバックグラウンド更新をトリガー
    import random
    return random.random() < 0.3

カスタムキージェネレーターとマングラー

from dogpile.cache import make_region
from dogpile.cache.util import function_key_generator
import hashlib
import json

# カスタムキージェネレーター
def custom_key_generator(namespace, fn, **kwargs):
    """カスタムキー生成ロジック"""
    # 関数名とパラメータからキーを生成
    fn_name = fn.__name__
    
    # 複雑なオブジェクトをシリアライズ
    serialized_args = json.dumps(kwargs, sort_keys=True, default=str)
    
    # ハッシュ化して短縮
    hash_obj = hashlib.md5(serialized_args.encode())
    hashed_args = hash_obj.hexdigest()[:12]
    
    return f"{namespace}:{fn_name}:{hashed_args}"

# カスタムキーマングラー
def custom_key_mangler(key):
    """キーを変換・正規化"""
    # 特殊文字を置換
    key = key.replace(':', '_').replace(' ', '_')
    # 長さ制限(memcached対応)
    if len(key) > 200:
        key = key[:190] + hashlib.md5(key.encode()).hexdigest()[:10]
    return key.lower()

# カスタム設定を適用したリージョン
custom_region = make_region(
    key_mangler=custom_key_mangler
).configure(
    'dogpile.cache.memory',
    expiration_time=3600
)

@custom_region.cache_on_arguments(
    function_key_generator=custom_key_generator
)
def complex_function(user_obj, config_dict, timestamp):
    """複雑な引数を持つ関数"""
    return f"Processed {user_obj} with {config_dict} at {timestamp}"

# 使用例
from datetime import datetime

user = {'id': 123, 'name': 'Alice'}
config = {'debug': True, 'timeout': 30}
result = complex_function(user, config, datetime.now())

エラーハンドリングとデシリアライゼーション

from dogpile.cache import make_region
from dogpile.cache.api import CantDeserializeException
import pickle
import json

class SafeJsonRegion:
    """安全なJSONシリアライゼーション付きキャッシュ"""
    
    def __init__(self, region):
        self.region = region
    
    def get_or_create(self, key, creator_func, expiration_time=3600):
        """安全なget_or_create実装"""
        try:
            # キャッシュから取得を試行
            cached_value = self.region.get(key)
            if cached_value is not None:
                return json.loads(cached_value)
        except (json.JSONDecodeError, CantDeserializeException):
            # デシリアライゼーション失敗時は新しい値を生成
            pass
        
        # 新しい値を生成
        fresh_value = creator_func()
        
        # JSON形式でキャッシュに保存
        try:
            serialized_value = json.dumps(fresh_value, default=str)
            self.region.set(key, serialized_value, expiration_time)
        except (TypeError, ValueError):
            # シリアライゼーション失敗時はキャッシュしない
            pass
        
        return fresh_value

# 使用例
error_region = make_region().configure('dogpile.cache.memory')
safe_cache = SafeJsonRegion(error_region)

def get_user_profile(user_id):
    """安全なユーザープロファイル取得"""
    def creator():
        # データベースから取得
        return {
            'id': user_id,
            'name': f'User {user_id}',
            'created_at': datetime.now(),
            'preferences': {'theme': 'dark', 'language': 'ja'}
        }
    
    return safe_cache.get_or_create(
        f'user_profile:{user_id}',
        creator,
        expiration_time=1800
    )

# グレースフルなキャッシュ無効化
def invalidate_with_grace(region, key, creator_func):
    """グレースフルなキャッシュ無効化"""
    old_value = region.get(key)
    
    try:
        # 新しい値を生成
        new_value = creator_func()
        region.set(key, new_value)
        return new_value
    except Exception as e:
        # 新しい値の生成に失敗した場合、古い値を返す
        if old_value is not None:
            print(f"Using stale value due to error: {e}")
            return old_value
        raise