Skip to content

Commit 82b01d2

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 72e4dab + f0c9bad commit 82b01d2

29 files changed

+203
-133
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ LOG_DIR="/opt/sqlbot/logs"
2121
LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
2222
SQL_DEBUG=False
2323

24+
CACHE_TYPE="memory"
25+
CACHE_REDIS_URL="redis://127.0.0.1:6379"
26+
2427
# Postgres
2528
POSTGRES_SERVER=localhost
2629
POSTGRES_PORT=5432

backend/common/core/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ def all_cors_origins(self) -> list[str]:
6161
DEFAULT_PWD: str = "SQLBot@123456"
6262
ASSISTANT_TOKEN_KEY: str = "X-SQLBOT-ASSISTANT-TOKEN"
6363

64+
CACHE_TYPE: Literal["redis", "memory", "None"] = "memory"
65+
CACHE_REDIS_URL: str | None = None # Redis URL, e.g., "redis://[[username]:[password]]@localhost:6379/0"
66+
6467
LOG_LEVEL: str = "INFO" # DEBUG, INFO, WARNING, ERROR
6568
LOG_DIR: str = "logs"
6669
LOG_FORMAT: str = "%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
Lines changed: 44 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,12 @@
1-
import contextvars
21
from fastapi_cache import FastAPICache
32
from functools import partial, wraps
43
from typing import Optional, Any, Dict, Tuple
54
from inspect import signature
6-
from contextlib import asynccontextmanager
7-
import asyncio
8-
import random
9-
from collections import defaultdict
10-
11-
from apps.system.schemas.auth import CacheName
12-
from apps.system.schemas.system_schema import UserInfoDTO
5+
from common.core.config import settings
136
from common.utils.utils import SQLBotLogUtil
7+
from fastapi_cache.backends.inmemory import InMemoryBackend
148

15-
# 使用contextvar来跟踪当前线程已持有的锁
16-
_held_locks = contextvars.ContextVar('held_locks', default=set())
17-
# 高效锁管理器
18-
class LockManager:
19-
_locks = defaultdict(asyncio.Lock)
20-
21-
@classmethod
22-
def get_lock(cls, key: str) -> asyncio.Lock:
23-
return cls._locks[key]
24-
25-
@asynccontextmanager
26-
async def _get_cache_lock(key: str):
27-
# 获取当前已持有的锁集合
28-
current_locks = _held_locks.get()
29-
30-
# 如果已经持有这个锁,直接yield(锁传递)
31-
if key in current_locks:
32-
yield
33-
return
34-
35-
# 否则获取锁并添加到当前上下文中
36-
lock = LockManager.get_lock(key)
37-
try:
38-
await lock.acquire()
39-
# 更新当前持有的锁集合
40-
new_locks = current_locks | {key}
41-
token = _held_locks.set(new_locks)
42-
43-
yield
44-
45-
finally:
46-
# 恢复之前的锁集合
47-
_held_locks.reset(token)
48-
if lock.locked():
49-
lock.release()
9+
from fastapi_cache.decorator import cache as original_cache
5010

5111
def custom_key_builder(
5212
func: Any,
@@ -93,7 +53,6 @@ def cache(
9353
*,
9454
cacheName: str, # 必须提供cacheName
9555
keyExpression: Optional[str] = None,
96-
jitter: int = 60, # 默认抖动60秒
9756
):
9857
def decorator(func):
9958
# 预先生成key builder
@@ -105,40 +64,22 @@ def decorator(func):
10564

10665
@wraps(func)
10766
async def wrapper(*args, **kwargs):
67+
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
68+
return await func(*args, **kwargs)
10869
# 生成缓存键
10970
cache_key = used_key_builder(
11071
func=func,
111-
namespace=namespace,
72+
namespace=str(namespace) if namespace else "",
11273
args=args,
11374
kwargs=kwargs
11475
)
11576

116-
# 防击穿锁
117-
async with _get_cache_lock(cache_key):
118-
backend = FastAPICache.get_backend()
119-
120-
# 双重检查
121-
if (cached := await backend.get(cache_key)) is not None:
122-
SQLBotLogUtil.debug(f"Cache hit: {cache_key}")
123-
if CacheName.USER_INFO.value in cache_key:
124-
user = UserInfoDTO.model_validate(cached)
125-
SQLBotLogUtil.info(f"User cache hit: [uid: {user.id}, account: {user.account}, oid: {user.oid}]")
126-
return cached
127-
128-
# 执行函数并缓存结果
129-
result = await func(*args, **kwargs)
130-
131-
actual_expire = expire + random.randint(-jitter, jitter)
132-
if await backend.get(cache_key):
133-
await backend.clear(cache_key)
134-
await backend.set(cache_key, result, actual_expire)
135-
136-
SQLBotLogUtil.debug(f"Cache set: {cache_key} (expire: {actual_expire}s)")
137-
if CacheName.USER_INFO.value in cache_key:
138-
user = UserInfoDTO.model_validate(result)
139-
SQLBotLogUtil.info(f"User cache set: [uid: {user.id}, account: {user.account}, oid: {user.oid}]")
140-
return result
141-
77+
return await original_cache(
78+
expire=expire,
79+
namespace=str(namespace) if namespace else "",
80+
key_builder=lambda *_, **__: cache_key
81+
)(func)(*args, **kwargs)
82+
14283
return wrapper
14384
return decorator
14485

@@ -148,30 +89,47 @@ def clear_cache(
14889
cacheName: str,
14990
keyExpression: Optional[str] = None,
15091
):
151-
"""精确清除单个缓存项的装饰器"""
15292
def decorator(func):
15393
@wraps(func)
15494
async def wrapper(*args, **kwargs):
95+
if not settings.CACHE_TYPE or settings.CACHE_TYPE.lower() == "none":
96+
return await func(*args, **kwargs)
15597
cache_key = custom_key_builder(
15698
func=func,
157-
namespace=namespace,
99+
namespace=str(namespace) if namespace else "",
158100
args=args,
159101
kwargs=kwargs,
160102
cacheName=cacheName,
161103
keyExpression=keyExpression,
162104
)
163-
164-
# 加锁防止竞争
165-
async with _get_cache_lock(cache_key):
166-
backend = FastAPICache.get_backend()
167-
result = None
168-
if await backend.get(cache_key):
169-
await backend.clear(cache_key)
170-
result = await func(*args, **kwargs)
171-
if await backend.get(cache_key):
172-
await backend.clear(cache_key)
173-
SQLBotLogUtil.info(f"Cache cleared: {cache_key}")
174-
return result
105+
backend = FastAPICache.get_backend()
106+
if await backend.get(cache_key):
107+
if settings.CACHE_TYPE.lower() == "redis":
108+
redis = backend.redis
109+
await redis.delete(cache_key)
110+
else:
111+
await backend.clear(key=cache_key)
112+
SQLBotLogUtil.debug(f"Cache cleared: {cache_key}")
113+
return await func(*args, **kwargs)
175114

176115
return wrapper
177-
return decorator
116+
return decorator
117+
118+
119+
def init_sqlbot_cache():
120+
cache_type: str = settings.CACHE_TYPE
121+
if cache_type == "memory":
122+
FastAPICache.init(InMemoryBackend())
123+
SQLBotLogUtil.info("SQLBot 使用内存缓存, 仅支持单进程模式")
124+
elif cache_type == "redis":
125+
from fastapi_cache.backends.redis import RedisBackend
126+
import redis.asyncio as redis
127+
from redis.asyncio.connection import ConnectionPool
128+
redis_url = settings.CACHE_REDIS_URL or "redis://localhost:6379/0"
129+
pool = ConnectionPool.from_url(url=redis_url)
130+
redis_client = redis.Redis(connection_pool=pool)
131+
FastAPICache.init(RedisBackend(redis_client), prefix="sqlbot-cache")
132+
SQLBotLogUtil.info(f"SQLBot 使用Redis缓存, 可使用多进程模式")
133+
else:
134+
SQLBotLogUtil.warning("SQLBot 未启用缓存, 可使用多进程模式")
135+

backend/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@
1414
from fastapi_mcp import FastApiMCP
1515
from fastapi.staticfiles import StaticFiles
1616
import sqlbot_xpack
17-
from fastapi_cache import FastAPICache
18-
from fastapi_cache.backends.inmemory import InMemoryBackend
1917

2018
from common.utils.utils import SQLBotLogUtil
19+
from common.core.sqlbot_cache import init_sqlbot_cache
2120

2221
def run_migrations():
2322
alembic_cfg = Config("alembic.ini")
@@ -27,7 +26,7 @@ def run_migrations():
2726
@asynccontextmanager
2827
async def lifespan(app: FastAPI):
2928
run_migrations()
30-
FastAPICache.init(InMemoryBackend())
29+
init_sqlbot_cache()
3130
init_dynamic_cors(app)
3231
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
3332
yield

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies = [
3939
"sqlbot-xpack==0.0.3.7",
4040
"fastapi-cache2>=0.2.2",
4141
"sqlparse>=0.5.3",
42+
"redis>=6.2.0",
4243
]
4344
[[tool.uv.index]]
4445
name = "default"
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 6 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)