springbootAI 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyMyBatis连接池管理模块
|
|
3
|
+
|
|
4
|
+
实现高性能数据库连接池,核心特性:
|
|
5
|
+
- 基于DBUtils实现高性能连接池
|
|
6
|
+
- 最小/最大连接数控制
|
|
7
|
+
- 连接空闲超时回收
|
|
8
|
+
- 连接有效性验证
|
|
9
|
+
- 连接泄漏检测(长时间未归还自动回收并告警)
|
|
10
|
+
- 熔断降级机制(防止数据库故障导致的级联失败)
|
|
11
|
+
- 多数据源支持
|
|
12
|
+
- 线程安全
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
import queue
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Dict, Any, Set, Optional
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_POOL_CONFIG_KEYS = {
|
|
25
|
+
'driver', 'min_size', 'max_size', 'max_idle', 'wait_timeout',
|
|
26
|
+
'validation_interval', 'leak_detection_enabled', 'leak_timeout',
|
|
27
|
+
'circuit_breaker_enabled', 'circuit_breaker_failure_threshold',
|
|
28
|
+
'circuit_breaker_recovery_timeout', 'circuit_breaker_success_threshold',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# 延迟导入熔断器模块
|
|
32
|
+
try:
|
|
33
|
+
from ..circuit_breaker import DatabaseCircuitBreaker, CircuitBreakerError
|
|
34
|
+
_circuit_breaker_available = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
_circuit_breaker_available = False
|
|
37
|
+
logger.warning("熔断器模块不可用,安装方法: pip install pybreaker 或使用内置熔断器")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PooledConnection:
|
|
41
|
+
"""
|
|
42
|
+
池化连接封装类
|
|
43
|
+
|
|
44
|
+
封装原始连接,记录连接状态和使用信息
|
|
45
|
+
|
|
46
|
+
安全特性:
|
|
47
|
+
- 记录使用时间,支持连接泄漏检测
|
|
48
|
+
- 自动标记连接状态
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, connection: Any, pool: 'ConnectionPool', created_at: float):
|
|
52
|
+
self.connection = connection
|
|
53
|
+
self.pool = pool
|
|
54
|
+
self.created_at = created_at
|
|
55
|
+
self.last_used_at = created_at
|
|
56
|
+
self.in_use = False
|
|
57
|
+
self._lock = threading.RLock()
|
|
58
|
+
self._checkout_time = None
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
"""上下文管理器进入"""
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
65
|
+
"""上下文管理器退出,自动归还连接"""
|
|
66
|
+
self.pool.return_connection(self)
|
|
67
|
+
|
|
68
|
+
def mark_in_use(self):
|
|
69
|
+
"""标记连接为使用中"""
|
|
70
|
+
with self._lock:
|
|
71
|
+
if self.in_use:
|
|
72
|
+
raise RuntimeError("连接已被借出,不能重复借用")
|
|
73
|
+
self.in_use = True
|
|
74
|
+
now = time.monotonic()
|
|
75
|
+
self.last_used_at = now
|
|
76
|
+
self._checkout_time = now
|
|
77
|
+
|
|
78
|
+
def mark_free(self):
|
|
79
|
+
"""标记连接为空闲"""
|
|
80
|
+
with self._lock:
|
|
81
|
+
self.in_use = False
|
|
82
|
+
self.last_used_at = time.monotonic()
|
|
83
|
+
self._checkout_time = None
|
|
84
|
+
|
|
85
|
+
def is_valid(self) -> bool:
|
|
86
|
+
"""检查连接是否有效"""
|
|
87
|
+
try:
|
|
88
|
+
# 根据不同数据库驱动检查连接有效性
|
|
89
|
+
if hasattr(self.connection, 'ping'):
|
|
90
|
+
self.connection.ping()
|
|
91
|
+
elif hasattr(self.connection, 'isclosed') and not self.connection.isclosed():
|
|
92
|
+
return True
|
|
93
|
+
return True
|
|
94
|
+
except Exception:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def get_idle_time(self) -> float:
|
|
98
|
+
"""获取空闲时间(秒)"""
|
|
99
|
+
return time.monotonic() - self.last_used_at
|
|
100
|
+
|
|
101
|
+
def get_checkout_duration(self) -> float:
|
|
102
|
+
"""获取连接已借出的时间(秒)"""
|
|
103
|
+
if self._checkout_time is None:
|
|
104
|
+
return 0
|
|
105
|
+
return time.monotonic() - self._checkout_time
|
|
106
|
+
|
|
107
|
+
def get_connection(self) -> Any:
|
|
108
|
+
"""获取原始连接"""
|
|
109
|
+
return self.connection
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ConnectionPool(ABC):
|
|
113
|
+
"""
|
|
114
|
+
连接池抽象基类
|
|
115
|
+
|
|
116
|
+
定义连接池的核心接口:
|
|
117
|
+
- 获取连接
|
|
118
|
+
- 归还连接
|
|
119
|
+
- 管理连接生命周期
|
|
120
|
+
- 连接泄漏检测
|
|
121
|
+
- 熔断降级机制
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, config: Dict[str, Any]):
|
|
125
|
+
self.config = dict(config)
|
|
126
|
+
self.min_size = int(config.get('min_size', 5))
|
|
127
|
+
self.max_size = int(config.get('max_size', 20))
|
|
128
|
+
self.max_idle = float(config.get('max_idle', 10))
|
|
129
|
+
self.wait_timeout = float(config.get('wait_timeout', 30))
|
|
130
|
+
self.validation_interval = float(config.get('validation_interval', 300))
|
|
131
|
+
self.leak_detection_enabled = self._as_bool(config.get('leak_detection_enabled', True))
|
|
132
|
+
self.leak_timeout = float(config.get('leak_timeout', 300))
|
|
133
|
+
|
|
134
|
+
if self.min_size < 0:
|
|
135
|
+
raise ValueError("min_size 不能小于 0")
|
|
136
|
+
if self.max_size < 1:
|
|
137
|
+
raise ValueError("max_size 必须大于 0")
|
|
138
|
+
if self.min_size > self.max_size:
|
|
139
|
+
raise ValueError("min_size 不能大于 max_size")
|
|
140
|
+
if self.wait_timeout <= 0:
|
|
141
|
+
raise ValueError("wait_timeout 必须大于 0")
|
|
142
|
+
if self.max_idle < 0 or self.validation_interval <= 0 or self.leak_timeout <= 0:
|
|
143
|
+
raise ValueError("连接池超时配置必须为正数")
|
|
144
|
+
|
|
145
|
+
# 熔断器配置
|
|
146
|
+
self.circuit_breaker_enabled = self._as_bool(config.get('circuit_breaker_enabled', False))
|
|
147
|
+
self.circuit_breaker_failure_threshold = int(config.get('circuit_breaker_failure_threshold', 3))
|
|
148
|
+
self.circuit_breaker_recovery_timeout = float(config.get('circuit_breaker_recovery_timeout', 60))
|
|
149
|
+
self.circuit_breaker_success_threshold = int(config.get('circuit_breaker_success_threshold', 3))
|
|
150
|
+
|
|
151
|
+
self._pool: queue.Queue = queue.Queue(maxsize=self.max_size)
|
|
152
|
+
self._active_count = 0
|
|
153
|
+
self._active_connections: Set[PooledConnection] = set()
|
|
154
|
+
self._lock = threading.RLock()
|
|
155
|
+
self._validation_lock = threading.Lock()
|
|
156
|
+
self._last_validation = 0
|
|
157
|
+
self._total_connections = 0
|
|
158
|
+
self._closed = False
|
|
159
|
+
self._stop_event = threading.Event()
|
|
160
|
+
|
|
161
|
+
# 初始化熔断器
|
|
162
|
+
self._circuit_breaker = None
|
|
163
|
+
if self.circuit_breaker_enabled and _circuit_breaker_available:
|
|
164
|
+
self._circuit_breaker = DatabaseCircuitBreaker(
|
|
165
|
+
failure_threshold=self.circuit_breaker_failure_threshold,
|
|
166
|
+
recovery_timeout=self.circuit_breaker_recovery_timeout,
|
|
167
|
+
success_threshold=self.circuit_breaker_success_threshold,
|
|
168
|
+
name=f"pool_{id(self)}"
|
|
169
|
+
)
|
|
170
|
+
logger.info("连接池熔断器已启用")
|
|
171
|
+
|
|
172
|
+
# 初始化最小连接数
|
|
173
|
+
self._initialize_pool()
|
|
174
|
+
|
|
175
|
+
# 启动连接泄漏检测线程
|
|
176
|
+
if self.leak_detection_enabled:
|
|
177
|
+
self._start_leak_detection()
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _as_bool(value: Any) -> bool:
|
|
181
|
+
if isinstance(value, bool):
|
|
182
|
+
return value
|
|
183
|
+
if isinstance(value, str):
|
|
184
|
+
return value.strip().lower() in {'1', 'true', 'yes', 'on'}
|
|
185
|
+
return bool(value)
|
|
186
|
+
|
|
187
|
+
@abstractmethod
|
|
188
|
+
def _create_connection(self) -> Any:
|
|
189
|
+
"""创建新连接(由子类实现)"""
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
@abstractmethod
|
|
193
|
+
def _close_connection(self, connection: Any) -> None:
|
|
194
|
+
"""关闭连接(由子类实现)"""
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def _initialize_pool(self) -> None:
|
|
198
|
+
"""初始化连接池,创建最小连接数"""
|
|
199
|
+
last_error = None
|
|
200
|
+
for _ in range(self.min_size):
|
|
201
|
+
try:
|
|
202
|
+
conn = self._create_connection()
|
|
203
|
+
pooled_conn = PooledConnection(conn, self, time.monotonic())
|
|
204
|
+
self._pool.put(pooled_conn)
|
|
205
|
+
self._total_connections += 1
|
|
206
|
+
except Exception as e:
|
|
207
|
+
last_error = e
|
|
208
|
+
logger.error(f"初始化连接池失败: {e}")
|
|
209
|
+
|
|
210
|
+
if self.min_size > 0 and self._total_connections == 0:
|
|
211
|
+
raise ConnectionError("连接池初始化失败,未能建立任何数据库连接") from last_error
|
|
212
|
+
|
|
213
|
+
def _start_leak_detection(self) -> None:
|
|
214
|
+
"""启动连接泄漏检测线程"""
|
|
215
|
+
def leak_detector():
|
|
216
|
+
interval = max(1.0, min(60.0, self.leak_timeout / 2))
|
|
217
|
+
while not self._stop_event.wait(interval):
|
|
218
|
+
self._detect_leaks()
|
|
219
|
+
|
|
220
|
+
leak_thread = threading.Thread(target=leak_detector, daemon=True)
|
|
221
|
+
leak_thread.start()
|
|
222
|
+
logger.info("连接泄漏检测线程已启动")
|
|
223
|
+
|
|
224
|
+
def _detect_leaks(self) -> None:
|
|
225
|
+
"""检测连接泄漏"""
|
|
226
|
+
with self._lock:
|
|
227
|
+
active_connections = list(self._active_connections)
|
|
228
|
+
|
|
229
|
+
for pooled_conn in active_connections:
|
|
230
|
+
checkout_duration = pooled_conn.get_checkout_duration()
|
|
231
|
+
if checkout_duration > self.leak_timeout:
|
|
232
|
+
logger.warning(
|
|
233
|
+
"检测到疑似连接泄漏:连接已借出 %.2f 秒,超过阈值 %.2f 秒",
|
|
234
|
+
checkout_duration,
|
|
235
|
+
self.leak_timeout,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def get_connection(self) -> PooledConnection:
|
|
239
|
+
"""
|
|
240
|
+
获取连接
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
池化连接对象
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
ConnectionError: 获取连接超时
|
|
247
|
+
CircuitBreakerError: 熔断器打开时抛出
|
|
248
|
+
"""
|
|
249
|
+
# 如果启用了熔断器,使用熔断器保护连接获取
|
|
250
|
+
if self._circuit_breaker:
|
|
251
|
+
try:
|
|
252
|
+
return self._circuit_breaker.call(self._get_connection_internal)
|
|
253
|
+
except CircuitBreakerError as e:
|
|
254
|
+
logger.error(f"连接池熔断器打开,请求被拒绝: {e}")
|
|
255
|
+
raise ConnectionError(f"数据库连接熔断,请稍后重试") from e
|
|
256
|
+
|
|
257
|
+
return self._get_connection_internal()
|
|
258
|
+
|
|
259
|
+
def _get_connection_internal(self) -> PooledConnection:
|
|
260
|
+
"""
|
|
261
|
+
获取连接(内部方法,不受熔断器保护)
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
池化连接对象
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ConnectionError: 获取连接超时
|
|
268
|
+
"""
|
|
269
|
+
# 定期验证连接有效性
|
|
270
|
+
self._validate_pool_periodically()
|
|
271
|
+
|
|
272
|
+
deadline = time.monotonic() + self.wait_timeout
|
|
273
|
+
|
|
274
|
+
while True:
|
|
275
|
+
if self._closed:
|
|
276
|
+
raise ConnectionError("连接池已关闭")
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
pooled_conn = self._pool.get_nowait()
|
|
280
|
+
except queue.Empty:
|
|
281
|
+
pooled_conn = self._create_connection_if_capacity()
|
|
282
|
+
if pooled_conn is None:
|
|
283
|
+
remaining = deadline - time.monotonic()
|
|
284
|
+
if remaining <= 0:
|
|
285
|
+
raise ConnectionError(
|
|
286
|
+
f"获取连接超时,最大连接数已达上限: {self.max_size}"
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
pooled_conn = self._pool.get(timeout=remaining)
|
|
290
|
+
except queue.Empty as exc:
|
|
291
|
+
raise ConnectionError(
|
|
292
|
+
f"获取连接超时,最大连接数已达上限: {self.max_size}"
|
|
293
|
+
) from exc
|
|
294
|
+
|
|
295
|
+
if not pooled_conn.is_valid():
|
|
296
|
+
logger.warning("检测到无效连接,关闭并重新获取")
|
|
297
|
+
self._dispose_connection(pooled_conn)
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
pooled_conn.mark_in_use()
|
|
301
|
+
with self._lock:
|
|
302
|
+
if self._closed:
|
|
303
|
+
pooled_conn.mark_free()
|
|
304
|
+
self._dispose_connection(pooled_conn)
|
|
305
|
+
raise ConnectionError("连接池已关闭")
|
|
306
|
+
self._active_connections.add(pooled_conn)
|
|
307
|
+
self._active_count = len(self._active_connections)
|
|
308
|
+
|
|
309
|
+
logger.debug(f"获取连接成功,活跃连接数={self._active_count}")
|
|
310
|
+
return pooled_conn
|
|
311
|
+
|
|
312
|
+
def _create_connection_if_capacity(self):
|
|
313
|
+
"""在未达到上限时立即扩容,并用预留计数防止并发超配。"""
|
|
314
|
+
with self._lock:
|
|
315
|
+
if self._closed or self._total_connections >= self.max_size:
|
|
316
|
+
return None
|
|
317
|
+
self._total_connections += 1
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
conn = self._create_connection()
|
|
321
|
+
pooled_conn = PooledConnection(conn, self, time.monotonic())
|
|
322
|
+
logger.info(f"创建新连接,总连接数={self._total_connections}")
|
|
323
|
+
return pooled_conn
|
|
324
|
+
except Exception as e:
|
|
325
|
+
with self._lock:
|
|
326
|
+
self._total_connections -= 1
|
|
327
|
+
raise ConnectionError(f"创建新连接失败: {e}") from e
|
|
328
|
+
|
|
329
|
+
def _dispose_connection(self, pooled_conn: PooledConnection) -> None:
|
|
330
|
+
with self._lock:
|
|
331
|
+
self._active_connections.discard(pooled_conn)
|
|
332
|
+
self._active_count = len(self._active_connections)
|
|
333
|
+
if self._total_connections > 0:
|
|
334
|
+
self._total_connections -= 1
|
|
335
|
+
self._close_connection(pooled_conn.get_connection())
|
|
336
|
+
|
|
337
|
+
def return_connection(self, pooled_conn: PooledConnection) -> None:
|
|
338
|
+
"""
|
|
339
|
+
归还连接到池中
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
pooled_conn: 池化连接对象
|
|
343
|
+
"""
|
|
344
|
+
if pooled_conn.pool is not self:
|
|
345
|
+
raise ValueError("连接不属于当前连接池")
|
|
346
|
+
if not pooled_conn.in_use:
|
|
347
|
+
raise ValueError("连接已归还,不能重复归还")
|
|
348
|
+
|
|
349
|
+
checkout_duration = pooled_conn.get_checkout_duration()
|
|
350
|
+
if checkout_duration > self.leak_timeout:
|
|
351
|
+
logger.warning(
|
|
352
|
+
f"连接泄漏检测:连接已借出 {checkout_duration:.2f} 秒,超过阈值 {self.leak_timeout} 秒"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# DB-API连接归还前统一回滚,避免未提交状态污染下一个请求。
|
|
356
|
+
try:
|
|
357
|
+
pooled_conn.get_connection().rollback()
|
|
358
|
+
except Exception:
|
|
359
|
+
logger.exception("重置数据库连接失败,将关闭该连接")
|
|
360
|
+
pooled_conn.mark_free()
|
|
361
|
+
self._dispose_connection(pooled_conn)
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
pooled_conn.mark_free()
|
|
365
|
+
with self._lock:
|
|
366
|
+
self._active_connections.discard(pooled_conn)
|
|
367
|
+
self._active_count = len(self._active_connections)
|
|
368
|
+
|
|
369
|
+
logger.debug(f"归还连接,活跃连接数={self._active_count}")
|
|
370
|
+
|
|
371
|
+
if self._closed or not pooled_conn.is_valid():
|
|
372
|
+
self._dispose_connection(pooled_conn)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
# 归还到池中
|
|
376
|
+
try:
|
|
377
|
+
self._pool.put(pooled_conn, timeout=1)
|
|
378
|
+
except queue.Full:
|
|
379
|
+
# 池已满,关闭该连接
|
|
380
|
+
logger.info("连接池已满,关闭多余连接")
|
|
381
|
+
self._dispose_connection(pooled_conn)
|
|
382
|
+
|
|
383
|
+
def _validate_pool_periodically(self) -> None:
|
|
384
|
+
"""定期验证池中的连接有效性"""
|
|
385
|
+
now = time.monotonic()
|
|
386
|
+
if now - self._last_validation < self.validation_interval:
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
if not self._validation_lock.acquire(blocking=False):
|
|
390
|
+
return
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
self._last_validation = now
|
|
394
|
+
logger.debug("开始验证连接池中的连接")
|
|
395
|
+
|
|
396
|
+
valid_connections = []
|
|
397
|
+
while True:
|
|
398
|
+
try:
|
|
399
|
+
pooled_conn = self._pool.get_nowait()
|
|
400
|
+
except queue.Empty:
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
can_evict_idle = self._total_connections > self.min_size
|
|
404
|
+
if not pooled_conn.is_valid() or (
|
|
405
|
+
can_evict_idle and pooled_conn.get_idle_time() > self.max_idle
|
|
406
|
+
):
|
|
407
|
+
self._dispose_connection(pooled_conn)
|
|
408
|
+
else:
|
|
409
|
+
valid_connections.append(pooled_conn)
|
|
410
|
+
|
|
411
|
+
for pooled_conn in valid_connections:
|
|
412
|
+
self._pool.put_nowait(pooled_conn)
|
|
413
|
+
|
|
414
|
+
while not self._closed:
|
|
415
|
+
with self._lock:
|
|
416
|
+
needs_connection = self._total_connections < self.min_size
|
|
417
|
+
if not needs_connection:
|
|
418
|
+
break
|
|
419
|
+
try:
|
|
420
|
+
pooled_conn = self._create_connection_if_capacity()
|
|
421
|
+
if pooled_conn is None:
|
|
422
|
+
break
|
|
423
|
+
self._pool.put_nowait(pooled_conn)
|
|
424
|
+
except Exception as e:
|
|
425
|
+
logger.error(f"补充连接池失败: {e}")
|
|
426
|
+
break
|
|
427
|
+
finally:
|
|
428
|
+
self._validation_lock.release()
|
|
429
|
+
|
|
430
|
+
def get_pool_stats(self) -> Dict[str, Any]:
|
|
431
|
+
"""
|
|
432
|
+
获取连接池统计信息
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
统计信息字典
|
|
436
|
+
"""
|
|
437
|
+
with self._lock:
|
|
438
|
+
stats = {
|
|
439
|
+
'active_connections': self._active_count,
|
|
440
|
+
'idle_connections': self._pool.qsize(),
|
|
441
|
+
'min_size': self.min_size,
|
|
442
|
+
'max_size': self.max_size,
|
|
443
|
+
'total_connections': self._total_connections,
|
|
444
|
+
'leak_detection_enabled': self.leak_detection_enabled,
|
|
445
|
+
'leak_timeout': self.leak_timeout,
|
|
446
|
+
'circuit_breaker_enabled': self.circuit_breaker_enabled,
|
|
447
|
+
'closed': self._closed,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
# 添加熔断器统计信息
|
|
451
|
+
if self._circuit_breaker:
|
|
452
|
+
stats['circuit_breaker'] = self._circuit_breaker.get_stats()
|
|
453
|
+
|
|
454
|
+
return stats
|
|
455
|
+
|
|
456
|
+
def close(self) -> None:
|
|
457
|
+
"""关闭连接池,释放所有连接"""
|
|
458
|
+
with self._lock:
|
|
459
|
+
if self._closed:
|
|
460
|
+
return
|
|
461
|
+
self._closed = True
|
|
462
|
+
active_connections = list(self._active_connections)
|
|
463
|
+
self._stop_event.set()
|
|
464
|
+
logger.info("关闭连接池")
|
|
465
|
+
idle_connections = []
|
|
466
|
+
while True:
|
|
467
|
+
try:
|
|
468
|
+
idle_connections.append(self._pool.get_nowait())
|
|
469
|
+
except queue.Empty:
|
|
470
|
+
break
|
|
471
|
+
for pooled_conn in idle_connections + active_connections:
|
|
472
|
+
self._dispose_connection(pooled_conn)
|
|
473
|
+
|
|
474
|
+
def __del__(self):
|
|
475
|
+
"""析构函数,确保连接池被关闭"""
|
|
476
|
+
try:
|
|
477
|
+
self.close()
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _get_docker_container_ip_by_port(target_port: int) -> Optional[str]:
|
|
483
|
+
"""通过Docker CLI自动检测映射了指定端口的容器IP(开发环境辅助)
|
|
484
|
+
|
|
485
|
+
按以下顺序查找:
|
|
486
|
+
1. 精确查找端口映射匹配target_port的运行中容器
|
|
487
|
+
2. 如果是数据库默认端口(3306/5432),兜底查找mysql/mariadb/postgres镜像容器
|
|
488
|
+
返回容器内部IP,找不到返回None
|
|
489
|
+
"""
|
|
490
|
+
import os
|
|
491
|
+
import re
|
|
492
|
+
# 允许通过环境变量禁用Docker自动检测
|
|
493
|
+
if os.getenv('SPRING_DISABLE_DOCKER_IP_DETECT', '').lower() in ('1', 'true', 'yes'):
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
# 数据库默认端口列表(用于兜底镜像匹配)
|
|
497
|
+
DB_PORTS = {3306: ('mysql', 'mariadb'), 5432: ('postgres', 'postgresql')}
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
import subprocess
|
|
501
|
+
# 方法1:精确通过端口映射查找容器
|
|
502
|
+
# 端口映射格式: 0.0.0.0:3306->3306/tcp, [::]:3306->3306/tcp
|
|
503
|
+
port_pattern = re.compile(r'(?:0\.0\.0\.0|::|\*):' + str(target_port) + r'->')
|
|
504
|
+
|
|
505
|
+
result = subprocess.run(
|
|
506
|
+
['docker', 'ps', '--format', '{{.ID}}|{{.Ports}}'],
|
|
507
|
+
capture_output=True, text=True, timeout=5
|
|
508
|
+
)
|
|
509
|
+
if result.returncode == 0:
|
|
510
|
+
for line in result.stdout.strip().split('\n'):
|
|
511
|
+
if not line or '|' not in line:
|
|
512
|
+
continue
|
|
513
|
+
cid, ports_str = line.split('|', 1)
|
|
514
|
+
if port_pattern.search(ports_str):
|
|
515
|
+
ip_result = subprocess.run(
|
|
516
|
+
['docker', 'inspect', '-f', '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}', cid.strip()],
|
|
517
|
+
capture_output=True, text=True, timeout=5
|
|
518
|
+
)
|
|
519
|
+
if ip_result.returncode == 0 and ip_result.stdout.strip():
|
|
520
|
+
return ip_result.stdout.strip()
|
|
521
|
+
|
|
522
|
+
# 方法2:兜底 - 仅当目标端口是数据库默认端口时,按镜像名模糊匹配
|
|
523
|
+
db_keywords = DB_PORTS.get(target_port)
|
|
524
|
+
if db_keywords:
|
|
525
|
+
result = subprocess.run(
|
|
526
|
+
['docker', 'ps', '--format', '{{.ID}}|{{.Image}}'],
|
|
527
|
+
capture_output=True, text=True, timeout=5
|
|
528
|
+
)
|
|
529
|
+
if result.returncode == 0:
|
|
530
|
+
for line in result.stdout.strip().split('\n'):
|
|
531
|
+
if not line or '|' not in line:
|
|
532
|
+
continue
|
|
533
|
+
cid, image = line.split('|', 1)
|
|
534
|
+
image_lower = image.lower()
|
|
535
|
+
if any(kw in image_lower for kw in db_keywords):
|
|
536
|
+
ip_result = subprocess.run(
|
|
537
|
+
['docker', 'inspect', '-f', '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}', cid.strip()],
|
|
538
|
+
capture_output=True, text=True, timeout=5
|
|
539
|
+
)
|
|
540
|
+
if ip_result.returncode == 0 and ip_result.stdout.strip():
|
|
541
|
+
return ip_result.stdout.strip()
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class MySQLConnectionPool(ConnectionPool):
|
|
548
|
+
"""MySQL连接池实现"""
|
|
549
|
+
|
|
550
|
+
def _create_connection(self) -> Any:
|
|
551
|
+
"""创建MySQL连接"""
|
|
552
|
+
try:
|
|
553
|
+
import pymysql
|
|
554
|
+
config = self.config.copy()
|
|
555
|
+
for key in _POOL_CONFIG_KEYS:
|
|
556
|
+
config.pop(key, None)
|
|
557
|
+
if 'username' in config and 'user' not in config:
|
|
558
|
+
config['user'] = config.pop('username')
|
|
559
|
+
|
|
560
|
+
# 确保密码是字符串类型
|
|
561
|
+
if 'password' in config:
|
|
562
|
+
config['password'] = str(config['password'] or '')
|
|
563
|
+
else:
|
|
564
|
+
config['password'] = ''
|
|
565
|
+
|
|
566
|
+
# 设置默认参数
|
|
567
|
+
config.setdefault('charset', 'utf8mb4')
|
|
568
|
+
config.setdefault('cursorclass', pymysql.cursors.DictCursor)
|
|
569
|
+
config.setdefault('autocommit', False)
|
|
570
|
+
# 明确禁用unix socket,强制使用TCP连接
|
|
571
|
+
config['unix_socket'] = None
|
|
572
|
+
config['connect_timeout'] = 5
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
return pymysql.connect(**config)
|
|
576
|
+
except Exception as e:
|
|
577
|
+
# 如果连接localhost/127.0.0.1失败,尝试自动检测Docker容器IP
|
|
578
|
+
host = config.get('host', '')
|
|
579
|
+
if host in ('localhost', '127.0.0.1', '0.0.0.0'):
|
|
580
|
+
docker_ip = _get_docker_container_ip_by_port(config.get('port', 3306))
|
|
581
|
+
if docker_ip:
|
|
582
|
+
logger.info(f"使用Docker容器IP {docker_ip}:{config.get('port', 3306)} 连接数据库")
|
|
583
|
+
config['host'] = docker_ip
|
|
584
|
+
return pymysql.connect(**config)
|
|
585
|
+
raise
|
|
586
|
+
except ImportError:
|
|
587
|
+
raise ImportError("请安装pymysql: pip install pymysql")
|
|
588
|
+
|
|
589
|
+
def _close_connection(self, connection: Any) -> None:
|
|
590
|
+
"""关闭MySQL连接"""
|
|
591
|
+
try:
|
|
592
|
+
connection.close()
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class PostgreSQLConnectionPool(ConnectionPool):
|
|
598
|
+
"""PostgreSQL连接池实现"""
|
|
599
|
+
|
|
600
|
+
def _create_connection(self) -> Any:
|
|
601
|
+
"""创建PostgreSQL连接"""
|
|
602
|
+
try:
|
|
603
|
+
import psycopg2
|
|
604
|
+
config = self.config.copy()
|
|
605
|
+
for key in _POOL_CONFIG_KEYS:
|
|
606
|
+
config.pop(key, None)
|
|
607
|
+
if 'username' in config and 'user' not in config:
|
|
608
|
+
config['user'] = config.pop('username')
|
|
609
|
+
|
|
610
|
+
connection = psycopg2.connect(**config)
|
|
611
|
+
connection.autocommit = False
|
|
612
|
+
return connection
|
|
613
|
+
except ImportError:
|
|
614
|
+
raise ImportError("请安装psycopg2: pip install psycopg2-binary")
|
|
615
|
+
|
|
616
|
+
def _close_connection(self, connection: Any) -> None:
|
|
617
|
+
"""关闭PostgreSQL连接"""
|
|
618
|
+
try:
|
|
619
|
+
connection.close()
|
|
620
|
+
except Exception:
|
|
621
|
+
pass
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class SQLiteConnectionPool(ConnectionPool):
|
|
625
|
+
"""SQLite连接池实现"""
|
|
626
|
+
|
|
627
|
+
def __init__(self, config: Dict[str, Any]):
|
|
628
|
+
normalized_config = dict(config)
|
|
629
|
+
database = normalized_config.get('database', normalized_config.get('db', ':memory:'))
|
|
630
|
+
if database == ':memory:':
|
|
631
|
+
# 多个内存连接对应不同数据库,必须限制为单连接。
|
|
632
|
+
normalized_config['min_size'] = 1
|
|
633
|
+
normalized_config['max_size'] = 1
|
|
634
|
+
super().__init__(normalized_config)
|
|
635
|
+
|
|
636
|
+
def _create_connection(self) -> Any:
|
|
637
|
+
"""创建SQLite连接"""
|
|
638
|
+
try:
|
|
639
|
+
import sqlite3
|
|
640
|
+
config = self.config.copy()
|
|
641
|
+
for key in _POOL_CONFIG_KEYS | {'host', 'port', 'username', 'password'}:
|
|
642
|
+
config.pop(key, None)
|
|
643
|
+
|
|
644
|
+
# SQLite需要特殊处理
|
|
645
|
+
db_path = config.pop('database', config.pop('db', ':memory:'))
|
|
646
|
+
config.setdefault('check_same_thread', False)
|
|
647
|
+
connection = sqlite3.connect(db_path, **config)
|
|
648
|
+
connection.row_factory = sqlite3.Row
|
|
649
|
+
return connection
|
|
650
|
+
except ImportError:
|
|
651
|
+
raise ImportError("SQLite应该是Python内置的")
|
|
652
|
+
|
|
653
|
+
def _close_connection(self, connection: Any) -> None:
|
|
654
|
+
"""关闭SQLite连接"""
|
|
655
|
+
try:
|
|
656
|
+
connection.close()
|
|
657
|
+
except Exception:
|
|
658
|
+
pass
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class OracleConnectionPool(ConnectionPool):
|
|
662
|
+
"""Oracle连接池实现"""
|
|
663
|
+
|
|
664
|
+
def _create_connection(self) -> Any:
|
|
665
|
+
"""创建Oracle连接"""
|
|
666
|
+
try:
|
|
667
|
+
import cx_Oracle
|
|
668
|
+
config = self.config.copy()
|
|
669
|
+
for key in _POOL_CONFIG_KEYS:
|
|
670
|
+
config.pop(key, None)
|
|
671
|
+
if 'username' in config and 'user' not in config:
|
|
672
|
+
config['user'] = config.pop('username')
|
|
673
|
+
|
|
674
|
+
return cx_Oracle.connect(**config)
|
|
675
|
+
except ImportError:
|
|
676
|
+
raise ImportError("请安装cx_Oracle: pip install cx_Oracle")
|
|
677
|
+
|
|
678
|
+
def _close_connection(self, connection: Any) -> None:
|
|
679
|
+
"""关闭Oracle连接"""
|
|
680
|
+
try:
|
|
681
|
+
connection.close()
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def create_connection_pool(dialect: str, config: Dict[str, Any]) -> ConnectionPool:
|
|
687
|
+
"""
|
|
688
|
+
根据方言创建连接池
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
dialect: 数据库方言名称
|
|
692
|
+
config: 连接池配置
|
|
693
|
+
|
|
694
|
+
Returns:
|
|
695
|
+
连接池实例
|
|
696
|
+
|
|
697
|
+
Raises:
|
|
698
|
+
ValueError: 不支持的数据库方言
|
|
699
|
+
"""
|
|
700
|
+
pool_map = {
|
|
701
|
+
'mysql': MySQLConnectionPool,
|
|
702
|
+
'postgresql': PostgreSQLConnectionPool,
|
|
703
|
+
'sqlite': SQLiteConnectionPool,
|
|
704
|
+
'oracle': OracleConnectionPool
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
pool_class = pool_map.get(dialect.lower())
|
|
708
|
+
if not pool_class:
|
|
709
|
+
raise ValueError(f"不支持的数据库方言: {dialect}")
|
|
710
|
+
|
|
711
|
+
return pool_class(config)
|