mdbq 4.0.110__tar.gz → 4.0.112__tar.gz
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.
Potentially problematic release.
This version of mdbq might be problematic. Click here for more details.
- {mdbq-4.0.110 → mdbq-4.0.112}/PKG-INFO +1 -1
- mdbq-4.0.112/mdbq/__version__.py +1 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/redis/redis_cache.py +358 -92
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq.egg-info/PKG-INFO +1 -1
- mdbq-4.0.110/mdbq/__version__.py +0 -1
- {mdbq-4.0.110 → mdbq-4.0.112}/README.txt +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/auth/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/auth/auth_backend.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/auth/rate_limiter.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/js/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/js/jc.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/log/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/log/mylogger.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/myconf/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/myconf/myconf.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/deduplicator.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/mysql.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/s_query.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/unique_.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/mysql/uploader.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/download_sku_picture.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/error_handler.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/otk.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/pov_city.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/other/ua_sj.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/pbix/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/pbix/pbix_refresh.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/pbix/refresh_all.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/redis/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/redis/getredis.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/route/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/route/analytics.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/route/monitor.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/route/routes.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/selenium/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/selenium/get_driver.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq/spider/__init__.py +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq.egg-info/SOURCES.txt +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq.egg-info/dependency_links.txt +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/mdbq.egg-info/top_level.txt +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/setup.cfg +0 -0
- {mdbq-4.0.110 → mdbq-4.0.112}/setup.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '4.0.112'
|
|
@@ -18,9 +18,17 @@ import socket
|
|
|
18
18
|
from datetime import datetime, date
|
|
19
19
|
from decimal import Decimal
|
|
20
20
|
from uuid import UUID
|
|
21
|
-
from typing import Optional, Dict, Any, List, Union
|
|
21
|
+
from typing import Optional, Dict, Any, List, Union, Callable
|
|
22
22
|
import redis
|
|
23
23
|
from mdbq.log import mylogger
|
|
24
|
+
|
|
25
|
+
import enum
|
|
26
|
+
from typing import Optional, Dict, Any, List, Union, Callable
|
|
27
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
28
|
+
import asyncio
|
|
29
|
+
from threading import Event
|
|
30
|
+
import functools
|
|
31
|
+
|
|
24
32
|
logger = mylogger.MyLogger(
|
|
25
33
|
logging_mode='file',
|
|
26
34
|
log_level='info',
|
|
@@ -34,6 +42,14 @@ logger = mylogger.MyLogger(
|
|
|
34
42
|
)
|
|
35
43
|
|
|
36
44
|
|
|
45
|
+
class CacheSystemState(enum.Enum):
|
|
46
|
+
"""缓存系统状态枚举"""
|
|
47
|
+
INITIALIZING = "initializing"
|
|
48
|
+
READY = "ready"
|
|
49
|
+
MYSQL_READY = "mysql_ready"
|
|
50
|
+
ERROR = "error"
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
class MySQLDataEncoder(json.JSONEncoder):
|
|
38
54
|
"""自定义JSON编码器,支持MySQL常见数据类型"""
|
|
39
55
|
def default(self, obj):
|
|
@@ -73,6 +89,64 @@ class MySQLDataEncoder(json.JSONEncoder):
|
|
|
73
89
|
return super().default(obj)
|
|
74
90
|
|
|
75
91
|
|
|
92
|
+
class SmartTTLConfig:
|
|
93
|
+
"""智能TTL配置策略"""
|
|
94
|
+
|
|
95
|
+
# TTL策略映射表(秒)
|
|
96
|
+
TTL_STRATEGIES = {
|
|
97
|
+
# 数据库相关缓存
|
|
98
|
+
'sycm_database': 1800, # 数据库列表:30分钟
|
|
99
|
+
'sycm_tables': 1200, # 表列表:20分钟
|
|
100
|
+
'sycm_table_data': 300, # 表数据:5分钟
|
|
101
|
+
|
|
102
|
+
# 用户会话相关
|
|
103
|
+
'user_session': 3600, # 用户会话:1小时
|
|
104
|
+
'user_info': 1800, # 用户信息:30分钟
|
|
105
|
+
|
|
106
|
+
# 静态配置
|
|
107
|
+
'static_config': 86400, # 静态配置:24小时
|
|
108
|
+
'system_config': 43200, # 系统配置:12小时
|
|
109
|
+
|
|
110
|
+
# API相关
|
|
111
|
+
'api_response': 600, # API响应:10分钟
|
|
112
|
+
'api_cache': 900, # API缓存:15分钟
|
|
113
|
+
|
|
114
|
+
# 临时数据
|
|
115
|
+
'temp_data': 300, # 临时数据:5分钟
|
|
116
|
+
'short_cache': 60, # 短期缓存:1分钟
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_ttl(cls, namespace: str, default: int = 3600) -> int:
|
|
121
|
+
"""
|
|
122
|
+
根据命名空间获取对应的TTL值
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
namespace: 缓存命名空间
|
|
126
|
+
default: 默认TTL值(秒)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
int: TTL值(秒)
|
|
130
|
+
"""
|
|
131
|
+
return cls.TTL_STRATEGIES.get(namespace, default)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def add_strategy(cls, namespace: str, ttl: int):
|
|
135
|
+
"""
|
|
136
|
+
添加新的TTL策略
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
namespace: 命名空间
|
|
140
|
+
ttl: TTL值(秒)
|
|
141
|
+
"""
|
|
142
|
+
cls.TTL_STRATEGIES[namespace] = ttl
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get_all_strategies(cls) -> Dict[str, int]:
|
|
146
|
+
"""获取所有TTL策略"""
|
|
147
|
+
return cls.TTL_STRATEGIES.copy()
|
|
148
|
+
|
|
149
|
+
|
|
76
150
|
class CacheConfig:
|
|
77
151
|
"""缓存系统配置类"""
|
|
78
152
|
|
|
@@ -98,6 +172,10 @@ class CacheConfig:
|
|
|
98
172
|
self.max_value_size = 1024 * 1024 # 1MB
|
|
99
173
|
self.batch_size = 100
|
|
100
174
|
|
|
175
|
+
# 热点键配置
|
|
176
|
+
self.max_hot_keys = 1000 # 最大热点键数量
|
|
177
|
+
self.hot_keys_cleanup_threshold = 800 # 热点键清理阈值
|
|
178
|
+
|
|
101
179
|
# MySQL数据库配置
|
|
102
180
|
self.db_name = db_name
|
|
103
181
|
self.table_name = table_name
|
|
@@ -129,6 +207,11 @@ class SmartCacheSystem:
|
|
|
129
207
|
|
|
130
208
|
self.logger = logger
|
|
131
209
|
|
|
210
|
+
# 状态管理
|
|
211
|
+
self._state = CacheSystemState.INITIALIZING
|
|
212
|
+
self._ready_event = Event()
|
|
213
|
+
self._mysql_ready_event = Event()
|
|
214
|
+
|
|
132
215
|
# 统计数据
|
|
133
216
|
self.stats = {
|
|
134
217
|
'hits': 0,
|
|
@@ -150,16 +233,69 @@ class SmartCacheSystem:
|
|
|
150
233
|
self._stats_thread = None
|
|
151
234
|
self._stats_lock = threading.RLock()
|
|
152
235
|
|
|
153
|
-
#
|
|
154
|
-
self.
|
|
155
|
-
|
|
236
|
+
# 使用线程池异步初始化
|
|
237
|
+
self._executor = ThreadPoolExecutor(max_workers=2, thread_name_prefix=f"cache_{instance_name}")
|
|
238
|
+
|
|
239
|
+
# 立即设置为基础就绪状态(Redis缓存可用)
|
|
240
|
+
if self._test_redis_connection():
|
|
241
|
+
self._state = CacheSystemState.READY
|
|
242
|
+
self._ready_event.set()
|
|
243
|
+
|
|
244
|
+
# 异步初始化MySQL相关功能
|
|
245
|
+
self._executor.submit(self._initialize_mysql_features)
|
|
156
246
|
|
|
157
247
|
self.logger.info("智能缓存系统初始化完成", {
|
|
158
248
|
'instance_name': self.instance_name,
|
|
159
|
-
'
|
|
160
|
-
'
|
|
249
|
+
'state': self._state.value,
|
|
250
|
+
'redis_ready': self._ready_event.is_set(),
|
|
251
|
+
'mysql_enabled': self.mysql_pool is not None
|
|
161
252
|
})
|
|
162
253
|
|
|
254
|
+
def _initialize_mysql_features(self):
|
|
255
|
+
"""在后台初始化MySQL相关功能"""
|
|
256
|
+
try:
|
|
257
|
+
if self.mysql_pool:
|
|
258
|
+
# 延迟初始化MySQL(给系统一些启动时间)
|
|
259
|
+
time.sleep(5) # 等待5秒让主系统完全启动
|
|
260
|
+
|
|
261
|
+
if self._init_mysql_db():
|
|
262
|
+
self._state = CacheSystemState.MYSQL_READY
|
|
263
|
+
self._mysql_ready_event.set()
|
|
264
|
+
|
|
265
|
+
# MySQL就绪后再启动统计线程
|
|
266
|
+
self._start_stats_worker()
|
|
267
|
+
|
|
268
|
+
self.logger.info("MySQL功能初始化完成", {
|
|
269
|
+
'instance_name': self.instance_name,
|
|
270
|
+
'state': self._state.value
|
|
271
|
+
})
|
|
272
|
+
else:
|
|
273
|
+
self.logger.warning("MySQL初始化失败,缓存功能仍可正常使用")
|
|
274
|
+
else:
|
|
275
|
+
self.logger.info("未配置MySQL,跳过统计功能")
|
|
276
|
+
|
|
277
|
+
except Exception as e:
|
|
278
|
+
self._state = CacheSystemState.ERROR
|
|
279
|
+
self.logger.error(f"MySQL功能初始化异常: {e}")
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def is_ready(self) -> bool:
|
|
283
|
+
"""检查缓存系统是否就绪(Redis可用即为就绪)"""
|
|
284
|
+
return self._ready_event.is_set()
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def is_mysql_ready(self) -> bool:
|
|
288
|
+
"""检查MySQL功能是否就绪"""
|
|
289
|
+
return self._mysql_ready_event.is_set()
|
|
290
|
+
|
|
291
|
+
def wait_for_ready(self, timeout: float = 1.0) -> bool:
|
|
292
|
+
"""等待缓存系统就绪(非阻塞,有超时)"""
|
|
293
|
+
return self._ready_event.wait(timeout)
|
|
294
|
+
|
|
295
|
+
def wait_for_mysql_ready(self, timeout: float = 5.0) -> bool:
|
|
296
|
+
"""等待MySQL功能就绪(非阻塞,有超时)"""
|
|
297
|
+
return self._mysql_ready_event.wait(timeout)
|
|
298
|
+
|
|
163
299
|
def _test_redis_connection(self) -> bool:
|
|
164
300
|
"""测试Redis连接"""
|
|
165
301
|
try:
|
|
@@ -176,9 +312,23 @@ class SmartCacheSystem:
|
|
|
176
312
|
return False
|
|
177
313
|
|
|
178
314
|
try:
|
|
315
|
+
# 设置连接超时,避免长时间阻塞
|
|
179
316
|
connection = self.mysql_pool.connection()
|
|
317
|
+
# 设置查询超时(如果支持)
|
|
318
|
+
try:
|
|
319
|
+
connection.autocommit(False) # 确保事务控制
|
|
320
|
+
except:
|
|
321
|
+
pass # 忽略不支持的操作
|
|
322
|
+
|
|
180
323
|
try:
|
|
181
324
|
with connection.cursor() as cursor:
|
|
325
|
+
# 设置会话超时
|
|
326
|
+
try:
|
|
327
|
+
cursor.execute("SET SESSION wait_timeout = 30")
|
|
328
|
+
cursor.execute("SET SESSION interactive_timeout = 30")
|
|
329
|
+
except:
|
|
330
|
+
pass # 忽略不支持的操作
|
|
331
|
+
|
|
182
332
|
# 创建数据库
|
|
183
333
|
cursor.execute(f"CREATE DATABASE IF NOT EXISTS `{self.config.db_name}` DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci")
|
|
184
334
|
cursor.execute(f"USE `{self.config.db_name}`")
|
|
@@ -230,6 +380,7 @@ class SmartCacheSystem:
|
|
|
230
380
|
|
|
231
381
|
except Exception as e:
|
|
232
382
|
self.logger.error(f"MySQL数据库初始化失败: {e}")
|
|
383
|
+
# 不抛出异常,让系统继续运行
|
|
233
384
|
return False
|
|
234
385
|
|
|
235
386
|
def _generate_cache_key(self, key: str, namespace: str = "") -> str:
|
|
@@ -254,75 +405,106 @@ class SmartCacheSystem:
|
|
|
254
405
|
"""记录热点键"""
|
|
255
406
|
cache_key = self._generate_cache_key(key, namespace)
|
|
256
407
|
with self.hot_keys_lock:
|
|
408
|
+
# 检查是否需要清理热点键(防止内存泄漏)
|
|
409
|
+
if len(self.hot_keys) >= self.config.hot_keys_cleanup_threshold:
|
|
410
|
+
# 保留访问次数最高的键,移除访问次数最少的键
|
|
411
|
+
sorted_keys = sorted(self.hot_keys.items(), key=lambda x: x[1], reverse=True)
|
|
412
|
+
# 保留前600个热点键,为新键留出空间
|
|
413
|
+
self.hot_keys = dict(sorted_keys[:600])
|
|
414
|
+
|
|
415
|
+
self.logger.info("热点键内存清理", {
|
|
416
|
+
'清理前数量': len(sorted_keys),
|
|
417
|
+
'清理后数量': len(self.hot_keys),
|
|
418
|
+
'清理阈值': self.config.hot_keys_cleanup_threshold
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
# 记录或更新热点键访问次数
|
|
257
422
|
self.hot_keys[cache_key] = self.hot_keys.get(cache_key, 0) + 1
|
|
258
423
|
|
|
424
|
+
def _cache_operation(operation_name: str):
|
|
425
|
+
"""缓存操作装饰器工厂"""
|
|
426
|
+
def decorator(func):
|
|
427
|
+
@functools.wraps(func)
|
|
428
|
+
def wrapper(self, *args, **kwargs):
|
|
429
|
+
start_time = time.time()
|
|
430
|
+
|
|
431
|
+
# 检查系统是否就绪
|
|
432
|
+
if not self.is_ready:
|
|
433
|
+
self._record_operation('errors')
|
|
434
|
+
self.logger.warning(f"缓存系统未就绪,{operation_name}操作失败", {
|
|
435
|
+
'state': self._state.value
|
|
436
|
+
})
|
|
437
|
+
return None if operation_name == 'get' else False
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
result = func(self, *args, **kwargs)
|
|
441
|
+
response_time = (time.time() - start_time) * 1000
|
|
442
|
+
self._record_operation(operation_name, response_time)
|
|
443
|
+
return result
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
self._record_operation('errors')
|
|
447
|
+
self.logger.error(f"缓存{operation_name}操作失败: {e}", {
|
|
448
|
+
'args': args[:2] if len(args) > 2 else args, # 避免记录过长的参数
|
|
449
|
+
'operation': operation_name
|
|
450
|
+
})
|
|
451
|
+
return None if operation_name == 'get' else False
|
|
452
|
+
|
|
453
|
+
return wrapper
|
|
454
|
+
return decorator
|
|
455
|
+
|
|
456
|
+
@_cache_operation('get')
|
|
259
457
|
def get(self, key: str, namespace: str = "", default=None) -> Any:
|
|
260
458
|
"""获取缓存值"""
|
|
261
|
-
|
|
459
|
+
cache_key = self._generate_cache_key(key, namespace)
|
|
262
460
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
461
|
+
# 获取缓存值
|
|
462
|
+
value = self.redis_client.get(cache_key)
|
|
463
|
+
|
|
464
|
+
if value is not None:
|
|
465
|
+
# 缓存命中
|
|
466
|
+
self._record_hot_key(key, namespace)
|
|
269
467
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return json.loads(value.decode('utf-8'))
|
|
277
|
-
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
278
|
-
return value.decode('utf-8')
|
|
279
|
-
else:
|
|
280
|
-
# 缓存未命中
|
|
281
|
-
self._record_operation('misses', response_time)
|
|
282
|
-
return default
|
|
283
|
-
|
|
284
|
-
except Exception as e:
|
|
285
|
-
self._record_operation('errors')
|
|
286
|
-
self.logger.error(f"缓存获取失败: {e}", {
|
|
287
|
-
'key': key,
|
|
288
|
-
'namespace': namespace
|
|
289
|
-
})
|
|
468
|
+
try:
|
|
469
|
+
return json.loads(value.decode('utf-8'))
|
|
470
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
471
|
+
return value.decode('utf-8')
|
|
472
|
+
else:
|
|
473
|
+
# 缓存未命中
|
|
290
474
|
return default
|
|
291
475
|
|
|
476
|
+
@_cache_operation('set')
|
|
292
477
|
def set(self, key: str, value: Any, ttl: int = None, namespace: str = "") -> bool:
|
|
293
478
|
"""设置缓存值"""
|
|
294
|
-
|
|
479
|
+
cache_key = self._generate_cache_key(key, namespace)
|
|
295
480
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
ttl =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
except Exception as e:
|
|
319
|
-
self._record_operation('errors')
|
|
320
|
-
self.logger.error(f"缓存设置失败: {e}", {
|
|
321
|
-
'key': key,
|
|
481
|
+
# 使用智能TTL策略
|
|
482
|
+
if ttl is None:
|
|
483
|
+
ttl = SmartTTLConfig.get_ttl(namespace, self.config.default_ttl)
|
|
484
|
+
|
|
485
|
+
# 序列化值
|
|
486
|
+
if isinstance(value, (dict, list, tuple)):
|
|
487
|
+
serialized_value = json.dumps(value, ensure_ascii=False, sort_keys=True, cls=MySQLDataEncoder)
|
|
488
|
+
else:
|
|
489
|
+
serialized_value = str(value)
|
|
490
|
+
|
|
491
|
+
# 检查值大小
|
|
492
|
+
if len(serialized_value.encode('utf-8')) > self.config.max_value_size:
|
|
493
|
+
self.logger.warning(f"缓存值过大,跳过设置: {len(serialized_value)} bytes")
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
# 设置缓存
|
|
497
|
+
result = self.redis_client.setex(cache_key, ttl, serialized_value)
|
|
498
|
+
|
|
499
|
+
# 记录TTL策略使用情况
|
|
500
|
+
if namespace and ttl != self.config.default_ttl:
|
|
501
|
+
self.logger.debug("使用智能TTL策略", {
|
|
322
502
|
'namespace': namespace,
|
|
323
|
-
'ttl': ttl
|
|
503
|
+
'ttl': ttl,
|
|
504
|
+
'key': key[:50] + "..." if len(key) > 50 else key
|
|
324
505
|
})
|
|
325
|
-
|
|
506
|
+
|
|
507
|
+
return bool(result)
|
|
326
508
|
|
|
327
509
|
def delete(self, key: str, namespace: str = "") -> bool:
|
|
328
510
|
"""删除缓存值"""
|
|
@@ -437,45 +619,87 @@ class SmartCacheSystem:
|
|
|
437
619
|
|
|
438
620
|
def _start_stats_worker(self):
|
|
439
621
|
"""启动统计工作线程"""
|
|
440
|
-
if not self._stats_running:
|
|
622
|
+
if not self._stats_running and self.mysql_pool:
|
|
441
623
|
self._stats_running = True
|
|
442
|
-
self._stats_thread = threading.Thread(
|
|
624
|
+
self._stats_thread = threading.Thread(
|
|
625
|
+
target=self._stats_worker,
|
|
626
|
+
daemon=True,
|
|
627
|
+
name=f"stats_worker_{self.instance_name}"
|
|
628
|
+
)
|
|
443
629
|
self._stats_thread.start()
|
|
444
|
-
self.logger.info("统计工作线程已启动"
|
|
630
|
+
self.logger.info("统计工作线程已启动", {
|
|
631
|
+
'instance_name': self.instance_name,
|
|
632
|
+
'delay_first_run': True
|
|
633
|
+
})
|
|
445
634
|
|
|
446
635
|
def _stats_worker(self):
|
|
447
636
|
"""后台统计工作线程"""
|
|
448
|
-
cleanup_counter = 0
|
|
637
|
+
cleanup_counter = 0
|
|
638
|
+
|
|
639
|
+
# 延迟启动:等待一个完整的统计间隔
|
|
640
|
+
self.logger.info("统计线程启动,等待首个统计间隔", {
|
|
641
|
+
'instance_name': self.instance_name,
|
|
642
|
+
'interval': self.config.stats_interval,
|
|
643
|
+
'delay_reason': '避免初始化时的阻塞操作'
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
# 使用可中断的等待
|
|
647
|
+
if not self._interruptible_sleep(self.config.stats_interval):
|
|
648
|
+
return # 如果被中断则退出
|
|
649
|
+
|
|
449
650
|
while self._stats_running:
|
|
450
651
|
try:
|
|
451
652
|
# 收集统计数据
|
|
452
653
|
stats_data = self.get_stats()
|
|
453
654
|
|
|
454
|
-
# 提交到MySQL
|
|
655
|
+
# 提交到MySQL(带状态检查)
|
|
455
656
|
self._submit_stats_to_mysql(stats_data)
|
|
456
657
|
|
|
457
658
|
# 清理过期的热点键统计
|
|
458
659
|
self._cleanup_hot_keys()
|
|
459
660
|
|
|
460
|
-
#
|
|
661
|
+
# 定期清理过期数据
|
|
461
662
|
cleanup_counter += 1
|
|
462
|
-
if cleanup_counter >= 24: # 24 *
|
|
663
|
+
if cleanup_counter >= 24: # 24 * 1800秒 = 12小时
|
|
463
664
|
self._cleanup_expired_mysql_data()
|
|
464
665
|
cleanup_counter = 0
|
|
465
666
|
|
|
466
667
|
except Exception as e:
|
|
467
668
|
self.logger.error(f"统计工作线程异常: {e}")
|
|
468
669
|
|
|
469
|
-
#
|
|
470
|
-
|
|
670
|
+
# 可中断的等待下一个统计间隔
|
|
671
|
+
if not self._interruptible_sleep(self.config.stats_interval):
|
|
672
|
+
break
|
|
673
|
+
|
|
674
|
+
def _interruptible_sleep(self, duration: float) -> bool:
|
|
675
|
+
"""可中断的睡眠"""
|
|
676
|
+
sleep_interval = 1.0 # 每秒检查一次
|
|
677
|
+
elapsed = 0.0
|
|
678
|
+
|
|
679
|
+
while elapsed < duration and self._stats_running:
|
|
680
|
+
time.sleep(min(sleep_interval, duration - elapsed))
|
|
681
|
+
elapsed += sleep_interval
|
|
682
|
+
|
|
683
|
+
return self._stats_running
|
|
471
684
|
|
|
472
685
|
def _submit_stats_to_mysql(self, stats_data: Dict[str, Any]):
|
|
473
686
|
"""提交统计数据到MySQL"""
|
|
474
687
|
if not self.mysql_pool:
|
|
475
688
|
self.logger.debug("MySQL连接池不可用,跳过统计数据提交")
|
|
476
689
|
return
|
|
477
|
-
|
|
690
|
+
|
|
691
|
+
# 使用状态检查而不是属性检查
|
|
692
|
+
if not self.is_mysql_ready:
|
|
693
|
+
self.logger.debug("MySQL尚未就绪,跳过统计数据提交", {
|
|
694
|
+
'state': self._state.value,
|
|
695
|
+
'instance_name': self.instance_name
|
|
696
|
+
})
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
logger.info("统计数据", {'stats_data': stats_data})
|
|
700
|
+
|
|
478
701
|
try:
|
|
702
|
+
# 使用上下文管理器确保连接正确关闭
|
|
479
703
|
connection = self.mysql_pool.connection()
|
|
480
704
|
try:
|
|
481
705
|
with connection.cursor() as cursor:
|
|
@@ -483,7 +707,7 @@ class SmartCacheSystem:
|
|
|
483
707
|
|
|
484
708
|
# 准备数据
|
|
485
709
|
now = datetime.now()
|
|
486
|
-
date_str = now.strftime("%Y-%m-%d")
|
|
710
|
+
date_str = now.strftime("%Y-%m-%d")
|
|
487
711
|
time_period = now.strftime("%Y%m%d_%H%M")
|
|
488
712
|
|
|
489
713
|
insert_sql = f"""
|
|
@@ -519,25 +743,40 @@ class SmartCacheSystem:
|
|
|
519
743
|
|
|
520
744
|
connection.commit()
|
|
521
745
|
|
|
522
|
-
self.logger.
|
|
746
|
+
self.logger.info("统计数据已提交到MySQL", {
|
|
523
747
|
'time_period': time_period,
|
|
524
748
|
'total_operations': stats_data['total_operations'],
|
|
525
|
-
'hit_rate': stats_data['hit_rate']
|
|
749
|
+
'hit_rate': stats_data['hit_rate'],
|
|
750
|
+
'instance_name': self.instance_name
|
|
526
751
|
})
|
|
527
752
|
|
|
528
753
|
finally:
|
|
529
754
|
connection.close()
|
|
530
755
|
|
|
531
756
|
except Exception as e:
|
|
532
|
-
self.logger.error(f"提交统计数据到MySQL失败: {e}"
|
|
757
|
+
self.logger.error(f"提交统计数据到MySQL失败: {e}", {
|
|
758
|
+
'instance_name': self.instance_name,
|
|
759
|
+
'error_type': type(e).__name__
|
|
760
|
+
})
|
|
533
761
|
|
|
534
762
|
def _cleanup_hot_keys(self):
|
|
535
|
-
"""
|
|
763
|
+
"""清理热点键统计 """
|
|
536
764
|
with self.hot_keys_lock:
|
|
537
|
-
|
|
538
|
-
|
|
765
|
+
current_count = len(self.hot_keys)
|
|
766
|
+
|
|
767
|
+
# 如果热点键数量超过最大限制,进行清理
|
|
768
|
+
if current_count > self.config.max_hot_keys:
|
|
769
|
+
# 保留访问次数最高的键
|
|
539
770
|
sorted_keys = sorted(self.hot_keys.items(), key=lambda x: x[1], reverse=True)
|
|
540
|
-
|
|
771
|
+
# 保留80%的热点键
|
|
772
|
+
keep_count = int(self.config.max_hot_keys * 0.8)
|
|
773
|
+
self.hot_keys = dict(sorted_keys[:keep_count])
|
|
774
|
+
|
|
775
|
+
self.logger.info("定期热点键清理", {
|
|
776
|
+
'清理前数量': current_count,
|
|
777
|
+
'清理后数量': len(self.hot_keys),
|
|
778
|
+
'保留比例': '80%'
|
|
779
|
+
})
|
|
541
780
|
|
|
542
781
|
def _cleanup_expired_mysql_data(self):
|
|
543
782
|
"""清理过期的MySQL统计数据"""
|
|
@@ -579,21 +818,46 @@ class SmartCacheSystem:
|
|
|
579
818
|
|
|
580
819
|
def shutdown(self):
|
|
581
820
|
"""关闭缓存系统"""
|
|
582
|
-
self.logger.info("正在关闭缓存系统..."
|
|
821
|
+
self.logger.info("正在关闭缓存系统...", {
|
|
822
|
+
'instance_name': self.instance_name,
|
|
823
|
+
'state': self._state.value
|
|
824
|
+
})
|
|
583
825
|
|
|
584
826
|
# 停止统计线程
|
|
585
827
|
self._stats_running = False
|
|
586
828
|
if self._stats_thread and self._stats_thread.is_alive():
|
|
587
|
-
self.
|
|
829
|
+
self.logger.info("等待统计线程结束...")
|
|
830
|
+
self._stats_thread.join(timeout=10) # 最多等待10秒
|
|
831
|
+
|
|
832
|
+
if self._stats_thread.is_alive():
|
|
833
|
+
self.logger.warning("统计线程未能在超时时间内结束")
|
|
588
834
|
|
|
589
|
-
#
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
self.
|
|
593
|
-
except Exception as e:
|
|
594
|
-
self.logger.error(f"关闭时提交统计数据失败: {e}")
|
|
835
|
+
# 关闭线程池
|
|
836
|
+
if hasattr(self, '_executor'):
|
|
837
|
+
self.logger.info("关闭线程池...")
|
|
838
|
+
self._executor.shutdown(wait=True, timeout=5)
|
|
595
839
|
|
|
596
|
-
|
|
840
|
+
# 最后一次提交统计数据(如果MySQL可用)
|
|
841
|
+
if self.is_mysql_ready:
|
|
842
|
+
try:
|
|
843
|
+
self.logger.info("提交最终统计数据...")
|
|
844
|
+
stats_data = self.get_stats()
|
|
845
|
+
self._submit_stats_to_mysql(stats_data)
|
|
846
|
+
except Exception as e:
|
|
847
|
+
self.logger.error(f"关闭时提交统计数据失败: {e}")
|
|
848
|
+
|
|
849
|
+
self.logger.info("缓存系统已关闭", {
|
|
850
|
+
'instance_name': self.instance_name
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
def __enter__(self):
|
|
854
|
+
"""上下文管理器支持"""
|
|
855
|
+
return self
|
|
856
|
+
|
|
857
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
858
|
+
"""上下文管理器退出时自动关闭"""
|
|
859
|
+
self.shutdown()
|
|
860
|
+
return False
|
|
597
861
|
|
|
598
862
|
|
|
599
863
|
class CacheManager:
|
|
@@ -621,8 +885,9 @@ class CacheManager:
|
|
|
621
885
|
|
|
622
886
|
def initialize(self, redis_client: redis.Redis, mysql_pool=None, instance_name: str = "default",
|
|
623
887
|
config: CacheConfig = None, db_name: str = None, table_name: str = None):
|
|
624
|
-
"""
|
|
888
|
+
"""初始化缓存系统(非阻塞)"""
|
|
625
889
|
try:
|
|
890
|
+
# 立即创建缓存实例,内部会异步初始化MySQL和统计线程
|
|
626
891
|
self.cache_instance = SmartCacheSystem(
|
|
627
892
|
redis_client=redis_client,
|
|
628
893
|
mysql_pool=mysql_pool,
|
|
@@ -632,11 +897,12 @@ class CacheManager:
|
|
|
632
897
|
table_name=table_name
|
|
633
898
|
)
|
|
634
899
|
self.initialization_error = None
|
|
635
|
-
self.logger.info("
|
|
900
|
+
self.logger.info("缓存管理器初始化成功(异步模式)", {
|
|
636
901
|
'instance_name': instance_name,
|
|
637
902
|
'mysql_enabled': mysql_pool is not None,
|
|
638
903
|
'db_name': self.cache_instance.config.db_name,
|
|
639
|
-
'table_name': self.cache_instance.config.table_name
|
|
904
|
+
'table_name': self.cache_instance.config.table_name,
|
|
905
|
+
'note': 'MySQL初始化将在后台异步完成'
|
|
640
906
|
})
|
|
641
907
|
return self # 支持链式调用
|
|
642
908
|
|
mdbq-4.0.110/mdbq/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '4.0.110'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|