database.py 10.0 KB

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