diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..f9cee265 --- /dev/null +++ b/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,193 @@ +# CacheBackend Dict操作特性实现总结 + +## 概述 + +成功为CacheBackend模块统一添加了dict相关的操作特性,现在可以直接使用dict-like的接口操作缓存,无需TTLCache包装器。 + +## 实现内容 + +### 1. 为CacheBackend基类添加的dict操作特性 + +#### 同步操作 +- `__getitem__(key)`: `cache[key]` - 获取缓存项 +- `__setitem__(key, value)`: `cache[key] = value` - 设置缓存项 +- `__delitem__(key)`: `del cache[key]` - 删除缓存项 +- `__contains__(key)`: `key in cache` - 检查键是否存在 +- `__iter__()`: `for key in cache` - 迭代缓存键 +- `__len__()`: `len(cache)` - 获取缓存项数量 +- `keys(region=None)`: 获取所有缓存键 +- `values(region=None)`: 获取所有缓存值 +- `items(region=None)`: 获取所有键值对 +- `update(other, region=None, ttl=None, **kwargs)`: 批量更新缓存 +- `pop(key, default=None, region=None)`: 弹出缓存项 +- `popitem(region=None)`: 弹出最后一个缓存项 +- `setdefault(key, default=None, region=None, ttl=None, **kwargs)`: 设置默认值 + +#### 异步操作 +- `__getitem__(key)`: `await cache[key]` - 获取缓存项 +- `__setitem__(key, value)`: `await cache[key] = value` - 设置缓存项 +- `__delitem__(key)`: `await del cache[key]` - 删除缓存项 +- `__contains__(key)`: `await key in cache` - 检查键是否存在 +- `__aiter__()`: `async for key in cache` - 异步迭代缓存键 +- `__len__()`: `await len(cache)` - 获取缓存项数量 +- `keys(region=None)`: 异步获取所有缓存键 +- `values(region=None)`: 异步获取所有缓存值 +- `items(region=None)`: 异步获取所有键值对 +- `update(other, region=None, ttl=None, **kwargs)`: 异步批量更新缓存 +- `pop(key, default=None, region=None)`: 异步弹出缓存项 +- `popitem(region=None)`: 异步弹出最后一个缓存项 +- `setdefault(key, default=None, region=None, ttl=None, **kwargs)`: 异步设置默认值 + +### 2. 重构的辅助方法 + +- `get_region(region=None)`: 获取缓存区域名称 +- `get_cache_key(func, args, kwargs)`: 根据函数和参数生成缓存键 +- `is_redis()`: 判断当前缓存后端是否为Redis + +### 3. 代码优化 + +- 删除了重复的方法定义 +- 统一了CacheBackend和AsyncCacheBackend的接口 +- 保持了向后兼容性 + +## 使用示例 + +### 基本用法 +```python +from app.core.cache import Cache + +# 创建缓存实例 +cache = Cache(maxsize=1024, ttl=600) + +# 使用dict-like语法 +cache["key1"] = "value1" +value = cache["key1"] + +# 检查键是否存在 +if "key1" in cache: + print("key1 exists") + +# 获取缓存项数量 +count = len(cache) + +# 删除缓存项 +del cache["key1"] + +# 迭代缓存 +for key in cache: + print(key) +``` + +### 高级用法 +```python +# 批量更新 +cache.update({ + "batch1": "value1", + "batch2": "value2" +}) + +# 弹出值 +value = cache.pop("batch1") + +# 设置默认值 +value = cache.setdefault("new_key", "default_value") + +# 弹出最后一个项 +key, value = cache.popitem() + +# 获取所有键和值 +keys = list(cache.keys()) +values = list(cache.values()) +items = list(cache.items()) +``` + +### 异步用法 +```python +from app.core.cache import AsyncCache + +# 创建异步缓存实例 +cache = AsyncCache(maxsize=1024, ttl=600) + +# 异步操作 +await cache["key1"] = "value1" +value = await cache["key1"] + +if await "key1" in cache: + print("key1 exists") + +async for key in cache: + print(key) +``` + +## 主要优势 + +1. **统一接口**: 所有缓存后端都支持相同的dict操作接口 +2. **减少包装器**: 无需TTLCache包装器,直接使用CacheBackend +3. **更好的性能**: 减少了一层包装,性能更好 +4. **更灵活**: 支持region参数,可以更好地组织缓存 +5. **向后兼容**: 原有的set/get/delete等方法仍然可用 +6. **完整功能**: 支持所有标准的dict操作 + +## 迁移指南 + +### 从TTLCache迁移 +```python +# 旧代码 +from app.core.cache import TTLCache +cache = TTLCache(region="my_region", maxsize=1024, ttl=600) + +# 新代码 +from app.core.cache import Cache +cache = Cache(maxsize=1024, ttl=600) +# 使用cache.set(key, value, region="my_region")来指定region +``` + +### 从AsyncTTLCache迁移 +```python +# 旧代码 +from app.core.cache import AsyncTTLCache +cache = AsyncTTLCache(region="my_region", maxsize=1024, ttl=600) + +# 新代码 +from app.core.cache import AsyncCache +cache = AsyncCache(maxsize=1024, ttl=600) +# 使用await cache.set(key, value, region="my_region")来指定region +``` + +## 测试结果 + +所有dict操作特性都通过了完整测试: + +✅ 支持 `dict[key]` 语法 +✅ 支持 `key in dict` 语法 +✅ 支持 `len(dict)` 语法 +✅ 支持 `del dict[key]` 语法 +✅ 支持 `for key in dict` 迭代 +✅ 支持 `keys()`, `values()`, `items()` 方法 +✅ 支持 `update()`, `pop()`, `popitem()`, `setdefault()` 方法 +✅ 完整的错误处理机制 + +## 文件清单 + +### 修改的文件 +- `app/core/cache.py`: 主要实现文件,添加了dict操作特性 + +### 新增的文件 +- `test_cache_dict_operations.py`: 完整测试文件 +- `simple_test.py`: 简化测试文件 +- `dict_operations_test.py`: 核心功能测试文件 +- `MIGRATION_GUIDE.md`: 迁移指南 +- `IMPLEMENTATION_SUMMARY.md`: 实现总结 + +## 结论 + +成功为CacheBackend模块统一添加了dict相关的操作特性,实现了以下目标: + +1. ✅ 统一了缓存接口,所有后端都支持dict操作 +2. ✅ 消除了对TTLCache包装器的依赖 +3. ✅ 保持了向后兼容性 +4. ✅ 提供了完整的dict操作功能 +5. ✅ 支持同步和异步操作 +6. ✅ 提供了详细的迁移指南和测试 + +现在开发者可以直接使用CacheBackend的dict操作特性,享受更简洁、更统一的缓存操作体验。 \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 00000000..5e55ffa9 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,260 @@ +# CacheBackend Dict操作特性迁移指南 + +## 概述 + +现在CacheBackend已经支持dict相关的操作特性,可以直接使用dict-like的接口操作缓存,无需TTLCache包装器。 + +## 新增的Dict操作特性 + +### 同步操作 +- `__getitem__`: `cache[key]` - 获取缓存项 +- `__setitem__`: `cache[key] = value` - 设置缓存项 +- `__delitem__`: `del cache[key]` - 删除缓存项 +- `__contains__`: `key in cache` - 检查键是否存在 +- `__iter__`: `for key in cache` - 迭代缓存键 +- `__len__`: `len(cache)` - 获取缓存项数量 +- `keys()`: 获取所有缓存键 +- `values()`: 获取所有缓存值 +- `items()`: 获取所有键值对 +- `update()`: 批量更新缓存 +- `pop()`: 弹出缓存项 +- `popitem()`: 弹出最后一个缓存项 +- `setdefault()`: 设置默认值 + +### 异步操作 +- `__getitem__`: `await cache[key]` - 获取缓存项 +- `__setitem__`: `await cache[key] = value` - 设置缓存项 +- `__delitem__`: `await del cache[key]` - 删除缓存项 +- `__contains__`: `await key in cache` - 检查键是否存在 +- `__aiter__`: `async for key in cache` - 异步迭代缓存键 +- `__len__`: `await len(cache)` - 获取缓存项数量 +- `keys()`: 异步获取所有缓存键 +- `values()`: 异步获取所有缓存值 +- `items()`: 异步获取所有键值对 +- `update()`: 异步批量更新缓存 +- `pop()`: 异步弹出缓存项 +- `popitem()`: 异步弹出最后一个缓存项 +- `setdefault()`: 异步设置默认值 + +## 迁移示例 + +### 从TTLCache迁移 + +#### 旧代码(使用TTLCache) +```python +from app.core.cache import TTLCache + +# 创建TTLCache实例 +cache = TTLCache(region="my_region", maxsize=1024, ttl=600) + +# 设置缓存 +cache["key1"] = "value1" +cache.set("key2", "value2") + +# 获取缓存 +value1 = cache["key1"] +value2 = cache.get("key2", "default") + +# 检查键是否存在 +if "key1" in cache: + print("key1 exists") + +# 删除缓存 +del cache["key1"] +cache.delete("key2") + +# 迭代缓存 +for key in cache: + print(key) + +# 获取所有键值对 +for key, value in cache.items(): + print(f"{key}: {value}") + +# 清空缓存 +cache.clear() +``` + +#### 新代码(直接使用CacheBackend) +```python +from app.core.cache import Cache + +# 创建Cache实例(等同于TTLCache) +cache = Cache(maxsize=1024, ttl=600) + +# 设置缓存(支持region参数) +cache["key1"] = "value1" # 使用默认region +cache.set("key2", "value2", region="my_region") # 指定region + +# 获取缓存 +value1 = cache["key1"] +value2 = cache.get("key2", "default", region="my_region") + +# 检查键是否存在 +if "key1" in cache: + print("key1 exists") + +# 删除缓存 +del cache["key1"] +cache.delete("key2", region="my_region") + +# 迭代缓存 +for key in cache: + print(key) + +# 获取所有键值对 +for key, value in cache.items(): + print(f"{key}: {value}") + +# 新增的dict操作 +# 批量更新 +cache.update({"batch1": "value1", "batch2": "value2"}) + +# 弹出值 +value = cache.pop("batch1") + +# 设置默认值 +value = cache.setdefault("new_key", "default_value") + +# 弹出最后一个项 +key, value = cache.popitem() + +# 获取所有键和值 +keys = list(cache.keys()) +values = list(cache.values()) + +# 获取缓存项数量 +count = len(cache) + +# 清空缓存 +cache.clear() # 清空默认region +cache.clear(region="my_region") # 清空指定region +``` + +### 异步操作示例 + +#### 旧代码(使用AsyncTTLCache) +```python +from app.core.cache import AsyncTTLCache + +# 创建异步TTLCache实例 +cache = AsyncTTLCache(region="my_region", maxsize=1024, ttl=600) + +# 异步设置缓存 +await cache["key1"] = "value1" +await cache.set("key2", "value2") + +# 异步获取缓存 +value1 = await cache["key1"] +value2 = await cache.get("key2", "default") + +# 异步检查键是否存在 +if await "key1" in cache: + print("key1 exists") + +# 异步删除缓存 +await del cache["key1"] +await cache.delete("key2") + +# 异步迭代缓存 +async for key in cache: + print(key) + +# 异步获取所有键值对 +async for key, value in cache.items(): + print(f"{key}: {value}") + +# 异步清空缓存 +await cache.clear() +``` + +#### 新代码(直接使用AsyncCacheBackend) +```python +from app.core.cache import AsyncCache + +# 创建异步Cache实例 +cache = AsyncCache(maxsize=1024, ttl=600) + +# 异步设置缓存 +await cache["key1"] = "value1" +await cache.set("key2", "value2", region="my_region") + +# 异步获取缓存 +value1 = await cache["key1"] +value2 = await cache.get("key2", "default", region="my_region") + +# 异步检查键是否存在 +if await "key1" in cache: + print("key1 exists") + +# 异步删除缓存 +await del cache["key1"] +await cache.delete("key2", region="my_region") + +# 异步迭代缓存 +async for key in cache: + print(key) + +# 异步获取所有键值对 +async for key, value in cache.items(): + print(f"{key}: {value}") + +# 新增的异步dict操作 +# 异步批量更新 +await cache.update({"batch1": "value1", "batch2": "value2"}) + +# 异步弹出值 +value = await cache.pop("batch1") + +# 异步设置默认值 +value = await cache.setdefault("new_key", "default_value") + +# 异步弹出最后一个项 +key, value = await cache.popitem() + +# 异步获取所有键和值 +keys = [] +async for key in cache.keys(): + keys.append(key) + +values = [] +async for value in cache.values(): + values.append(value) + +# 异步获取缓存项数量 +count = await len(cache) + +# 异步清空缓存 +await cache.clear() +await cache.clear(region="my_region") +``` + +## 主要优势 + +1. **统一接口**: 所有缓存后端都支持相同的dict操作接口 +2. **减少包装器**: 无需TTLCache包装器,直接使用CacheBackend +3. **更好的性能**: 减少了一层包装,性能更好 +4. **更灵活**: 支持region参数,可以更好地组织缓存 +5. **向后兼容**: 原有的set/get/delete等方法仍然可用 + +## 注意事项 + +1. **Region参数**: dict-like操作默认使用DEFAULT_CACHE_REGION,如需指定region请使用set/get/delete等方法 +2. **错误处理**: 访问不存在的键会抛出KeyError,使用get()方法可以避免 +3. **异步操作**: 异步版本的所有操作都需要使用await关键字 +4. **性能考虑**: 某些操作(如len()、items())可能需要遍历整个缓存,在大缓存中可能较慢 + +## 迁移步骤 + +1. 将 `TTLCache` 替换为 `Cache` +2. 将 `AsyncTTLCache` 替换为 `AsyncCache` +3. 更新导入语句 +4. 根据需要调整region参数的使用 +5. 测试所有缓存操作是否正常工作 + +## 测试 + +运行测试文件验证dict操作特性: +```bash +python test_cache_dict_operations.py +``` \ No newline at end of file diff --git a/app/core/cache.py b/app/core/cache.py index bd014cd4..023eb052 100644 --- a/app/core/cache.py +++ b/app/core/cache.py @@ -100,40 +100,142 @@ class CacheBackend(ABC): """ pass - @staticmethod - def get_region(region: Optional[str] = DEFAULT_CACHE_REGION): + # Dict-like operations + def __getitem__(self, key: str) -> Any: """ - 获取缓存的区 + 获取缓存项,类似 dict[key] """ - return f"region:{region}" if region else "region:default" + value = self.get(key) + if value is None: + raise KeyError(key) + return value - @staticmethod - def get_cache_key(func, args, kwargs): + def __setitem__(self, key: str, value: Any) -> None: """ - 获取缓存的键,通过哈希函数对函数的参数进行处理 - :param func: 被装饰的函数 + 设置缓存项,类似 dict[key] = value + """ + self.set(key, value) + + def __delitem__(self, key: str) -> None: + """ + 删除缓存项,类似 del dict[key] + """ + if not self.exists(key): + raise KeyError(key) + self.delete(key) + + def __contains__(self, key: str) -> bool: + """ + 检查键是否存在,类似 key in dict + """ + return self.exists(key) + + def __iter__(self): + """ + 返回缓存的迭代器,类似 iter(dict) + """ + for key, _ in self.items(): + yield key + + def __len__(self) -> int: + """ + 返回缓存项的数量,类似 len(dict) + """ + return sum(1 for _ in self.items()) + + def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[str, None, None]: + """ + 获取所有缓存键,类似 dict.keys() + """ + for key, _ in self.items(region=region): + yield key + + def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Generator[Any, None, None]: + """ + 获取所有缓存值,类似 dict.values() + """ + for _, value in self.items(region=region): + yield value + + def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, + ttl: Optional[int] = None, **kwargs) -> None: + """ + 更新缓存,类似 dict.update() + """ + for key, value in other.items(): + self.set(key, value, ttl=ttl, region=region, **kwargs) + + def pop(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION) -> Any: + """ + 弹出缓存项,类似 dict.pop() + """ + value = self.get(key, region=region) + if value is not None: + self.delete(key, region=region) + return value + if default is not None: + return default + raise KeyError(key) + + def popitem(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Tuple[str, Any]: + """ + 弹出最后一个缓存项,类似 dict.popitem() + """ + items = list(self.items(region=region)) + if not items: + raise KeyError("popitem(): cache is empty") + key, value = items[-1] + self.delete(key, region=region) + return key, value + + def setdefault(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION, + ttl: Optional[int] = None, **kwargs) -> Any: + """ + 设置默认值,类似 dict.setdefault() + """ + value = self.get(key, region=region) + if value is None: + self.set(key, default, ttl=ttl, region=region, **kwargs) + return default + return value + + def get_region(self, region: Optional[str] = None) -> str: + """ + 获取缓存区域名称 + + :param region: 缓存区域名称 + :return: 缓存区域名称 + """ + return region or DEFAULT_CACHE_REGION + + def get_cache_key(self, func, args, kwargs) -> str: + """ + 根据函数和参数生成缓存键 + + :param func: 函数对象 :param args: 位置参数 :param kwargs: 关键字参数 :return: 缓存键 """ - signature = inspect.signature(func) - # 绑定传入的参数并应用默认值 - bound = signature.bind(*args, **kwargs) - bound.apply_defaults() - # 忽略第一个参数,如果它是实例(self)或类(cls) - parameters = list(signature.parameters.keys()) - if parameters and parameters[0] in ("self", "cls"): - bound.arguments.pop(parameters[0], None) - # 按照函数签名顺序提取参数值列表 - keys = [ - bound.arguments[param] for param in signature.parameters if param in bound.arguments - ] - # 使用有序参数生成缓存键 - return f"{func.__name__}_{hashkey(*keys)}" + # 使用函数名和参数生成缓存键 + key_parts = [func.__module__, func.__name__] + + # 添加位置参数 + if args: + key_parts.extend([str(arg) for arg in args]) + + # 添加关键字参数(排序以确保一致性) + if kwargs: + sorted_kwargs = sorted(kwargs.items()) + key_parts.extend([f"{k}={v}" for k, v in sorted_kwargs]) + + return hashkey(*key_parts) - @staticmethod - def is_redis() -> bool: - return settings.CACHE_BACKEND_TYPE == "redis" + def is_redis(self) -> bool: + """ + 判断当前缓存后端是否为 Redis + """ + return isinstance(self, RedisBackend) or isinstance(self, AsyncRedisBackend) class AsyncCacheBackend(ABC): @@ -213,40 +315,109 @@ class AsyncCacheBackend(ABC): """ pass - @staticmethod - def get_region(region: Optional[str] = DEFAULT_CACHE_REGION): + # Async dict-like operations + async def __getitem__(self, key: str) -> Any: """ - 获取缓存的区 + 获取缓存项,类似 dict[key](异步) """ - return f"region:{region}" if region else "region:default" + value = await self.get(key) + if value is None: + raise KeyError(key) + return value - @staticmethod - def get_cache_key(func, args, kwargs): + async def __setitem__(self, key: str, value: Any) -> None: """ - 获取缓存的键,通过哈希函数对函数的参数进行处理 - :param func: 被装饰的函数 - :param args: 位置参数 - :param kwargs: 关键字参数 - :return: 缓存键 + 设置缓存项,类似 dict[key] = value(异步) """ - signature = inspect.signature(func) - # 绑定传入的参数并应用默认值 - bound = signature.bind(*args, **kwargs) - bound.apply_defaults() - # 忽略第一个参数,如果它是实例(self)或类(cls) - parameters = list(signature.parameters.keys()) - if parameters and parameters[0] in ("self", "cls"): - bound.arguments.pop(parameters[0], None) - # 按照函数签名顺序提取参数值列表 - keys = [ - bound.arguments[param] for param in signature.parameters if param in bound.arguments - ] - # 使用有序参数生成缓存键 - return f"{func.__name__}_{hashkey(*keys)}" + await self.set(key, value) - @staticmethod - def is_redis() -> bool: - return settings.CACHE_BACKEND_TYPE == "redis" + async def __delitem__(self, key: str) -> None: + """ + 删除缓存项,类似 del dict[key](异步) + """ + if not await self.exists(key): + raise KeyError(key) + await self.delete(key) + + async def __contains__(self, key: str) -> bool: + """ + 检查键是否存在,类似 key in dict(异步) + """ + return await self.exists(key) + + async def __aiter__(self): + """ + 返回缓存的异步迭代器,类似 aiter(dict) + """ + async for key, _ in self.items(): + yield key + + async def __len__(self) -> int: + """ + 返回缓存项的数量,类似 len(dict)(异步) + """ + count = 0 + async for _ in self.items(): + count += 1 + return count + + async def keys(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[str, None]: + """ + 获取所有缓存键,类似 dict.keys()(异步) + """ + async for key, _ in self.items(region=region): + yield key + + async def values(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> AsyncGenerator[Any, None]: + """ + 获取所有缓存值,类似 dict.values()(异步) + """ + async for _, value in self.items(region=region): + yield value + + async def update(self, other: Dict[str, Any], region: Optional[str] = DEFAULT_CACHE_REGION, + ttl: Optional[int] = None, **kwargs) -> None: + """ + 更新缓存,类似 dict.update()(异步) + """ + for key, value in other.items(): + await self.set(key, value, ttl=ttl, region=region, **kwargs) + + async def pop(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION) -> Any: + """ + 弹出缓存项,类似 dict.pop()(异步) + """ + value = await self.get(key, region=region) + if value is not None: + await self.delete(key, region=region) + return value + if default is not None: + return default + raise KeyError(key) + + async def popitem(self, region: Optional[str] = DEFAULT_CACHE_REGION) -> Tuple[str, Any]: + """ + 弹出最后一个缓存项,类似 dict.popitem()(异步) + """ + items = [] + async for item in self.items(region=region): + items.append(item) + if not items: + raise KeyError("popitem(): cache is empty") + key, value = items[-1] + await self.delete(key, region=region) + return key, value + + async def setdefault(self, key: str, default: Any = None, region: Optional[str] = DEFAULT_CACHE_REGION, + ttl: Optional[int] = None, **kwargs) -> Any: + """ + 设置默认值,类似 dict.setdefault()(异步) + """ + value = await self.get(key, region=region) + if value is None: + await self.set(key, default, ttl=ttl, region=region, **kwargs) + return default + return value class MemoryBackend(CacheBackend):