配置管理
核心思路:配置表从数据库加载到内存(pandas DataFrame),运行时通过文件锁检测变更并热更新,避免每次请求都查库。
ConfigTable 模式
架构
核心实现
import pandas as pd
import json
import time
from pathlib import Path
from functools import wraps
from sqlmodel import create_engine
engine_config = create_engine('sqlite:///volume/dbs/config.db')
class ConfigTable:
def __init__(self):
self.path = Path('volume/dbs/update.lock')
if not self.path.exists():
self.path.touch()
self.update_time = self.path.stat().st_mtime
self.master_table = self._load_master_table()
# 启动时一次性加载所有配置表到内存
self.tables = {
tn: pd.read_sql(tn, engine_config, index_col=index_col)
if index_col else pd.read_sql(tn, engine_config)
for tn, index_col in self.master_table['index_col'].items()
}
@property
def config(self):
"""通过 tables.config['表名', '列名'] 访问"""
return self.master_table.loc
@staticmethod
def _load_master_table():
mt = pd.read_sql_table('table_master', engine_config, index_col='table_name')
mt['index_col'] = mt['index_col'].apply(json.loads)
mt['bool_col'] = mt['bool_col'].apply(json.loads)
return mt
@staticmethod
def ensure_new(func):
"""装饰器:访问前检查文件锁时间戳,有变化才重新加载"""
@wraps(func)
def wrapper(self, *args, **kwargs):
if self.path.stat().st_mtime != self.update_time:
new_master = self._load_master_table()
# 只重新加载发生变化的表(对比 update_time 列)
old_time = self.master_table['update_time']
new_time = new_master['update_time']
changed = new_time[new_time != old_time.reindex(new_time.index)].index
for tn in changed:
self.tables[tn] = pd.read_sql(
tn, engine_config,
index_col=new_master.loc[tn, 'index_col']
)
self.master_table = new_master
self.update_time = self.path.stat().st_mtime
return func(self, *args, **kwargs)
return wrapper
@ensure_new
def __getitem__(self, table_name: str) -> pd.DataFrame:
if table_name not in self.tables:
raise KeyError(f'Table {table_name} not found')
return self.tables[table_name]
tables = ConfigTable()
更新机制
通过 API 端点触发更新,从外部数据源拉取最新数据:
async def update_table(self, table_name: str):
# 1. 从外部源拉取数据
df = pd.read_csv(f'{source_url}/csv/{table_name}', ...)
# 2. 写入 SQLite(持久化)
df.to_sql(table_name, engine_config, index=True, if_exists='replace')
# 3. 更新 master_table 的时间戳
self.master_table.loc[table_name, 'update_time'] = int(time.time() * 1000)
self.master_table.to_sql('master_table', engine_config, ...)
# 4. 更新内存中的表
self.tables[table_name] = df
# 5. 触摸文件锁,通知其他进程
self.path.touch()
self.update_time = self.path.stat().st_mtime
safe_at 安全取值(带回退)
注册 pandas accessor,当索引不存在时按回退规则查找:
@register_dataframe_accessor("safe_at")
class SafeLocAccessor:
# 回退规则:索引找不到时,尝试这些默认值
fallback_rules = {
'game_cd': 0, # 找不到特定游戏配置 → 用默认配置
'lang': 'en', # 找不到特定语言 → 用英语
}
def __getitem__(self, key: tuple):
k0, k1 = key # (索引值, 列名)
if k0 in self._df.index:
return self._df.at[key]
# 多级索引:用笛卡尔积遍历所有回退组合
...
使用方式:
# 直接取值(可能 KeyError)
value = tables['prompt_config'].at['game_a', 'system_prompt']
# 安全取值(自动回退)
value = tables['prompt_config'].safe_at['game_a', 'system_prompt']
# 如果 game_a 不存在,自动尝试 game_cd=0 的默认配置
简单场景:启动时加载到全局字典
对于不需要热更新的简单配置,直接在 lifespan 中加载:
# 全局缓存
game_expire_dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时从API加载
response = await client.get(f'{api_url}/config')
game_expire_dict.update(response.json())
yield
何时用哪种
| 场景 | 方案 |
|---|---|
| 简单、很少变的配置 | 全局字典,启动时加载 |
| 需要运行时更新、多表管理 | ConfigTable 模式 |
| 需要多进程同步 | ConfigTable + 文件锁 |