ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

authentik 中的 django-postgres-cache:基于 PostgreSQL 的 Django 缓存后端与迁移依赖实战

authentik 中的 django-postgres-cache:基于 PostgreSQL 的 Django 缓存后端与迁移依赖实战 authentik 中的 django-postgres-cache基于 PostgreSQL 的 Django 缓存后端与迁移依赖实战【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik导读packages/django-postgres-cache是 authentik 项目内置的一个独立 Python 包它把 Django 的缓存抽象层django.core.cache重新实现为一张 PostgreSQL 表并为该表提供了keys()通配符枚举、on_conflict批量写入等增强能力。本文以该包 README.md 为核心围绕「使用缓存的迁移必须依赖缓存建表迁移」这一关键约束展开并结合 backend.py、models.py、tasks.py 等源码以及 authentik 主项目中的真实用法讲清其工作原理、配置方式与迁移依赖的完整实战要点。一、包概览一个「库中库」的 Django 应用从目录结构看django-postgres-cache是一个可独立分发、也可作为 Django app 安装的包packages/django-postgres-cache/ ├── README.md ├── pyproject.toml ├── django_postgres_cache/ │ ├── __init__.py # 空文件仅标记为 Python 包 │ ├── apps.py # Django AppConfigdjango_postgres_cache │ ├── backend.py # DatabaseCache 缓存后端核心实现 │ ├── models.py # CacheEntry 模型PostgresManager │ ├── tasks.py # 清理过期缓存条目的定时任务入口 │ ├── tests.py # _make_expiry 时区回归测试 │ └── migrations/ │ ├── 0001_initial.py # 创建 CacheEntry 表 │ └── 0002_alter_cacheentry_managers.py # 切换为 PostgresManager └── tests/ └── test_keys_method.py # keys() 的 glob→SQL 翻译单元测试包元数据pyproject.toml显示其定位与约束版本0.1.0状态为 AlphaDevelopment Status :: 3 - Alpha描述为Improved Django Postgres Cache依赖django 4.2,6.0与django-postgres-extra 2.0,2.1——后者提供了PostgresManager、on_conflict、bulk_insert、truncate等 PostgreSQL 特有 ORM 能力支持 Python 3.93.14分类器覆盖 Django 4.2 / 5.0 / 5.1 / 5.2。在 authentik 主项目中它被注册为 INSTALLED_APPS 之一见 authentik/root/settings.py并作为默认缓存后端配置见 authentik/root/settings.py。二、核心模型与存储结构CacheEntry 表缓存条目的落库结构定义在 models.pyclass CacheEntry(models.Model): cache_key models.TextField(primary_keyTrue) value models.TextField() expires models.DateTimeField(db_indexTrue) objects PostgresManager()三个字段各有深意cache_key直接作为主键Text 主键这意味着同一 key 天然唯一配合on_conflict即可实现幂等 upsertvalue以Text 而非 BinaryField存储因此在写入前必须经过 base64 编码见下文_make_valueexpires带db_index过期筛选expires__gtenow()可以走索引是get/has_key/get_many高频查询的性能关键。模型还设置了default_permissions []即不生成 add/change/delete/view 默认权限避免该内部表污染权限体系。表结构由迁移 0001_initial.py 创建0002_alter_cacheentry_managers.py 则将 manager 切换为PostgresManager从而启用on_conflict/bulk_insert/truncate等增强方法。三、README 核心主题使用缓存的迁移必须依赖建表迁移包自带的 README 只讲了一件事而这件事恰恰是最容易踩坑的点——迁移时序。原文如下Migrations that use the cache with this installed need to depend on the migration to create the cache entry table.即任何在迁移migrations.RunPython或RunSQL中使用该缓存的迁移都必须把django_postgres_cache的初始迁移声明为依赖项否则迁移执行顺序无法保证可能在CacheEntry表尚未创建时就去读写缓存直接触发django.db.utils.ProgrammingError: relation django_postgres_cache_cacheentry does not exist。标准写法原文示例dependencies [ # ...other requirements (django_postgres_cache, 0001_initial), ]3.1 authentik 主项目中的真实范例在 authentik 源码中authentik/core/migrations/0054_alter_application_meta_icon_alter_source_icon.py 正是这样实践的def clear_app_cache(apps, schema_editor): CacheEntry apps.get_model(django_postgres_cache, CacheEntry) CacheEntry.objects.filter(cache_key__startswithgoauthentik.io/apps/).delete() class Migration(migrations.Migration): dependencies [ (authentik_core, 0053_...), (django_postgres_cache, 0001_initial), ] operations [migrations.RunPython(clear_app_cache), ...]该迁移在数据迁移中直接通过历史模型CacheEntry按 key 前缀清理缓存并把(django_postgres_cache, 0001_initial)加入依赖从而保证在迁移执行时缓存表一定已就绪。这是 README 约束在真实项目中的直接落地。3.2 为什么依赖节点是0001_initial而不是其他依赖必须指向建表迁移即0001_initial。0002_alter_cacheentry_managers只是把 manager 换成PostgresManager不影响表结构是否存在而0001_initial中的migrations.CreateModel负责真正创建CacheEntry表。若只依赖0002Django 的迁移图依然会保证0001先于0002执行但语义上显式依赖建表迁移更清晰、更贴近 README 的原始要求。3.3 迁移中使用缓存的两种方式通过历史模型直查CacheEntry如上例绕过缓存后端直接用 ORM 按cache_key过滤/删除适合做「按前缀批量失效」这类后端 API 做不到的事通过django.core.cache调用后端迁移里若调用cache.set(...)会走 backend.py 的完整实现同样要求表已存在因此依赖项必不可少。四、后端实现剖析从DatabaseCache看每个操作的 SQL 语义backend.py 中的DatabaseCache(BaseCache)实现了 DjangoBaseCache的全部标准接口但其底层全部映射为对CacheEntry表的高效 SQL 操作。4.1 值的序列化与反序列化def _make_value(self, value): pickled pickle.dumps(value, self.pickle_protocol) b64encoded base64.b64encode(pickled).decode(latin1) return b64encoded def _unmake_value(self, encoded_value): return pickle.loads(base64.b64decode(encoded_value.encode()))值用pickleHIGHEST_PROTOCOL序列化再经base64 编码为 str后存入 Text 列——源码注释明确说明这是为了兼容 Django 的Refs #19274问题DB 列期望字符串而非 bytes。_make_value使用latin1解码保证 bytes 到 str 的往返无损。4.2 过期时间的时区处理def _make_expiry(self, timeout): tz UTC if settings.USE_TZ else None timeout self.get_backend_timeout(timeout) if timeout is None: exp datetime.max.replace(tzinfotz) else: exp datetime.fromtimestamp(timeout, tztz) return exp.replace(microsecond0)timeoutNone永不过期映射为datetime.max同时微秒被截断为 0。这里有一个值得注意的细节tests.py 中的MakeExpiryTests记录了历史 bug——早期实现即使USE_TZTrue也会返回 naive 的datetime.max导致 Django 保存时抛出RuntimeWarning现在的实现会根据USE_TZ决定tzinfoUTC还是None保证返回的 datetime 与配置一致。4.3 核心操作的 SQL 策略操作底层 SQL 策略说明add先DELETE ... expires__ltenow()再INSERT失败返回False利用主键唯一性插入失败如并发冲突即视为「已存在」无事务开销setINSERT ... ON CONFLICT (cache_key) DO UPDATEon_conflictConflictAction.UPDATE原子 upsert避免先查后写的竞态touchUPDATE expires返回受影响行数是否 0刷新过期时间getWHERE cache_key? AND expires__gtenow()取第一条天然过滤过期条目get_many/delete_manyIN (...)批量查询/删除一次往返处理多个 keyset_manybulk_insert(... ON CONFLICT DO UPDATE)单条 SQL 批量 upsertclearCacheEntry.objects.truncate()直接 TRUNCATE 整表比逐行 DELETE 快得多has_keyEXISTS(...)短路判断所有「读」操作都带expires__gtenow()过滤因此读路径上过期条目会被自动忽略而清理工作则交给后台任务见下文第五节。4.4keys()方法glob 模式到 SQL 的智能翻译keys(keys_pattern)是标准 Django 数据库缓存后端没有的能力用于按模式枚举缓存键def keys(self, keys_pattern, versionNone): wildcard_count keys_pattern.count(*) if wildcard_count 0: key self.make_key(keys_pattern, versionversion) qs CacheEntry.objects.filter(cache_keykey) # 主键等值 elif wildcard_count 1 and keys_pattern.endswith(*): prefix self.make_key(keys_pattern[:-1], versionversion) qs CacheEntry.objects.filter(cache_key__startswithprefix) # LIKE prefix% else: regex self.make_key(keys_pattern.replace(*, .*), versionversion) qs CacheEntry.objects.filter(cache_key__regexregex) # 正则回退 return [self.reverse_key_func(key) for key in qs.values_list(cache_key, flatTrue)]翻译规则及性能考量无通配符退化为主键等值查询速度最快单个后缀通配符prefix*包括纯*使用__startswith生成LIKE prefix%可被cache_key上的 B-tree 主键索引回答即使无索引也比正则路径便宜得多复杂模式通配符不在末尾、或多个通配符如foo*bar*、*foo回退到__regex。tests/test_keys_method.py用 mock 方式验证了这套翻译逻辑真实场景goauthentik.io/policies/app_access/*authentik 策略引擎的缓存键前缀必须走__startswith而非__regex这正是针对线上热查询路径的回归保护。注意返回前会调用reverse_key_func把「带前缀/版本的完整存储 key」还原为调用方传入的原始 key——该函数由配置项REVERSE_KEY_FUNCTION指定。五、过期条目的清理机制读路径自动忽略过期条目但过期数据不会自动删除需要显式清理。包内 tasks.py 提供了def clear_expired_cache() - None: for obj in chunked_queryset(CacheEntry.objects.filter(expires__ltnow())): obj.delete()chunked_queryset来自 authentik 主项目的 authentik/lib/utils/db.py该文件位于 authentik/lib/utils 目录下用于分块遍历避免一次载入全部过期行在 authentik 中此函数被 authentik/core/tasks.py 导入作为周期性 Celery 任务调度执行防止缓存表无限膨胀源码注释也坦承该函数目前依赖主项目的工具函数FIXME: do we copy it here to make it independent?说明该包仍在演进中。六、在 authentik 中的真实配置与调用点6.1 缓存配置authentik/root/settings.py 中CACHES { default: { BACKEND: django_postgres_cache.backend.DatabaseCache, REVERSE_KEY_FUNCTION: django_tenants.cache.reverse_key, # ... }, }REVERSE_KEY_FUNCTION指向django_tenants.cache.reverse_key——因为 authentik 使用多租户tenants每个租户的 key 都会带租户前缀keys()枚举结果必须通过该函数还原出无前缀的原始 key。这解释了为什么 backend 在__init__中通过get_key_func(params[REVERSE_KEY_FUNCTION])解析这个自定义配置项backend.py。6.2 实际调用点authentik/events/middleware.py 直接导入CacheEntry模型authentik/policies/tests/test_engine_filter.py 的注释确认了「此处缓存后端为 DB-backeddjango_postgres_cache因此缓存写入是真实 DB 写」——这意味着测试中调用策略缓存会真实写入 PostgreSQL 表策略引擎的访问控制缓存 key 形如goauthentik.io/policies/app_access/...即 4.4 节测试用例中的真实前缀。七、落地 checklist把 README 的约束变成工程实践依赖声明任何在RunPython/RunSQL中读写该缓存的迁移dependencies必须包含(django_postgres_cache, 0001_initial)确认表名迁移生成的默认表名为django_postgres_cache_cacheentryapp_label_ 模型名报错信息中出现该表名时优先检查迁移依赖优先0001_initial依赖指向建表迁移避免与0002的 manager 切换迁移产生歧义迁移内清理用历史模型直查参考 0054 号迁移用CacheEntry.objects.filter(cache_key__startswith...)按前缀批量失效过期清理交给周期任务不要在生产代码里手动逐条删过期数据复用 tasks.py 的clear_expired_cache接入调度系统。结语django-postgres-cache虽小却集中体现了三类值得借鉴的工程思路用一张 Text 主键表承载整个 Django 缓存协议、借助ON CONFLICT与TRUNCATE把缓存操作压成单条 SQL、以及用迁移依赖图严格约束「数据操作必须先于建表」。README 中那一句迁移依赖要求背后是运行时可能直接报错的表缺失问题——本文结合 authentik 源码给出了完整依据与可复制的范例读者在自己的 Django 项目中按第七节的 checklist 落地即可避免同类陷阱。【免费下载链接】authentikThe authentication glue you need.项目地址: https://gitcode.com/GitHub_Trending/au/authentik创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表