database.py 9.9 KB

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