🐛 修复数据库超时问题

This commit is contained in:
HibiKier 2025-08-22 02:43:19 +08:00
parent a63f26c3b6
commit 8d8cbf20c3
6 changed files with 70 additions and 11 deletions

View File

@ -4,6 +4,7 @@ from typing_extensions import Self
from tortoise import fields from tortoise import fields
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.services.log import logger from zhenxun.services.log import logger
from zhenxun.utils.enum import CacheType, DbLockType from zhenxun.utils.enum import CacheType, DbLockType
@ -57,14 +58,15 @@ class BanConsole(Model):
""" """
if not user_id and not group_id: if not user_id and not group_id:
raise UserAndGroupIsNone() raise UserAndGroupIsNone()
dao = DataAccess(cls)
if user_id: if user_id:
return ( return (
await cls.safe_get_or_none(user_id=user_id, group_id=group_id) await dao.safe_get_or_none(user_id=user_id, group_id=group_id)
if group_id if group_id
else await cls.safe_get_or_none(user_id=user_id, group_id__isnull=True) else await dao.safe_get_or_none(user_id=user_id, group_id__isnull=True)
) )
else: else:
return await cls.safe_get_or_none(user_id="", group_id=group_id) return await dao.safe_get_or_none(user_id="", group_id=group_id)
@classmethod @classmethod
async def check_ban_level( async def check_ban_level(

View File

@ -7,6 +7,7 @@ from tortoise.backends.base.client import BaseDBAsyncClient
from zhenxun.models.plugin_info import PluginInfo from zhenxun.models.plugin_info import PluginInfo
from zhenxun.models.task_info import TaskInfo from zhenxun.models.task_info import TaskInfo
from zhenxun.services.cache import CacheRoot from zhenxun.services.cache import CacheRoot
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import Model from zhenxun.services.db_context import Model
from zhenxun.utils.enum import CacheType, DbLockType, PluginType from zhenxun.utils.enum import CacheType, DbLockType, PluginType
@ -254,13 +255,14 @@ class GroupConsole(Model):
返回: 返回:
Self: GroupConsole Self: GroupConsole
""" """
dao = DataAccess(cls)
if channel_id: if channel_id:
return await cls.safe_get_or_none( return await dao.safe_get_or_none(
group_id=group_id, group_id=group_id,
channel_id=channel_id, channel_id=channel_id,
clean_duplicates=clean_duplicates, clean_duplicates=clean_duplicates,
) )
return await cls.safe_get_or_none( return await dao.safe_get_or_none(
group_id=group_id, group_id=group_id,
channel_id__isnull=True, channel_id__isnull=True,
clean_duplicates=clean_duplicates, clean_duplicates=clean_duplicates,

View File

@ -37,7 +37,7 @@ class CacheDict(Generic[T]):
return 0 return 0
return data.expire_time return data.expire_time
def __getitem__(self, key: str) -> T | None: def __getitem__(self, key: str) -> T:
"""获取字典项 """获取字典项
参数: 参数:
@ -47,8 +47,10 @@ class CacheDict(Generic[T]):
T: 字典值 T: 字典值
""" """
if value := self._data.get(key): if value := self._data.get(key):
return value.value if self.expire_time(key) else None if self.expire_time(key):
return None raise KeyError(f"{key} 已过期")
return value.value
raise KeyError(f"{key} 不存在")
def __setitem__(self, key: str, value: T) -> None: def __setitem__(self, key: str, value: T) -> None:
"""设置字典项 """设置字典项

View File

@ -7,6 +7,8 @@ from zhenxun.services.log import logger
T = TypeVar("T", bound=Model) T = TypeVar("T", bound=Model)
cache = CacheRoot.cache_dict("DB_TEST_BAN", 10, int)
class DataAccess(Generic[T]): class DataAccess(Generic[T]):
"""数据访问层,根据配置决定是否使用缓存 """数据访问层,根据配置决定是否使用缓存
@ -167,6 +169,7 @@ class DataAccess(Generic[T]):
return await with_db_timeout( return await with_db_timeout(
db_query_func(*args, **kwargs), db_query_func(*args, **kwargs),
operation=f"{self.model_cls.__name__}.{db_query_func.__name__}", operation=f"{self.model_cls.__name__}.{db_query_func.__name__}",
source="DataAccess",
) )
# 尝试从缓存获取 # 尝试从缓存获取
@ -178,10 +181,11 @@ class DataAccess(Generic[T]):
# 如果成功构建缓存键,尝试从缓存获取 # 如果成功构建缓存键,尝试从缓存获取
if cache_key is not None: if cache_key is not None:
data = await self.cache.get(cache_key) data = await self.cache.get(cache_key)
logger.debug( logger.info(
f"{self.model_cls.__name__} self.cache.get(cache_key)" f"{self.model_cls.__name__} key: {cache_key}"
f" 从缓存获取到的数据 {type(data)}: {data}" f" 从缓存获取到的数据 {type(data)}: {data}"
) )
if data == self._NULL_RESULT: if data == self._NULL_RESULT:
# 空结果缓存命中 # 空结果缓存命中
self._cache_stats[self.cache_type]["null_hits"] += 1 self._cache_stats[self.cache_type]["null_hits"] += 1
@ -193,6 +197,13 @@ class DataAccess(Generic[T]):
f"{self.model_cls.__name__} 从缓存获取" f"{self.model_cls.__name__} 从缓存获取"
f"到空结果: {cache_key}, 允许数据不存在返回None" f"到空结果: {cache_key}, 允许数据不存在返回None"
) )
if self.model_cls.__name__ == "BanConsole":
uid = kwargs.get("user_id")
if uid:
logger.info(
f"查询ban: {cache_key}:{uid}数据不存在返回NULL结果",
"DB_TEST__BAN",
)
return None return None
elif data: elif data:
# 缓存命中 # 缓存命中

View File

@ -16,6 +16,9 @@ from zhenxun.utils.enum import DbLockType
from .config import LOG_COMMAND, db_model from .config import LOG_COMMAND, db_model
from .utils import with_db_timeout from .utils import with_db_timeout
total = {}
index = 0
class Model(TortoiseModel): class Model(TortoiseModel):
""" """
@ -221,12 +224,25 @@ class Model(TortoiseModel):
返回: 返回:
Self | None: 查询结果如果不存在返回None Self | None: 查询结果如果不存在返回None
""" """
global index, total
try: try:
if cls.__name__ == "BanConsole":
uid = kwargs.get("user_id")
if uid:
if uid not in total:
total[uid] = CacheRoot.cache_dict(f"DB_TEST_UID_{uid}", 10, int)
total[uid][str(index)] = "1"
index += 1
logger.info(
f"BanConsole 10秒内查询次数 uid: {uid}"
f" 查询次数: {len(total[uid])}"
)
# 先尝试使用 get_or_none 获取单个记录 # 先尝试使用 get_or_none 获取单个记录
try: try:
return await with_db_timeout( return await with_db_timeout(
cls.get_or_none(*args, using_db=using_db, **kwargs), cls.get_or_none(*args, using_db=using_db, **kwargs),
operation=f"{cls.__name__}.get_or_none", operation=f"{cls.__name__}.get_or_none",
source="DataBaseModel",
) )
except MultipleObjectsReturned: except MultipleObjectsReturned:
# 如果出现多个记录的情况,进行特殊处理 # 如果出现多个记录的情况,进行特殊处理
@ -239,6 +255,7 @@ class Model(TortoiseModel):
records = await with_db_timeout( records = await with_db_timeout(
cls.filter(*args, **kwargs).all(), cls.filter(*args, **kwargs).all(),
operation=f"{cls.__name__}.filter.all", operation=f"{cls.__name__}.filter.all",
source="DataBaseModel",
) )
if not records: if not records:
@ -255,6 +272,7 @@ class Model(TortoiseModel):
await with_db_timeout( await with_db_timeout(
record.delete(), record.delete(),
operation=f"{cls.__name__}.delete_duplicate", operation=f"{cls.__name__}.delete_duplicate",
source="DataBaseModel",
) )
logger.info( logger.info(
f"{cls.__name__} 删除重复记录:" f"{cls.__name__} 删除重复记录:"
@ -269,11 +287,13 @@ class Model(TortoiseModel):
return await with_db_timeout( return await with_db_timeout(
cls.filter(*args, **kwargs).order_by("-id").first(), cls.filter(*args, **kwargs).order_by("-id").first(),
operation=f"{cls.__name__}.filter.order_by.first", operation=f"{cls.__name__}.filter.order_by.first",
source="DataBaseModel",
) )
# 如果没有 id 字段,则返回第一个记录 # 如果没有 id 字段,则返回第一个记录
return await with_db_timeout( return await with_db_timeout(
cls.filter(*args, **kwargs).first(), cls.filter(*args, **kwargs).first(),
operation=f"{cls.__name__}.filter.first", operation=f"{cls.__name__}.filter.first",
source="DataBaseModel",
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
logger.error( logger.error(

View File

@ -1,6 +1,7 @@
import asyncio import asyncio
import time import time
from zhenxun.services.cache import CacheRoot
from zhenxun.services.log import logger from zhenxun.services.log import logger
from .config import ( from .config import (
@ -9,14 +10,35 @@ from .config import (
SLOW_QUERY_THRESHOLD, SLOW_QUERY_THRESHOLD,
) )
cache = CacheRoot.cache_dict("DB_TEST", 10)
index = 0
total = {}
async def with_db_timeout( async def with_db_timeout(
coro, timeout: float = DB_TIMEOUT_SECONDS, operation: str | None = None coro,
timeout: float = DB_TIMEOUT_SECONDS,
operation: str | None = None,
source: str | None = None,
): ):
"""带超时控制的数据库操作""" """带超时控制的数据库操作"""
global index, total, index2
start_time = time.time() start_time = time.time()
try: try:
if operation not in total:
total[operation] = CacheRoot.cache_dict(f"DB_TEST_{operation}", 10)
total[operation][str(index)] = "1"
index += 1
logger.info(f"当前数据库查询来源: {source}")
logger.info(
f"10秒内数据库查询次数 [{operation}]: {len(total[operation])}", "DB_TEST"
)
result = await asyncio.wait_for(coro, timeout=timeout) result = await asyncio.wait_for(coro, timeout=timeout)
cache[str(index)] = "1"
index += 1
elapsed = time.time() - start_time elapsed = time.time() - start_time
if elapsed > SLOW_QUERY_THRESHOLD and operation: if elapsed > SLOW_QUERY_THRESHOLD and operation:
logger.warning(f"慢查询: {operation} 耗时 {elapsed:.3f}s", LOG_COMMAND) logger.warning(f"慢查询: {operation} 耗时 {elapsed:.3f}s", LOG_COMMAND)