# easy-cache **Repository Path**: 170cmcurry/easy-cache ## Basic Information - **Project Name**: easy-cache - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-24 - **Last Updated**: 2026-09-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # easy-cache 轻量级 Go KV 缓存服务,供后台管理系统内部使用。**不引入 Redis**,仅依赖 Go 标准库,以内存 `map[string]json.RawMessage` 存储数据,JSON 文件持久化。 > 用途:后台任务状态、临时配置、运行状态、后台共享数据。 > 注意:网站访问逻辑不使用此服务,网站继续走 FastAPI + JSON 文件方案。 ## 特性 - ✅ 纯标准库实现,零第三方依赖 - ✅ `sync.RWMutex` 并发安全,读操作可并行 - ✅ 值为任意合法 JSON,写入什么就返回什么 - ✅ **秒级 TTL**:写入时可指定过期时间,支持永久缓存 - ✅ 过期 key 三重回收:读取惰性删除 + 后台每 60s 扫描清理 + 启动加载过滤 - ✅ **内存上限 + 淘汰**:默认 1GB 可配置,超限按「过期 → 短 TTL → LRU」优先级淘汰,防止 OOM - ✅ **分片持久化**:单文件最大 100MB,超出自动拆为 `cache-0001.json`…;启动多 goroutine 并行加载,几十万 key 秒级恢复 - ✅ dirty 标记 + 后台定时落盘(默认 5s),不是每次写都写磁盘 - ✅ 原子写入:`cache.json.tmp` 写完 + fsync 后 `rename`,异常退出不损坏文件 - ✅ 优雅退出:SIGINT/SIGTERM → 停 HTTP → 立即保存 → 退出 - ✅ 单元测试 + 基准测试覆盖,`-race` 检测无数据竞争 ## 项目结构 ``` easy-cache/ ├── main.go # 入口:配置加载、定时落盘、优雅退出 ├── cache/ │ ├── cache.go # 内存缓存(map + RWMutex + dirty) │ ├── cache_test.go # 单元测试 │ ├── persist.go # 加载 / 原子保存(JSON 文件) │ └── bench_test.go # 性能基准测试 ├── api/ │ ├── handler.go # HTTP 接口 │ └── handler_test.go # 接口测试(httptest) ├── config/ │ └── config.json # 配置文件 ├── data/ │ ├── cache.json # 旧版单文件(存在时自动迁移为分片) │ └── cache-0001.json … # 分片文件,每个 ≤ 100MB ├── kv-cache.service # systemd 部署文件 └── go.mod ``` ## 快速开始 ```bash go build -o easy-cache . # 编译 ./easy-cache # 默认读取 config/config.json ./easy-cache -config /path/to/config.json # 指定配置 ``` ## 配置文件 `config/config.json`: ```json { "listen": "127.0.0.1:9000", "data_file": "./data/cache.json", "flush_interval": 5, "max_memory": 1073741824, "eviction": "lru" } ``` | 字段 | 说明 | 默认值 | |---|---|---| | `listen` | HTTP 监听地址 | `127.0.0.1:9000` | | `data_file` | 持久化文件路径 | `./data/cache.json` | | `flush_interval` | 定时落盘间隔(秒) | `5` | | `max_memory` | 内存上限(byte) | `1073741824`(1GB) | | `eviction` | 淘汰策略,目前支持 `lru` | `lru` | 文件缺失或字段缺失时回落默认值;`flush_interval <= 0` 时按 5 秒处理。 ## API 所有缓存接口均为 `POST`,请求/响应为 JSON。 ### 健康检查(含加载状态与内存状态) ```bash curl http://127.0.0.1:9000/health # {"status":"ok","loaded":true,"keys":97468,"files":3,"memory_usage":298240970,"memory_limit":1073741824,"memory_percent":27} # loaded: 分片文件是否加载完成;files: 分片文件数量 ``` ### 写入(存在则覆盖,不存在则新增;`ttl` 单位秒,0 或省略表示永久) 超过内存上限且淘汰后仍放不下时,返回 `{"success":false,"error":"memory_limit"}`。 ```bash curl -X POST http://127.0.0.1:9000/cache/write \ -H "Content-Type: application/json" \ -d '{"key":"task:10001","value":{"status":"running","progress":50},"ttl":3600}' # {"success":true} ``` ### 读取(过期 key 自动删除) ```bash curl -X POST http://127.0.0.1:9000/cache/read \ -H "Content-Type: application/json" \ -d '{"key":"task:10001"}' # 命中: {"success":true,"value":{"status":"running","progress":50}} # 未命中: {"success":false,"value":null,"expired":false} # 已过期: {"success":false,"value":null,"expired":true} ``` ### 查询剩余 TTL ```bash curl -X POST http://127.0.0.1:9000/cache/ttl \ -H "Content-Type: application/json" \ -d '{"key":"task:10001"}' # {"ttl":300} 剩余 300 秒 # {"ttl":-1} 永久缓存 # {"ttl":-2} 不存在(含已过期) ``` ### 删除 ```bash curl -X POST http://127.0.0.1:9000/cache/delete \ -H "Content-Type: application/json" \ -d '{"key":"task:10001"}' # {"success":true} ``` ### 判断存在 ```bash curl -X POST http://127.0.0.1:9000/cache/exists \ -H "Content-Type: application/json" \ -d '{"key":"task:10001"}' # {"exists":true} ``` ### 列出所有 KEY ```bash curl -X POST http://127.0.0.1:9000/cache/list # {"keys":["task:10001","config:model"]} ``` ### 清空缓存(清空内存并立即保存文件) ```bash curl -X POST http://127.0.0.1:9000/cache/clear # {"success":true} ``` ### 错误响应 - 请求体不是合法 JSON、key 为空、value 非法 → `400 {"success":false,"error":"..."}` - 方法不对(如 GET 缓存接口)→ `405`,响应头带 `Allow: POST` - 单个请求体上限 8MB ## 持久化设计(分片) ``` write/delete → 修改内存 map → dirty=true ↓ 后台 goroutine 每 5s 检查 dirty,为 true 才保存 ↓ 流式写入 cache-0001.json.tmp (≤100MB, fsync) → rename → cache-0001.json → cache-0002.json … 写完后删除多余的旧分片 ``` - 单个分片最大 **100MB**,超过自动拆分;数据量小时只有一个 `cache-0001.json` - 每个分片独立 tmp+rename 原子替换,任意时刻磁盘上的分片都是完整 JSON - 保存后自动删除序号新增的旧分片(如 3 片变 2 片时删除 cache-0003.json) - 旧版单文件 `cache.json` 若存在:加载时自动回退读取,首次保存后自动迁移删除 数据文件格式(每条记录含 `created_at` / `expire_at` / `last_access`,`expire_at: 0` 表示永久): ```json { "task:10001": { "value": {"status": "running"}, "created_at": 1720000000, "expire_at": 1720003600, "last_access": 1720000100 }, "config:image_model": { "value": "z-image-turbo", "created_at": 1720000000, "expire_at": 0, "last_access": 1720000000 } } ``` ## TTL 与过期回收 - **写入**:带 `ttl` 字段(秒),`expire_at = now + ttl`;`ttl=0` 永久 - **读取惰性删除**:`/cache/read`、`/cache/exists`、`/cache/ttl` 碰到已过期 key 时立即删除并标记 dirty,读取返回 `expired:true` - **后台扫描**:每 60 秒全量扫描一次 map,删除所有过期 key(日志输出 `后台清理过期缓存 N 条`) - **启动过滤**:重启加载时跳过已过期与非法条目,不会进入内存 - **启动限流**:加载过程中内存达到 `max_memory` 即停止加载,输出 WARNING 日志 ## 分片加载与性能 - 启动时扫描 `data` 目录下所有 `cache-*.json`,**每文件一个 goroutine 并行读取解析**,主线程按分片序号从新到旧合并 - 跨分片重复 key 记 WARNING,后面的分片覆盖前面的;加载期间的运行期写入优先级最高 - 服务先启动、数据后台加载,期间 `/health` 返回 `loaded:false` - 读取始终只访问内存 map,分片机制不影响读写路径 - 实测:约 300MB / 9.7 万 key,重启并行加载 **842ms** 完成,key 数量与重启前完全一致 ## 内存限制与淘汰 - **占用估算**:每条 = `len(key) + len(value) + 48`(结构开销),`memory_usage` 在 write/delete/load 时维护 - **写入检查**:写入前若 `memory_usage + 新增 > max_memory`,先执行淘汰腾出空间;淘汰后仍放不下则拒绝写入并返回 `memory_limit` 错误 - **淘汰优先级**: 1. TTL 已过期数据 2. TTL 剩余时间最短的数据 3. 长期未访问的数据(`last_access` 最旧优先;每次 read 原子更新,不影响并行读) - **后台维护**:每 60 秒清理过期 key;若内存超过 80%,提前淘汰到水位以下 - 实测:10MB 上限持续写入 16MB 数据,4.5 万条全部写入成功,内存稳定在 100%,进程无 OOM ## FastAPI 调用示例 ```python import httpx KV = "http://127.0.0.1:9000" async def kv_write(key: str, value: dict): async with httpx.AsyncClient() as c: r = await c.post(f"{KV}/cache/write", json={"key": key, "value": value}) r.raise_for_status() async def kv_read(key: str): async with httpx.AsyncClient() as c: r = await c.post(f"{KV}/cache/read", json={"key": key}) data = r.json() return data["value"] if data["success"] else None ``` ## systemd 部署 ```bash # 1. 部署目录 sudo mkdir -p /opt/easy-cache sudo cp easy-cache kv-cache.service config/config.json -t /opt/easy-cache/ sudo mkdir -p /opt/easy-cache/data sudo cp data/cache.json /opt/easy-cache/data/ 2>/dev/null || echo '{}' | sudo tee /opt/easy-cache/data/cache.json # 2. 安装并启动 sudo cp /opt/easy-cache/kv-cache.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now kv-cache # 3. 查看状态与日志 sudo systemctl status kv-cache journalctl -u kv-cache -f ``` `kv-cache.service` 已配置:开机自启(`WantedBy=multi-user.target`)、崩溃自动重启(`Restart=always`,3s 间隔)、固定工作目录(`/opt/easy-cache`)。 ## 测试与性能 ```bash go test -race ./... # 全部单元测试 + 数据竞争检测 go test -bench=. -benchmem -run=^$ ./cache/ # 基准测试 ``` 基准测试参考(Apple M1, 1000 条数据): | 基准 | 结果 | 说明 | |---|---|---| | BenchmarkSet | ~600 ns/op | 单次写入,3 allocs | | BenchmarkSetWithTTL | ~420 ns/op | 带 TTL 写入 | | BenchmarkGet | ~64 ns/op | 单次读取,**0 allocs** | | BenchmarkGetParallel | ~101 ns/op | 多核并行读,RWMutex 读锁共享 | | BenchmarkSetParallel | ~865 ns/op | 并行写 | | BenchmarkMixedParallel | ~168 ns/op | 9读1写混合负载 | | BenchmarkTTLQuery | ~96 ns/op | TTL 查询,0 allocs | | BenchmarkCleanup | ~11 µs/op | 过期扫描(1000 条,0 allocs) | | BenchmarkSave | ~6.3 ms/op | 1000 条原子落盘(含 fsync),仅定时触发 | ## 日志 服务输出以下关键日志(前缀 `[kv-cache]`): - 服务启动 / 监听地址 - 启动时加载的缓存条数 - 定时保存成功 / 失败 - 后台清理过期缓存 N 条 / 内存超水位提前淘汰 N 条 - 加载超过内存上限的 WARNING / 分片间 key 重复 WARNING / 损坏分片跳过 WARNING - 分片保存成功(条数 → 分片数)、删除多余旧分片、旧单文件迁移 - API 错误(请求体解析失败、写入超内存限制、panic 等) - 收到退出信号、退出前保存、服务关闭