Files
chongming-utils-cache/README.md
T
2026-09-04 14:44:06 +08:00

133 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# chongming-cache
统一缓存封装,dict-like 接口,后端可切换 **sqlite**(本机)/ **redis**(跨主机),通过配置一行切换,业务代码零改动。
---
## 安装
```bash
uv pip install -e utils/cache
# 或作为依赖接入(与 chongming-database 等同约定):
# [tool.uv.sources]
# chongming-cache = { git = "http://124.71.68.6:3000/chongming/chongming-utils-cache.git" }
```
---
## 快速开始
```python
from chongming_cache import Cache
# 方式一:从配置创建(主项目 config.toml 的 [cache] 段)
cache = Cache.from_config(config["cache"])
# 方式二:显式参数
cache = Cache("sqlite", path="./cache_data/cache.db")
# cache = Cache("redis", redis_host="127.0.0.1", redis_port=6379, redis_prefix="chongming:")
# 历史后端名 "diskcache" 兼容映射为 sqlite
```
### 映射协议(与 dict / diskcache 用法一致)
```python
cache["key"] = {"a": 1} # 写入(无 TTL)
if "key" in cache: # 存在性检查(含 TTL 判断)
value = cache["key"] # 读取,未命中抛 KeyError
del cache["key"] # 删除,未命中抛 KeyError
len(cache) # 条目数
for k in cache: ... # 迭代 key
```
### 扩展接口
```python
cache.get("key", default=None) # 未命中返回 default
cache.set("key", value, expire=60) # 写入,TTL 秒(None = 永久)
cache.touch("key", expire=120) # 刷新 TTL,返回是否命中(未命中 False)
cache.delete("key") # 删除,返回是否命中
cache.clear() # 清空(redis 仅清本前缀命名空间)
cache.close() # 释放连接
with Cache.from_config(cfg) as cache: ... # 上下文管理器,退出自动 close
cache.client # 底层客户端逃生口(sqlite3.Connection / redis.Redis)
# 原子写(分布式锁原语):仅当 key 不存在时写入,成功返回 True,已存在返回 False,不覆盖
ok = cache.backend.set_nx("leader", {"node": "gw-1"}, expire=30)
```
### 分布式排他锁 `DistributedLock`
基于 `backend.set_nx`(SET NX + TTL 租约)实现的**跨进程/跨主机排他锁**,用于集群协调(如保证全局只有一个 MQTT Bridge 消费):
```python
import asyncio
from chongming_cache import Cache, DistributedLock
async def main():
cache = Cache.from_config(config["cache"])
lock = DistributedLock(cache, "mqtt_bridge", ttl=30, renew_interval=10)
if not lock.acquire(): # 全局唯一持锁方返回 True,其余返回 False
print("其他实例已持有,跳过")
return
lock.start() # 后台自动续约(防宕机/长暂停导致锁过期被抢)
try:
... # 执行受保护的单例任务
finally:
await lock.stop() # 停止续约
lock.release() # 受保护释放
cache.close()
asyncio.run(main())
```
- `acquire()`:SET NX,原子地仅在不存在时写入;key 带 TTL 租约,持有方宕机后到期自动让出,不会死锁。
- `start()` / `stop()`:后台续约任务;续约前校验 token,仅在自己仍持有时 `touch`,锁丢失即停止续约。
- `release()`:受保护释放——仅当 token 仍匹配(仍是自己持有)才删除,绝不误删其他进程/主机抢到的锁。
- **后端适用性**:`redis` 真正跨主机共享;`sqlite` 基于共享文件(SQLite 多进程安全),可用于单机多进程或共享卷场景,不可跨无共享文件系统的主机。
---
## 后端配置(config.toml `[cache]` 段)
```toml
[cache]
backend = "sqlite" # "sqlite"(默认,本机共享) | "redis"(跨主机)
path = "./cache_data/cache.db" # 兼容旧键 cache_store_path(指向目录时自动追加 cache.db)
# redis 后端专用(backend = "redis" 时生效,均有默认值)
# redis_host = "127.0.0.1"
# redis_port = 6379
# redis_db = 0
# redis_password = ""
# redis_prefix = "chongming:"
```
所有键均可缺省:旧配置(只有 `cache_store_path`)自动按 sqlite 后端工作。
---
## 双后端语义对齐
| 行为 | sqlite(本机) | redis(跨主机) |
| ---- | ----------- | ----------- |
| 值序列化 | JSON(orjson) | JSON(orjson)(与 Rust 进程插件 serde_json 互通) |
| 未命中 `cache[key]` | `KeyError` | `KeyError` |
| `set(..., expire=N)` | 秒级 TTL,读时惰性淘汰 | `SET EX N`,到期即消失 |
| `touch(未命中)` | 返回 False | 返回 False |
| `clear()` | 清空本地缓存 | 仅清 `redis_prefix` 命名空间(不 flushdb) |
| 多进程安全 | 是(SQLite 文件 + WAL) | 是(服务端原子性) |
## 跨语言共享(Rust 插件 / sqlite)
sqlite 后端把值以 JSON 文本存进表 `cache(k TEXT PRIMARY KEY, v TEXT NOT NULL, expire REAL)`,
schema 与 Rust 进程插件(`chongming-worker-rs`,feature `local-cache`)完全一致——Python 与
Rust 读写**同一份 `.db` 文件**即可互通,无需 Redis。Rust 侧读侧不改变该 schema。
## 依赖
- `sqlite3` — 本地共享后端(Python 标准库,零第三方依赖)
- `redis` — 远程后端(懒加载,仅选择 redis 后端时才 import)
- `orjson` — 全后端统一 JSON 序列化(缺失时回退标准库 `json`)