237 lines
8.8 KiB
Python
237 lines
8.8 KiB
Python
|
|
#!/usr/bin/env python
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
数据库连接配置
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import pymysql
|
|||
|
|
import logging
|
|||
|
|
import time
|
|||
|
|
from pymysql.cursors import DictCursor
|
|||
|
|
from contextlib import contextmanager
|
|||
|
|
from pymysql.err import OperationalError, InterfaceError, InternalError
|
|||
|
|
import threading
|
|||
|
|
|
|||
|
|
# 数据库配置
|
|||
|
|
DB_CONFIG = {
|
|||
|
|
'host': '8.149.233.36',
|
|||
|
|
'port': 3306,
|
|||
|
|
'user': 'ai_wht_write',
|
|||
|
|
'password': '7aK_H2yvokVumr84lLNDt8fDBp6P',
|
|||
|
|
'database': 'ai_wht',
|
|||
|
|
'charset': 'utf8mb4',
|
|||
|
|
'collation': 'utf8mb4_unicode_ci',
|
|||
|
|
'cursorclass': DictCursor,
|
|||
|
|
'autocommit': True,
|
|||
|
|
'connect_timeout': 10,
|
|||
|
|
'read_timeout': 30,
|
|||
|
|
'write_timeout': 30,
|
|||
|
|
'max_allowed_packet': 16777216, # 16MB
|
|||
|
|
'sql_mode': 'STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 使用与主应用相同的日志记录器
|
|||
|
|
logger = logging.getLogger('wht_server')
|
|||
|
|
|
|||
|
|
class DatabaseManager:
|
|||
|
|
"""数据库连接管理器"""
|
|||
|
|
|
|||
|
|
def __init__(self, config=None):
|
|||
|
|
self.config = config or DB_CONFIG
|
|||
|
|
# 使用线程本地连接,避免多线程复用同一连接导致的数据包序号错误
|
|||
|
|
self._local = threading.local()
|
|||
|
|
self._max_retries = 3
|
|||
|
|
self._retry_delay = 1 # 秒
|
|||
|
|
|
|||
|
|
def get_connection(self):
|
|||
|
|
"""获取数据库连接"""
|
|||
|
|
try:
|
|||
|
|
# 线程本地连接对象
|
|||
|
|
connection = getattr(self._local, 'connection', None)
|
|||
|
|
|
|||
|
|
# 检查现有连接是否有效
|
|||
|
|
if connection is not None:
|
|||
|
|
try:
|
|||
|
|
# 尝试 ping 并在需要时自动重连
|
|||
|
|
connection.ping(reconnect=True)
|
|||
|
|
if connection.open:
|
|||
|
|
return connection
|
|||
|
|
except (OperationalError, InterfaceError, AttributeError):
|
|||
|
|
logger.warning("检测到无效连接,将重新创建")
|
|||
|
|
connection = None
|
|||
|
|
self._local.connection = None
|
|||
|
|
|
|||
|
|
# 创建新连接(线程专有)
|
|||
|
|
self._local.connection = pymysql.connect(**self.config)
|
|||
|
|
|
|||
|
|
# 确保连接使用正确的字符集
|
|||
|
|
with self._local.connection.cursor() as cursor:
|
|||
|
|
cursor.execute("SET NAMES utf8mb4")
|
|||
|
|
|
|||
|
|
logger.info("数据库连接成功,字符集设置完成")
|
|||
|
|
return self._local.connection
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"数据库连接失败: {e}")
|
|||
|
|
# 确保线程本地连接被清理
|
|||
|
|
try:
|
|||
|
|
self._local.connection = None
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
def close_connection(self):
|
|||
|
|
"""关闭数据库连接"""
|
|||
|
|
connection = getattr(self._local, 'connection', None)
|
|||
|
|
if connection:
|
|||
|
|
try:
|
|||
|
|
if connection.open:
|
|||
|
|
connection.close()
|
|||
|
|
logger.info("数据库连接已关闭")
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"关闭数据库连接时出错: {e}")
|
|||
|
|
finally:
|
|||
|
|
self._local.connection = None
|
|||
|
|
|
|||
|
|
def _execute_with_retry(self, operation, *args, **kwargs):
|
|||
|
|
"""带重试机制的执行操作"""
|
|||
|
|
last_exception = None
|
|||
|
|
|
|||
|
|
for attempt in range(self._max_retries):
|
|||
|
|
try:
|
|||
|
|
return operation(*args, **kwargs)
|
|||
|
|
except (OperationalError, InterfaceError, InternalError, AttributeError) as e:
|
|||
|
|
last_exception = e
|
|||
|
|
error_code = getattr(e, 'args', [None])[0] if getattr(e, 'args', None) else None
|
|||
|
|
|
|||
|
|
# 检查是否是连接丢失错误
|
|||
|
|
message = str(e)
|
|||
|
|
is_packet_seq_error = isinstance(e, InternalError) and 'Packet sequence number wrong' in message
|
|||
|
|
is_already_closed = isinstance(e, InterfaceError) and ('Already closed' in message or 'closed' in message.lower())
|
|||
|
|
is_none_read = isinstance(e, AttributeError) and "'NoneType' object has no attribute 'read'" in message
|
|||
|
|
if error_code in [2006, 2013, 2055] or is_packet_seq_error or is_already_closed or is_none_read:
|
|||
|
|
logger.warning(f"数据库连接丢失 (尝试 {attempt + 1}/{self._max_retries}): {e}")
|
|||
|
|
# 关闭现有连接
|
|||
|
|
self.close_connection()
|
|||
|
|
|
|||
|
|
if attempt < self._max_retries - 1:
|
|||
|
|
time.sleep(self._retry_delay)
|
|||
|
|
continue
|
|||
|
|
else:
|
|||
|
|
# 其他数据库错误,不重试
|
|||
|
|
raise
|
|||
|
|
except Exception as e:
|
|||
|
|
# 其他错误,不重试
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 所有重试都失败了
|
|||
|
|
logger.error(f"数据库操作失败,已重试 {self._max_retries} 次: {last_exception}")
|
|||
|
|
raise last_exception
|
|||
|
|
|
|||
|
|
@contextmanager
|
|||
|
|
def get_cursor(self):
|
|||
|
|
"""获取数据库游标的上下文管理器"""
|
|||
|
|
connection = None
|
|||
|
|
cursor = None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 获取连接
|
|||
|
|
connection = self._execute_with_retry(self.get_connection)
|
|||
|
|
cursor = connection.cursor(DictCursor)
|
|||
|
|
yield cursor
|
|||
|
|
|
|||
|
|
# 提交事务
|
|||
|
|
if connection and connection.open:
|
|||
|
|
try:
|
|||
|
|
connection.commit()
|
|||
|
|
except (OperationalError, InterfaceError) as e:
|
|||
|
|
logger.warning(f"提交事务失败: {e}")
|
|||
|
|
# 不抛出异常,因为可能是连接问题
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
# 回滚事务
|
|||
|
|
if connection and connection.open:
|
|||
|
|
try:
|
|||
|
|
connection.rollback()
|
|||
|
|
except (OperationalError, InterfaceError) as rollback_error:
|
|||
|
|
logger.warning(f"回滚事务失败: {rollback_error}")
|
|||
|
|
# 不抛出异常,因为可能是连接问题
|
|||
|
|
|
|||
|
|
logger.error(f"数据库操作失败: {e}")
|
|||
|
|
raise
|
|||
|
|
finally:
|
|||
|
|
# 关闭游标
|
|||
|
|
if cursor:
|
|||
|
|
try:
|
|||
|
|
cursor.close()
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.warning(f"关闭游标时出错: {e}")
|
|||
|
|
|
|||
|
|
def execute_query(self, sql, params=None):
|
|||
|
|
"""执行查询SQL"""
|
|||
|
|
def _query_operation():
|
|||
|
|
with self.get_cursor() as cursor:
|
|||
|
|
# 记录SQL执行开始
|
|||
|
|
logger.info(f"[SQL执行] 开始执行查询SQL")
|
|||
|
|
logger.info(f"[SQL语句] {sql}")
|
|||
|
|
if params:
|
|||
|
|
logger.info(f"[SQL参数] {params}")
|
|||
|
|
|
|||
|
|
cursor.execute(sql, params)
|
|||
|
|
result = cursor.fetchall()
|
|||
|
|
|
|||
|
|
# 记录SQL执行结果
|
|||
|
|
logger.info(f"[SQL结果] 查询完成,返回 {len(result)} 条记录")
|
|||
|
|
if result and len(result) <= 3: # 只记录前3条结果,避免日志过长
|
|||
|
|
logger.info(f"[SQL数据] 查询结果: {result}")
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
return self._execute_with_retry(_query_operation)
|
|||
|
|
|
|||
|
|
def execute_update(self, sql, params=None):
|
|||
|
|
"""执行更新SQL"""
|
|||
|
|
def _update_operation():
|
|||
|
|
with self.get_cursor() as cursor:
|
|||
|
|
# 记录SQL执行开始
|
|||
|
|
logger.info(f"[SQL执行] 开始执行更新SQL")
|
|||
|
|
logger.info(f"[SQL语句] {sql}")
|
|||
|
|
if params:
|
|||
|
|
logger.info(f"[SQL参数] {params}")
|
|||
|
|
|
|||
|
|
affected_rows = cursor.execute(sql, params)
|
|||
|
|
|
|||
|
|
# 记录SQL执行结果
|
|||
|
|
logger.info(f"[SQL结果] 更新完成,影响 {affected_rows} 行")
|
|||
|
|
|
|||
|
|
return affected_rows
|
|||
|
|
|
|||
|
|
return self._execute_with_retry(_update_operation)
|
|||
|
|
|
|||
|
|
def execute_insert(self, sql, params=None):
|
|||
|
|
"""执行插入SQL"""
|
|||
|
|
def _insert_operation():
|
|||
|
|
with self.get_cursor() as cursor:
|
|||
|
|
# 记录SQL执行开始
|
|||
|
|
logger.info(f"[SQL执行] 开始执行插入SQL")
|
|||
|
|
logger.info(f"[SQL语句] {sql}")
|
|||
|
|
if params:
|
|||
|
|
logger.info(f"[SQL参数] {params}")
|
|||
|
|
|
|||
|
|
cursor.execute(sql, params)
|
|||
|
|
last_id = cursor.lastrowid
|
|||
|
|
|
|||
|
|
# 记录SQL执行结果
|
|||
|
|
logger.info(f"[SQL结果] 插入完成,新记录ID: {last_id}")
|
|||
|
|
|
|||
|
|
return last_id
|
|||
|
|
|
|||
|
|
return self._execute_with_retry(_insert_operation)
|
|||
|
|
|
|||
|
|
# 全局数据库管理器实例
|
|||
|
|
db_manager = DatabaseManager()
|
|||
|
|
|
|||
|
|
def get_db_manager():
|
|||
|
|
"""获取数据库管理器实例"""
|
|||
|
|
return db_manager
|