将Access Token存储在shelve对象中的方案存在哪些缺陷?
我们在开发一个用于编程学习的多微服务项目,主微服务使用Redis存储认证令牌效果良好,但有一个仅包含认证功能与几个API端点的极小FastAPI微服务,认为引入Redis作为依赖过于冗余,目前已使用Postgres数据库。
我们需要存储1小时后自动过期的Access Token,排除了难以测试维护的触发器和不符合预期的定时任务,最终选择将令牌存储在shelve中,并编写了包装器,在获取令牌时若发现其已过期则将其删除,代码如下:
import shelve from dal.utils import timezone_aware_now class ShelveDriver: def __init__(self): self._db = shelve.open("shelve_store", "c") def get(self, key): token = self._db.get(key, None) if token is None: return None if token["expire"] < timezone_aware_now(): del self._db[key] return self.get(key) return token def update(self, key, value): self._db[key] = value return True def delete(self, key): del self._db[key] return True def close(self): self._db.close() cache_driver = ShelveDriver()
该方案虽可正常运行,但存在以下诸多缺陷:
多进程/多线程安全风险:shelve底层依赖的dbm类库(如gdbm、bsddb)大多不支持多进程并发读写,而FastAPI默认使用的Uvicorn是多进程/多线程运行模式。多个请求同时操作
shelve_store文件时,极易出现数据损坏、读写冲突(比如一个进程删除key的同时另一个进程读取,可能拿到脏数据或抛出IO异常)。此外,全局的cache_driver实例会让每个进程各自打开文件副本,进一步加剧数据不一致问题。过期数据清理不彻底:只有当调用
get方法访问某个过期key时,才会触发删除操作。那些从未被访问的过期key会一直滞留在文件中,随着时间推移,文件体积会持续膨胀,既占用磁盘空间,又会拖慢后续读写的效率(因为需要遍历更多无效数据)。无事务支持导致数据不一致:shelve不提供事务机制,
get方法中"判断过期→删除key"的操作是非原子的。如果中间发生IO异常或进程崩溃,可能出现key已过期但未被删除的状态,导致数据逻辑不一致。全局实例的资源泄漏隐患:代码中创建了全局的
cache_driver实例,但未确保在FastAPI应用 shutdown 时调用close()方法。应用停止时,若未正确关闭shelve,可能导致数据未刷盘而丢失,或文件句柄未释放,重启时出现文件占用无法打开的问题。性能瓶颈明显:shelve是基于磁盘文件的存储,每次读写都涉及磁盘IO,相比Redis这类内存存储,性能差距极大。当令牌数量增多时,API响应延迟会显著上升。此外,
get方法中删除过期key后递归调用自身的逻辑,会额外增加一次磁盘IO,完全没必要。数据持久化可靠性不足:shelve的写入依赖底层dbm的缓存机制,调用
update后数据并非实时刷盘。如果此时进程意外崩溃,未刷盘的数据会直接丢失。缺乏监控与调试能力:不像Redis有成熟的监控工具可以查看缓存命中率、过期key数量等指标,shelve没有这类原生支持,排查性能问题或数据异常时会非常困难。
跨平台兼容性差:不同操作系统下shelve的底层实现不同(如Windows用bsddb,Linux用gdbm),生成的数据文件无法跨平台兼容,若项目需要跨环境部署,数据迁移会变得十分麻烦。
内容的提问来源于stack exchange,提问作者Bohdan Kholodenko

