database.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. """
  2. 数据库连接管理模块(SQLAlchemy 版本)
  3. """
  4. import asyncio
  5. import time
  6. import traceback
  7. from typing import Optional
  8. from urllib.parse import quote_plus
  9. import redis.asyncio as aioredis
  10. from sqlalchemy.ext.asyncio import (
  11. create_async_engine,
  12. AsyncSession,
  13. async_sessionmaker,
  14. AsyncEngine
  15. )
  16. from sqlalchemy.orm import declarative_base
  17. from config import settings
  18. import logging
  19. logger = logging.getLogger(__name__)
  20. class DatabaseManager:
  21. """数据库连接管理器(MySQL + Redis)"""
  22. def __init__(self):
  23. self.engine: Optional[AsyncEngine] = None
  24. self.async_session_maker: Optional[async_sessionmaker] = None
  25. self.redis_client: Optional[aioredis.Redis] = None
  26. self._redis_lock = asyncio.Lock()
  27. self._last_redis_healthcheck: float = 0.0
  28. self._healthcheck_task: Optional[asyncio.Task] = None
  29. self._healthcheck_running = False
  30. async def connect(self):
  31. """初始化数据库连接"""
  32. try:
  33. await self._create_mysql_engine()
  34. await self._create_redis_client()
  35. # 🔥 启动后台健康检查任务
  36. self._start_healthcheck_task()
  37. except Exception as e:
  38. logger.error(f"❌ 数据库连接失败: {e}")
  39. raise
  40. async def _create_mysql_engine(self):
  41. """创建 SQLAlchemy 异步引擎"""
  42. # 构建 MySQL 连接 URL
  43. encoded_user = quote_plus(settings.MYSQL_USER)
  44. encoded_password = quote_plus(settings.MYSQL_PASSWORD)
  45. mysql_url = (
  46. f"mysql+aiomysql://{encoded_user}:{encoded_password}"
  47. f"@{settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}"
  48. f"?charset=utf8mb4"
  49. )
  50. # 创建异步引擎(自动连接池 + 健康检查 + 超时配置)
  51. self.engine = create_async_engine(
  52. mysql_url,
  53. pool_size=settings.MYSQL_POOL_SIZE,
  54. max_overflow=settings.MYSQL_MAX_OVERFLOW,
  55. pool_pre_ping=True, # 🔥 自动健康检查(类似 ping)
  56. pool_recycle=settings.MYSQL_POOL_RECYCLE, # 连接回收
  57. pool_timeout=5.0, # 🔥 连接池获取连接超时(秒),避免长时间等待
  58. echo=False, # 生产环境关闭 SQL 日志
  59. # echo_pool="debug", # ⭐ 新增:打印连接借出/归还的详细日志
  60. future=True,
  61. connect_args={
  62. "connect_timeout": settings.MYSQL_CONNECT_TIMEOUT, # 🔥 连接超时(aiomysql 支持)
  63. # ⚠️ read_timeout 已移除:aiomysql 不支持此参数,读取超时由 pool_timeout 控制
  64. },
  65. )
  66. # 创建异步 Session 工厂
  67. self.async_session_maker = async_sessionmaker(
  68. self.engine,
  69. class_=AsyncSession,
  70. expire_on_commit=False,
  71. autoflush=False,
  72. autocommit=False,
  73. )
  74. logger.info(
  75. f"✅ MySQL连接池已创建: {settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}"
  76. )
  77. async def _create_redis_client(self):
  78. """创建 Redis 客户端"""
  79. async with self._redis_lock:
  80. if self.redis_client:
  81. await self.redis_client.close()
  82. self.redis_client = None
  83. redis_protocol = "rediss" if settings.REDIS_SSL else "redis"
  84. redis_url = (
  85. f"{redis_protocol}://:{settings.REDIS_PASSWORD}@"
  86. f"{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}"
  87. )
  88. self.redis_client = await aioredis.from_url(
  89. redis_url,
  90. encoding="utf-8",
  91. decode_responses=True,
  92. max_connections=settings.REDIS_MAX_CONNECTIONS,
  93. socket_connect_timeout=10,
  94. socket_timeout=30,
  95. retry_on_timeout=True,
  96. health_check_interval=30,
  97. socket_keepalive=True,
  98. )
  99. await self.redis_client.ping()
  100. logger.info(
  101. f"✅ Redis连接成功: {settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}"
  102. )
  103. async def disconnect(self):
  104. """关闭数据库连接"""
  105. try:
  106. # 🔥 停止健康检查任务
  107. self._stop_healthcheck_task()
  108. if self.engine:
  109. await self.engine.dispose()
  110. self.engine = None
  111. self.async_session_maker = None
  112. logger.info("✅ MySQL连接池已关闭")
  113. if self.redis_client:
  114. await self.redis_client.close()
  115. self.redis_client = None
  116. logger.info("✅ Redis连接已关闭")
  117. except Exception as e:
  118. logger.error(f"❌ 关闭数据库连接失败: {e}")
  119. def get_session(self) -> AsyncSession:
  120. """
  121. 获取异步 Session(上下文管理器)
  122. 使用方式:
  123. async with db_manager.get_session() as session:
  124. user = await session.get(User, user_id)
  125. """
  126. if not self.async_session_maker:
  127. raise RuntimeError("MySQL连接池未初始化")
  128. return self.async_session_maker()
  129. async def _ensure_redis_client(self):
  130. """确保Redis连接可用(定期健康检查,自动重连)"""
  131. start_time = time.time()
  132. if not self.redis_client:
  133. logger.info("🔧 Redis客户端不存在,创建新连接...")
  134. await self._create_redis_client()
  135. elapsed = (time.time() - start_time) * 1000
  136. logger.info(f"✅ Redis客户端创建完成,耗时: {elapsed:.1f}ms")
  137. return
  138. loop = asyncio.get_running_loop()
  139. now = loop.time()
  140. time_since_last_check = now - self._last_redis_healthcheck
  141. # 🔥 Redis健康检查间隔(从配置读取)
  142. if time_since_last_check < settings.REDIS_HEALTHCHECK_INTERVAL:
  143. logger.debug(f"⏭️ Redis健康检查跳过(距离上次检查: {time_since_last_check:.1f}s < {settings.REDIS_HEALTHCHECK_INTERVAL}s)")
  144. return
  145. logger.info(f"🏥 开始Redis健康检查(距离上次检查: {time_since_last_check:.1f}s)...")
  146. # 🔥 执行健康检查(使用超时,避免长时间阻塞)
  147. try:
  148. # 使用asyncio.wait_for设置超时,避免健康检查阻塞太久
  149. ping_start = time.time()
  150. await asyncio.wait_for(self.redis_client.ping(), timeout=1.0)
  151. ping_elapsed = (time.time() - ping_start) * 1000
  152. self._last_redis_healthcheck = now
  153. elapsed = (time.time() - start_time) * 1000
  154. logger.info(f"✅ Redis健康检查成功,PING耗时: {ping_elapsed:.1f}ms,总耗时: {elapsed:.1f}ms")
  155. except asyncio.TimeoutError:
  156. elapsed = (time.time() - start_time) * 1000
  157. # 健康检查超时,可能连接有问题,但不在关键路径上阻塞
  158. logger.warning(f"⚠️ Redis健康检查超时(耗时: {elapsed:.1f}ms),将在后台重建连接")
  159. # 🔥 在后台重建连接,不阻塞当前请求
  160. asyncio.create_task(self._recreate_redis_client_background())
  161. self._last_redis_healthcheck = loop.time()
  162. except Exception as exc:
  163. elapsed = (time.time() - start_time) * 1000
  164. # 🔥 健康检查失败,在后台重建连接,不阻塞当前请求
  165. logger.warning(f"⚠️ Redis健康检查失败(耗时: {elapsed:.1f}ms),将在后台重建连接: {exc}")
  166. asyncio.create_task(self._recreate_redis_client_background())
  167. self._last_redis_healthcheck = loop.time()
  168. async def _recreate_redis_client_background(self):
  169. """后台重建Redis连接(不阻塞主流程)"""
  170. start_time = time.time()
  171. logger.info("🔧 开始后台重建Redis连接...")
  172. old_client = None
  173. try:
  174. old_client = self.redis_client
  175. self.redis_client = None # 先清空,避免其他请求使用旧连接
  176. await self._create_redis_client()
  177. elapsed = (time.time() - start_time) * 1000
  178. logger.info(f"✅ Redis连接已在后台重建完成,耗时: {elapsed:.1f}ms")
  179. except asyncio.CancelledError:
  180. # 🔥 任务被取消时,确保清理资源
  181. logger.info("Redis重建任务被取消")
  182. raise # 重新抛出 CancelledError
  183. except Exception as e:
  184. elapsed = (time.time() - start_time) * 1000
  185. logger.error(f"❌ 后台重建Redis连接失败,耗时: {elapsed:.1f}ms, 错误: {e}")
  186. logger.error(traceback.format_exc())
  187. finally:
  188. # 🔥 确保旧连接被关闭
  189. if old_client:
  190. try:
  191. await old_client.close()
  192. except Exception:
  193. pass
  194. def _start_healthcheck_task(self):
  195. """启动后台健康检查任务"""
  196. if self._healthcheck_running:
  197. return
  198. self._healthcheck_running = True
  199. async def healthcheck_loop():
  200. """定期健康检查循环"""
  201. while self._healthcheck_running:
  202. try:
  203. await asyncio.sleep(300) # 每5分钟检查一次
  204. if not self._healthcheck_running:
  205. break
  206. # 检查Redis(MySQL由SQLAlchemy的pool_pre_ping自动处理)
  207. await self._ensure_redis_client()
  208. except asyncio.CancelledError:
  209. # 🔥 任务被取消时,确保清理资源
  210. logger.info("数据库健康检查任务被取消")
  211. raise # 重新抛出 CancelledError
  212. except Exception as e:
  213. logger.error(f"❌ 健康检查任务异常: {e}")
  214. self._healthcheck_task = asyncio.create_task(healthcheck_loop())
  215. logger.info("✅ 数据库健康检查任务已启动")
  216. def _stop_healthcheck_task(self):
  217. """停止后台健康检查任务"""
  218. self._healthcheck_running = False
  219. if self._healthcheck_task and not self._healthcheck_task.done():
  220. self._healthcheck_task.cancel()
  221. logger.info("✅ 数据库健康检查任务已停止")
  222. async def get_redis(self) -> aioredis.Redis:
  223. """获取Redis客户端(自动健康检查)"""
  224. await self._ensure_redis_client()
  225. if not self.redis_client:
  226. raise RuntimeError("Redis客户端未初始化")
  227. return self.redis_client
  228. # 全局数据库管理器实例
  229. db_manager = DatabaseManager()
  230. async def init_database():
  231. """初始化数据库连接"""
  232. await db_manager.connect()
  233. async def close_database():
  234. """关闭数据库连接"""
  235. await db_manager.disconnect()