database.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. """
  2. 数据库连接管理模块
  3. """
  4. import asyncio
  5. import aiomysql
  6. import redis.asyncio as aioredis
  7. from typing import Optional, Callable, Awaitable
  8. from contextlib import asynccontextmanager
  9. from config import settings
  10. import logging
  11. logger = logging.getLogger(__name__)
  12. MYSQL_RETRY_EXCEPTIONS = (
  13. aiomysql.OperationalError,
  14. aiomysql.InterfaceError,
  15. aiomysql.InternalError,
  16. ConnectionError,
  17. ConnectionResetError,
  18. ConnectionAbortedError,
  19. BrokenPipeError,
  20. TimeoutError,
  21. asyncio.TimeoutError,
  22. )
  23. class DatabaseManager:
  24. """数据库连接管理器(MySQL + Redis)"""
  25. def __init__(self):
  26. self.mysql_pool: Optional[aiomysql.Pool] = None
  27. self.redis_client: Optional[aioredis.Redis] = None
  28. self._mysql_lock = asyncio.Lock()
  29. self._redis_lock = asyncio.Lock()
  30. self._last_mysql_healthcheck: float = 0.0
  31. async def connect(self):
  32. """初始化数据库连接"""
  33. try:
  34. await self._create_mysql_pool()
  35. await self._create_redis_client()
  36. except Exception as e:
  37. logger.error(f"❌ 数据库连接失败: {e}")
  38. raise
  39. async def _create_mysql_pool(self):
  40. async with self._mysql_lock:
  41. if self.mysql_pool:
  42. self.mysql_pool.close()
  43. await self.mysql_pool.wait_closed()
  44. self.mysql_pool = None
  45. self.mysql_pool = await aiomysql.create_pool(
  46. host=settings.MYSQL_HOST,
  47. port=settings.MYSQL_PORT,
  48. user=settings.MYSQL_USER,
  49. password=settings.MYSQL_PASSWORD,
  50. db=settings.MYSQL_DATABASE,
  51. charset='utf8mb4',
  52. minsize=5,
  53. maxsize=settings.MYSQL_POOL_SIZE,
  54. autocommit=True,
  55. echo=False,
  56. connect_timeout=settings.MYSQL_CONNECT_TIMEOUT,
  57. # 注意:aiomysql 不支持 read_timeout 参数,已移除
  58. pool_recycle=settings.MYSQL_POOL_RECYCLE,
  59. )
  60. self._last_mysql_healthcheck = 0.0
  61. logger.info(
  62. f"✅ MySQL连接池已创建: {settings.MYSQL_HOST}:{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}"
  63. )
  64. async def _create_redis_client(self):
  65. async with self._redis_lock:
  66. if self.redis_client:
  67. await self.redis_client.close()
  68. self.redis_client = None
  69. redis_protocol = "rediss" if settings.REDIS_SSL else "redis"
  70. redis_url = (
  71. f"{redis_protocol}://:{settings.REDIS_PASSWORD}@"
  72. f"{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}"
  73. )
  74. self.redis_client = await aioredis.from_url(
  75. redis_url,
  76. encoding="utf-8",
  77. decode_responses=True,
  78. max_connections=settings.REDIS_MAX_CONNECTIONS,
  79. socket_connect_timeout=10,
  80. socket_timeout=30, # 🔥 增加超时时间,避免短时网络波动断开连接
  81. retry_on_timeout=True,
  82. health_check_interval=30,
  83. socket_keepalive=True, # 🔥 启用 TCP keepalive,保持长连接(Redis 3.2+ 默认启用)
  84. )
  85. await self.redis_client.ping()
  86. logger.info(
  87. f"✅ Redis连接成功: {settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.REDIS_DB}"
  88. )
  89. async def disconnect(self):
  90. """关闭数据库连接"""
  91. try:
  92. if self.mysql_pool:
  93. self.mysql_pool.close()
  94. await self.mysql_pool.wait_closed()
  95. self.mysql_pool = None
  96. logger.info("✅ MySQL连接池已关闭")
  97. if self.redis_client:
  98. await self.redis_client.close()
  99. self.redis_client = None
  100. logger.info("✅ Redis连接已关闭")
  101. except Exception as e:
  102. logger.error(f"❌ 关闭数据库连接失败: {e}")
  103. async def _ensure_mysql_pool(self):
  104. """确保MySQL连接池可用(定期健康检查,保持长连接)"""
  105. if not self.mysql_pool:
  106. await self._create_mysql_pool()
  107. return
  108. loop = asyncio.get_running_loop()
  109. now = loop.time()
  110. # 🔥 健康检查间隔:只在超过间隔时检查,避免频繁检查
  111. if now - self._last_mysql_healthcheck < settings.MYSQL_HEALTHCHECK_INTERVAL:
  112. return
  113. # 🔥 只做健康检查,不重建连接(保持长连接)
  114. try:
  115. async with self.mysql_pool.acquire() as conn:
  116. await conn.ping()
  117. self._last_mysql_healthcheck = now
  118. # 健康检查成功,连接正常,不重建
  119. except Exception as exc:
  120. # 🔥 只有在健康检查失败时才重建连接(连接已断开)
  121. logger.warning(f"⚠️ MySQL健康检查失败,连接可能已断开,准备重建: {exc}")
  122. await self._create_mysql_pool()
  123. self._last_mysql_healthcheck = loop.time()
  124. async def _run_with_retry(self, operation: Callable[[], Awaitable], description: str):
  125. """执行MySQL操作并在连接异常时重试一次"""
  126. for attempt in range(2):
  127. try:
  128. await self._ensure_mysql_pool()
  129. return await operation()
  130. except MYSQL_RETRY_EXCEPTIONS as exc:
  131. if attempt == 1:
  132. logger.error(f"❌ {description} 失败,已重试: {exc}")
  133. raise
  134. logger.warning(f"⚠️ {description} 连接异常,尝试重建连接: {exc}")
  135. await self._create_mysql_pool()
  136. @asynccontextmanager
  137. async def get_mysql_connection(self):
  138. """获取MySQL连接(上下文管理器)"""
  139. await self._ensure_mysql_pool()
  140. if not self.mysql_pool:
  141. raise RuntimeError("MySQL连接池未初始化")
  142. async with self.mysql_pool.acquire() as conn:
  143. async with conn.cursor(aiomysql.DictCursor) as cursor:
  144. yield cursor
  145. async def execute_query(self, query: str, params: dict = None):
  146. """执行查询(SELECT)"""
  147. params = params or {}
  148. async def _operation():
  149. async with self.get_mysql_connection() as cursor:
  150. await cursor.execute(query, params)
  151. return await cursor.fetchall()
  152. return await self._run_with_retry(_operation, "执行查询")
  153. async def execute_one(self, query: str, params: dict = None):
  154. """执行查询并返回一条记录"""
  155. params = params or {}
  156. async def _operation():
  157. async with self.get_mysql_connection() as cursor:
  158. await cursor.execute(query, params)
  159. return await cursor.fetchone()
  160. return await self._run_with_retry(_operation, "执行单条查询")
  161. async def execute_update(self, query: str, params: dict = None):
  162. """执行更新(INSERT/UPDATE/DELETE)"""
  163. params = params or {}
  164. async def _operation():
  165. async with self.get_mysql_connection() as cursor:
  166. await cursor.execute(query, params)
  167. return cursor.lastrowid
  168. return await self._run_with_retry(_operation, "执行更新")
  169. def get_redis(self) -> aioredis.Redis:
  170. """获取Redis客户端"""
  171. if not self.redis_client:
  172. raise RuntimeError("Redis客户端未初始化")
  173. return self.redis_client
  174. # 全局数据库管理器实例
  175. db_manager = DatabaseManager()
  176. async def init_database():
  177. """初始化数据库连接"""
  178. await db_manager.connect()
  179. async def close_database():
  180. """关闭数据库连接"""
  181. await db_manager.disconnect()