aury-boot 0.0.36__py3-none-any.whl → 0.0.38__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.
- aury/boot/_version.py +2 -2
- aury/boot/application/config/settings.py +4 -4
- aury/boot/commands/init.py +2 -2
- aury/boot/commands/templates/project/AGENTS.md.tpl +1 -1
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +86 -36
- aury/boot/commands/templates/project/env_templates/messaging.tpl +15 -7
- aury/boot/infrastructure/cache/backends.py +4 -422
- aury/boot/infrastructure/cache/base.py +38 -0
- aury/boot/infrastructure/cache/manager.py +151 -1
- aury/boot/infrastructure/cache/memory.py +306 -0
- aury/boot/infrastructure/cache/redis.py +259 -0
- aury/boot/infrastructure/mq/backends/redis.py +1 -1
- aury/boot/infrastructure/mq/backends/redis_stream.py +1 -1
- {aury_boot-0.0.36.dist-info → aury_boot-0.0.38.dist-info}/METADATA +1 -1
- {aury_boot-0.0.36.dist-info → aury_boot-0.0.38.dist-info}/RECORD +17 -15
- {aury_boot-0.0.36.dist-info → aury_boot-0.0.38.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.36.dist-info → aury_boot-0.0.38.dist-info}/entry_points.txt +0 -0
|
@@ -1,428 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""缓存后端实现(兼容层)。
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
实际实现在 redis.py 和 memory.py 中。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
import asyncio
|
|
9
|
-
from collections.abc import Callable
|
|
10
|
-
from datetime import timedelta
|
|
11
|
-
import json
|
|
12
|
-
import pickle
|
|
13
|
-
from typing import TYPE_CHECKING, Any
|
|
14
|
-
|
|
15
|
-
from redis.asyncio import Redis
|
|
16
|
-
|
|
17
|
-
from aury.boot.common.logging import logger
|
|
18
|
-
|
|
19
|
-
from .base import ICache
|
|
20
|
-
|
|
21
|
-
if TYPE_CHECKING:
|
|
22
|
-
from aury.boot.infrastructure.clients.redis import RedisClient
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class RedisCache(ICache):
|
|
26
|
-
"""Redis缓存实现。
|
|
27
|
-
|
|
28
|
-
支持两种初始化方式:
|
|
29
|
-
1. 传入 URL 自行创建连接
|
|
30
|
-
2. 传入 RedisClient 实例(推荐)
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def __init__(
|
|
34
|
-
self,
|
|
35
|
-
url: str | None = None,
|
|
36
|
-
*,
|
|
37
|
-
redis_client: RedisClient | None = None,
|
|
38
|
-
serializer: str = "json",
|
|
39
|
-
):
|
|
40
|
-
"""初始化Redis缓存。
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
url: Redis连接URL
|
|
44
|
-
redis_client: RedisClient 实例(推荐)
|
|
45
|
-
serializer: 序列化方式(json/pickle)
|
|
46
|
-
"""
|
|
47
|
-
self._url = url
|
|
48
|
-
self._redis_client = redis_client
|
|
49
|
-
self._serializer = serializer
|
|
50
|
-
self._redis: Redis | None = None
|
|
51
|
-
self._owns_connection = False # 是否自己拥有连接(需要自己关闭)
|
|
52
|
-
|
|
53
|
-
async def initialize(self) -> None:
|
|
54
|
-
"""初始化连接。"""
|
|
55
|
-
# 优先使用 RedisClient
|
|
56
|
-
if self._redis_client is not None:
|
|
57
|
-
self._redis = self._redis_client.connection
|
|
58
|
-
self._owns_connection = False
|
|
59
|
-
logger.info("Redis缓存初始化成功(使用 RedisClient)")
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
# 使用 URL 创建连接
|
|
63
|
-
if self._url:
|
|
64
|
-
try:
|
|
65
|
-
self._redis = Redis.from_url(
|
|
66
|
-
self._url,
|
|
67
|
-
encoding="utf-8",
|
|
68
|
-
decode_responses=False,
|
|
69
|
-
socket_connect_timeout=5,
|
|
70
|
-
socket_timeout=5,
|
|
71
|
-
)
|
|
72
|
-
await self._redis.ping()
|
|
73
|
-
self._owns_connection = True
|
|
74
|
-
logger.info("Redis缓存初始化成功")
|
|
75
|
-
except Exception as exc:
|
|
76
|
-
logger.error(f"Redis连接失败: {exc}")
|
|
77
|
-
raise
|
|
78
|
-
else:
|
|
79
|
-
raise ValueError("Redis缓存需要提供 url 或 redis_client 参数")
|
|
80
|
-
|
|
81
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
82
|
-
"""获取缓存。"""
|
|
83
|
-
if not self._redis:
|
|
84
|
-
return default
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
data = await self._redis.get(key)
|
|
88
|
-
if data is None:
|
|
89
|
-
return default
|
|
90
|
-
|
|
91
|
-
# 使用函数式编程处理序列化器
|
|
92
|
-
deserializers: dict[str, Callable[[bytes], Any]] = {
|
|
93
|
-
"json": lambda d: json.loads(d.decode()),
|
|
94
|
-
"pickle": pickle.loads,
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
deserializer = deserializers.get(self._serializer)
|
|
98
|
-
if deserializer:
|
|
99
|
-
return deserializer(data)
|
|
100
|
-
return data.decode()
|
|
101
|
-
except Exception as exc:
|
|
102
|
-
logger.error(f"Redis获取失败: {key}, {exc}")
|
|
103
|
-
return default
|
|
104
|
-
|
|
105
|
-
async def set(
|
|
106
|
-
self,
|
|
107
|
-
key: str,
|
|
108
|
-
value: Any,
|
|
109
|
-
expire: int | timedelta | None = None,
|
|
110
|
-
) -> bool:
|
|
111
|
-
"""设置缓存。"""
|
|
112
|
-
if not self._redis:
|
|
113
|
-
return False
|
|
114
|
-
|
|
115
|
-
try:
|
|
116
|
-
# 使用函数式编程处理序列化器
|
|
117
|
-
serializers: dict[str, Callable[[Any], bytes]] = {
|
|
118
|
-
"json": lambda v: json.dumps(v).encode(),
|
|
119
|
-
"pickle": pickle.dumps,
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
serializer = serializers.get(self._serializer)
|
|
123
|
-
if serializer:
|
|
124
|
-
data = serializer(value)
|
|
125
|
-
else:
|
|
126
|
-
data = str(value).encode()
|
|
127
|
-
|
|
128
|
-
# 转换过期时间
|
|
129
|
-
if isinstance(expire, timedelta):
|
|
130
|
-
expire = int(expire.total_seconds())
|
|
131
|
-
|
|
132
|
-
await self._redis.set(key, data, ex=expire)
|
|
133
|
-
return True
|
|
134
|
-
except Exception as exc:
|
|
135
|
-
logger.error(f"Redis设置失败: {key}, {exc}")
|
|
136
|
-
return False
|
|
137
|
-
|
|
138
|
-
async def delete(self, *keys: str) -> int:
|
|
139
|
-
"""删除缓存。"""
|
|
140
|
-
if not self._redis or not keys:
|
|
141
|
-
return 0
|
|
142
|
-
|
|
143
|
-
try:
|
|
144
|
-
return await self._redis.delete(*keys)
|
|
145
|
-
except Exception as exc:
|
|
146
|
-
logger.error(f"Redis删除失败: {keys}, {exc}")
|
|
147
|
-
return 0
|
|
148
|
-
|
|
149
|
-
async def exists(self, *keys: str) -> int:
|
|
150
|
-
"""检查缓存是否存在。"""
|
|
151
|
-
if not self._redis or not keys:
|
|
152
|
-
return 0
|
|
153
|
-
|
|
154
|
-
try:
|
|
155
|
-
return await self._redis.exists(*keys)
|
|
156
|
-
except Exception as exc:
|
|
157
|
-
logger.error(f"Redis检查失败: {keys}, {exc}")
|
|
158
|
-
return 0
|
|
159
|
-
|
|
160
|
-
async def clear(self) -> None:
|
|
161
|
-
"""清空所有缓存。"""
|
|
162
|
-
if self._redis:
|
|
163
|
-
await self._redis.flushdb()
|
|
164
|
-
logger.info("Redis缓存已清空")
|
|
165
|
-
|
|
166
|
-
async def delete_pattern(self, pattern: str) -> int:
|
|
167
|
-
"""按模式删除缓存。
|
|
168
|
-
|
|
169
|
-
Args:
|
|
170
|
-
pattern: 通配符模式,如 "todo:*"
|
|
171
|
-
|
|
172
|
-
Returns:
|
|
173
|
-
int: 删除的键数量
|
|
174
|
-
"""
|
|
175
|
-
if not self._redis:
|
|
176
|
-
return 0
|
|
177
|
-
|
|
178
|
-
try:
|
|
179
|
-
# 使用 SCAN 遍历匹配的键(比 KEYS 更安全,不会阻塞)
|
|
180
|
-
count = 0
|
|
181
|
-
cursor = 0
|
|
182
|
-
while True:
|
|
183
|
-
cursor, keys = await self._redis.scan(cursor, match=pattern, count=100)
|
|
184
|
-
if keys:
|
|
185
|
-
count += await self._redis.delete(*keys)
|
|
186
|
-
if cursor == 0:
|
|
187
|
-
break
|
|
188
|
-
logger.debug(f"按模式删除缓存: {pattern}, 删除 {count} 个键")
|
|
189
|
-
return count
|
|
190
|
-
except Exception as exc:
|
|
191
|
-
logger.error(f"Redis模式删除失败: {pattern}, {exc}")
|
|
192
|
-
return 0
|
|
193
|
-
|
|
194
|
-
async def close(self) -> None:
|
|
195
|
-
"""关闭连接(仅当自己拥有连接时)。"""
|
|
196
|
-
if self._redis and self._owns_connection:
|
|
197
|
-
await self._redis.close()
|
|
198
|
-
logger.info("Redis连接已关闭")
|
|
199
|
-
self._redis = None
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def redis(self) -> Redis | None:
|
|
203
|
-
"""获取Redis客户端。"""
|
|
204
|
-
return self._redis
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class MemoryCache(ICache):
|
|
208
|
-
"""内存缓存实现。"""
|
|
209
|
-
|
|
210
|
-
def __init__(self, max_size: int = 1000):
|
|
211
|
-
"""初始化内存缓存。
|
|
212
|
-
|
|
213
|
-
Args:
|
|
214
|
-
max_size: 最大缓存项数
|
|
215
|
-
"""
|
|
216
|
-
self._max_size = max_size
|
|
217
|
-
self._cache: dict[str, tuple[Any, float | None]] = {}
|
|
218
|
-
self._lock = asyncio.Lock()
|
|
219
|
-
|
|
220
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
221
|
-
"""获取缓存。"""
|
|
222
|
-
async with self._lock:
|
|
223
|
-
if key not in self._cache:
|
|
224
|
-
return default
|
|
225
|
-
|
|
226
|
-
value, expire_at = self._cache[key]
|
|
227
|
-
|
|
228
|
-
# 检查过期
|
|
229
|
-
if expire_at is not None and asyncio.get_event_loop().time() > expire_at:
|
|
230
|
-
del self._cache[key]
|
|
231
|
-
return default
|
|
232
|
-
|
|
233
|
-
return value
|
|
234
|
-
|
|
235
|
-
async def set(
|
|
236
|
-
self,
|
|
237
|
-
key: str,
|
|
238
|
-
value: Any,
|
|
239
|
-
expire: int | timedelta | None = None,
|
|
240
|
-
) -> bool:
|
|
241
|
-
"""设置缓存。"""
|
|
242
|
-
async with self._lock:
|
|
243
|
-
# 转换过期时间
|
|
244
|
-
expire_at = None
|
|
245
|
-
if expire:
|
|
246
|
-
if isinstance(expire, timedelta):
|
|
247
|
-
expire_seconds = expire.total_seconds()
|
|
248
|
-
else:
|
|
249
|
-
expire_seconds = expire
|
|
250
|
-
expire_at = asyncio.get_event_loop().time() + expire_seconds
|
|
251
|
-
|
|
252
|
-
# 如果超出容量,删除最旧的
|
|
253
|
-
if len(self._cache) >= self._max_size and key not in self._cache:
|
|
254
|
-
# 简单策略:删除第一个
|
|
255
|
-
first_key = next(iter(self._cache))
|
|
256
|
-
del self._cache[first_key]
|
|
257
|
-
|
|
258
|
-
self._cache[key] = (value, expire_at)
|
|
259
|
-
return True
|
|
260
|
-
|
|
261
|
-
async def delete(self, *keys: str) -> int:
|
|
262
|
-
"""删除缓存。"""
|
|
263
|
-
async with self._lock:
|
|
264
|
-
count = 0
|
|
265
|
-
for key in keys:
|
|
266
|
-
if key in self._cache:
|
|
267
|
-
del self._cache[key]
|
|
268
|
-
count += 1
|
|
269
|
-
return count
|
|
270
|
-
|
|
271
|
-
async def exists(self, *keys: str) -> int:
|
|
272
|
-
"""检查缓存是否存在。"""
|
|
273
|
-
async with self._lock:
|
|
274
|
-
count = 0
|
|
275
|
-
for key in keys:
|
|
276
|
-
if key in self._cache:
|
|
277
|
-
_value, expire_at = self._cache[key]
|
|
278
|
-
# 检查是否过期
|
|
279
|
-
if expire_at is None or asyncio.get_event_loop().time() <= expire_at:
|
|
280
|
-
count += 1
|
|
281
|
-
return count
|
|
282
|
-
|
|
283
|
-
async def clear(self) -> None:
|
|
284
|
-
"""清空所有缓存。"""
|
|
285
|
-
async with self._lock:
|
|
286
|
-
self._cache.clear()
|
|
287
|
-
logger.info("内存缓存已清空")
|
|
288
|
-
|
|
289
|
-
async def delete_pattern(self, pattern: str) -> int:
|
|
290
|
-
"""按模式删除缓存。
|
|
291
|
-
|
|
292
|
-
Args:
|
|
293
|
-
pattern: 通配符模式,支持 * 和 ?
|
|
294
|
-
|
|
295
|
-
Returns:
|
|
296
|
-
int: 删除的键数量
|
|
297
|
-
"""
|
|
298
|
-
import fnmatch
|
|
299
|
-
|
|
300
|
-
async with self._lock:
|
|
301
|
-
keys_to_delete = [
|
|
302
|
-
key for key in self._cache
|
|
303
|
-
if fnmatch.fnmatch(key, pattern)
|
|
304
|
-
]
|
|
305
|
-
for key in keys_to_delete:
|
|
306
|
-
del self._cache[key]
|
|
307
|
-
logger.debug(f"按模式删除缓存: {pattern}, 删除 {len(keys_to_delete)} 个键")
|
|
308
|
-
return len(keys_to_delete)
|
|
309
|
-
|
|
310
|
-
async def close(self) -> None:
|
|
311
|
-
"""关闭连接(内存缓存无需关闭)。"""
|
|
312
|
-
await self.clear()
|
|
313
|
-
|
|
314
|
-
async def size(self) -> int:
|
|
315
|
-
"""获取缓存大小。"""
|
|
316
|
-
return len(self._cache)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
class MemcachedCache(ICache):
|
|
320
|
-
"""Memcached缓存实现(可选)。"""
|
|
321
|
-
|
|
322
|
-
def __init__(self, servers: list[str]):
|
|
323
|
-
"""初始化Memcached缓存。
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
servers: Memcached服务器列表,如 ["127.0.0.1:11211"]
|
|
327
|
-
"""
|
|
328
|
-
self._servers = servers
|
|
329
|
-
self._client = None
|
|
330
|
-
|
|
331
|
-
async def initialize(self) -> None:
|
|
332
|
-
"""初始化连接。"""
|
|
333
|
-
try:
|
|
334
|
-
# 需要安装 python-memcached 或 aiomcache
|
|
335
|
-
try:
|
|
336
|
-
import aiomcache
|
|
337
|
-
self._client = aiomcache.Client(
|
|
338
|
-
self._servers[0].split(":")[0],
|
|
339
|
-
int(self._servers[0].split(":")[1]) if ":" in self._servers[0] else 11211,
|
|
340
|
-
)
|
|
341
|
-
logger.info("Memcached缓存初始化成功")
|
|
342
|
-
except ImportError:
|
|
343
|
-
logger.error("请安装 aiomcache: pip install aiomcache")
|
|
344
|
-
raise
|
|
345
|
-
except Exception as exc:
|
|
346
|
-
logger.error(f"Memcached连接失败: {exc}")
|
|
347
|
-
raise
|
|
348
|
-
|
|
349
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
350
|
-
"""获取缓存。"""
|
|
351
|
-
if not self._client:
|
|
352
|
-
return default
|
|
353
|
-
|
|
354
|
-
try:
|
|
355
|
-
data = await self._client.get(key.encode())
|
|
356
|
-
if data is None:
|
|
357
|
-
return default
|
|
358
|
-
return json.loads(data.decode())
|
|
359
|
-
except Exception as exc:
|
|
360
|
-
logger.error(f"Memcached获取失败: {key}, {exc}")
|
|
361
|
-
return default
|
|
362
|
-
|
|
363
|
-
async def set(
|
|
364
|
-
self,
|
|
365
|
-
key: str,
|
|
366
|
-
value: Any,
|
|
367
|
-
expire: int | timedelta | None = None,
|
|
368
|
-
) -> bool:
|
|
369
|
-
"""设置缓存。"""
|
|
370
|
-
if not self._client:
|
|
371
|
-
return False
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
if isinstance(expire, timedelta):
|
|
375
|
-
expire = int(expire.total_seconds())
|
|
376
|
-
|
|
377
|
-
data = json.dumps(value).encode()
|
|
378
|
-
return await self._client.set(key.encode(), data, exptime=expire or 0)
|
|
379
|
-
except Exception as exc:
|
|
380
|
-
logger.error(f"Memcached设置失败: {key}, {exc}")
|
|
381
|
-
return False
|
|
382
|
-
|
|
383
|
-
async def delete(self, *keys: str) -> int:
|
|
384
|
-
"""删除缓存。"""
|
|
385
|
-
if not self._client or not keys:
|
|
386
|
-
return 0
|
|
387
|
-
|
|
388
|
-
count = 0
|
|
389
|
-
for key in keys:
|
|
390
|
-
try:
|
|
391
|
-
if await self._client.delete(key.encode()):
|
|
392
|
-
count += 1
|
|
393
|
-
except Exception as exc:
|
|
394
|
-
logger.error(f"Memcached删除失败: {key}, {exc}")
|
|
395
|
-
return count
|
|
396
|
-
|
|
397
|
-
async def exists(self, *keys: str) -> int:
|
|
398
|
-
"""检查缓存是否存在。"""
|
|
399
|
-
if not self._client or not keys:
|
|
400
|
-
return 0
|
|
401
|
-
|
|
402
|
-
count = 0
|
|
403
|
-
for key in keys:
|
|
404
|
-
try:
|
|
405
|
-
if await self._client.get(key.encode()) is not None:
|
|
406
|
-
count += 1
|
|
407
|
-
except Exception:
|
|
408
|
-
pass
|
|
409
|
-
return count
|
|
410
|
-
|
|
411
|
-
async def clear(self) -> None:
|
|
412
|
-
"""清空所有缓存(Memcached不支持)。"""
|
|
413
|
-
logger.warning("Memcached不支持清空所有缓存")
|
|
414
|
-
|
|
415
|
-
async def delete_pattern(self, pattern: str) -> int:
|
|
416
|
-
"""按模式删除缓存(Memcached 不支持)。"""
|
|
417
|
-
logger.warning("Memcached 不支持模式删除,请使用 Redis 或 Memory 后端")
|
|
418
|
-
return 0
|
|
419
|
-
|
|
420
|
-
async def close(self) -> None:
|
|
421
|
-
"""关闭连接。"""
|
|
422
|
-
if self._client:
|
|
423
|
-
self._client.close()
|
|
424
|
-
logger.info("Memcached连接已关闭")
|
|
425
|
-
|
|
6
|
+
from .memory import MemcachedCache, MemoryCache
|
|
7
|
+
from .redis import RedisCache
|
|
426
8
|
|
|
427
9
|
__all__ = [
|
|
428
10
|
"MemcachedCache",
|
|
@@ -71,6 +71,44 @@ class ICache(ABC):
|
|
|
71
71
|
async def close(self) -> None:
|
|
72
72
|
"""关闭连接。"""
|
|
73
73
|
pass
|
|
74
|
+
|
|
75
|
+
# ==================== 分布式锁 ====================
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
async def acquire_lock(
|
|
79
|
+
self,
|
|
80
|
+
key: str,
|
|
81
|
+
token: str,
|
|
82
|
+
timeout: int,
|
|
83
|
+
blocking: bool,
|
|
84
|
+
blocking_timeout: float | None,
|
|
85
|
+
) -> bool:
|
|
86
|
+
"""获取分布式锁。
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
key: 锁的键名(已加 lock: 前缀)
|
|
90
|
+
token: 锁的 token
|
|
91
|
+
timeout: 锁的超时时间(秒)
|
|
92
|
+
blocking: 是否阻塞等待
|
|
93
|
+
blocking_timeout: 阻塞等待的最大时间(秒)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
bool: 是否获取成功
|
|
97
|
+
"""
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
async def release_lock(self, key: str, token: str) -> bool:
|
|
102
|
+
"""释放分布式锁。
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key: 锁的键名(已加 lock: 前缀)
|
|
106
|
+
token: 获取锁时的 token
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
bool: 是否成功释放
|
|
110
|
+
"""
|
|
111
|
+
pass
|
|
74
112
|
|
|
75
113
|
|
|
76
114
|
__all__ = [
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
8
9
|
from collections.abc import Callable
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
9
11
|
from datetime import timedelta
|
|
10
12
|
from functools import wraps
|
|
11
13
|
import hashlib
|
|
12
|
-
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any, AsyncIterator, TypeVar
|
|
16
|
+
import uuid
|
|
13
17
|
|
|
14
18
|
from aury.boot.common.logging import logger
|
|
15
19
|
|
|
@@ -328,6 +332,152 @@ class CacheManager:
|
|
|
328
332
|
self._backend = None
|
|
329
333
|
logger.info("缓存管理器已清理")
|
|
330
334
|
|
|
335
|
+
# ==================== 分布式锁 ====================
|
|
336
|
+
|
|
337
|
+
async def acquire_lock(
|
|
338
|
+
self,
|
|
339
|
+
key: str,
|
|
340
|
+
*,
|
|
341
|
+
timeout: int = 30,
|
|
342
|
+
blocking: bool = True,
|
|
343
|
+
blocking_timeout: float | None = None,
|
|
344
|
+
) -> str | None:
|
|
345
|
+
"""获取分布式锁。
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
key: 锁的键名
|
|
349
|
+
timeout: 锁的超时时间(秒),防止死锁
|
|
350
|
+
blocking: 是否阻塞等待
|
|
351
|
+
blocking_timeout: 阻塞等待的最大时间(秒)
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
str | None: 锁的 token(用于释放),获取失败返回 None
|
|
355
|
+
"""
|
|
356
|
+
lock_key = f"lock:{key}"
|
|
357
|
+
token = str(uuid.uuid4())
|
|
358
|
+
|
|
359
|
+
acquired = await self.backend.acquire_lock(
|
|
360
|
+
lock_key, token, timeout, blocking, blocking_timeout
|
|
361
|
+
)
|
|
362
|
+
return token if acquired else None
|
|
363
|
+
|
|
364
|
+
async def release_lock(self, key: str, token: str) -> bool:
|
|
365
|
+
"""释放分布式锁。
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
key: 锁的键名
|
|
369
|
+
token: acquire_lock 返回的 token
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
bool: 是否成功释放
|
|
373
|
+
"""
|
|
374
|
+
lock_key = f"lock:{key}"
|
|
375
|
+
return await self.backend.release_lock(lock_key, token)
|
|
376
|
+
|
|
377
|
+
@asynccontextmanager
|
|
378
|
+
async def lock(
|
|
379
|
+
self,
|
|
380
|
+
key: str,
|
|
381
|
+
*,
|
|
382
|
+
timeout: int = 30,
|
|
383
|
+
blocking: bool = True,
|
|
384
|
+
blocking_timeout: float | None = None,
|
|
385
|
+
) -> AsyncIterator[bool]:
|
|
386
|
+
"""分布式锁上下文管理器。
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
key: 锁的键名
|
|
390
|
+
timeout: 锁的超时时间(秒)
|
|
391
|
+
blocking: 是否阻塞等待
|
|
392
|
+
blocking_timeout: 阻塞等待的最大时间(秒)
|
|
393
|
+
|
|
394
|
+
Yields:
|
|
395
|
+
bool: 是否成功获取锁
|
|
396
|
+
|
|
397
|
+
示例:
|
|
398
|
+
async with cache.lock("my_resource") as acquired:
|
|
399
|
+
if acquired:
|
|
400
|
+
# 执行需要互斥的操作
|
|
401
|
+
pass
|
|
402
|
+
"""
|
|
403
|
+
token = await self.acquire_lock(
|
|
404
|
+
key,
|
|
405
|
+
timeout=timeout,
|
|
406
|
+
blocking=blocking,
|
|
407
|
+
blocking_timeout=blocking_timeout,
|
|
408
|
+
)
|
|
409
|
+
try:
|
|
410
|
+
yield token is not None
|
|
411
|
+
finally:
|
|
412
|
+
if token:
|
|
413
|
+
await self.release_lock(key, token)
|
|
414
|
+
|
|
415
|
+
@asynccontextmanager
|
|
416
|
+
async def semaphore(
|
|
417
|
+
self,
|
|
418
|
+
key: str,
|
|
419
|
+
max_concurrency: int,
|
|
420
|
+
*,
|
|
421
|
+
timeout: int = 300,
|
|
422
|
+
blocking: bool = True,
|
|
423
|
+
blocking_timeout: float | None = None,
|
|
424
|
+
) -> AsyncIterator[bool]:
|
|
425
|
+
"""分布式信号量(限制并发数)。
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
key: 信号量的键名
|
|
429
|
+
max_concurrency: 最大并发数
|
|
430
|
+
timeout: 单个槽位的超时时间(秒)
|
|
431
|
+
blocking: 是否阻塞等待
|
|
432
|
+
blocking_timeout: 阻塞等待的最大时间(秒)
|
|
433
|
+
|
|
434
|
+
Yields:
|
|
435
|
+
bool: 是否成功获取槽位
|
|
436
|
+
|
|
437
|
+
示例:
|
|
438
|
+
async with cache.semaphore("pdf_ocr", max_concurrency=2) as acquired:
|
|
439
|
+
if acquired:
|
|
440
|
+
# 执行受并发限制的操作
|
|
441
|
+
pass
|
|
442
|
+
"""
|
|
443
|
+
slot_token: str | None = None
|
|
444
|
+
acquired_slot: int | None = None
|
|
445
|
+
start_time = time.monotonic()
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
while True:
|
|
449
|
+
# 尝试获取任意一个槽位
|
|
450
|
+
for slot in range(max_concurrency):
|
|
451
|
+
slot_key = f"{key}:slot:{slot}"
|
|
452
|
+
token = await self.acquire_lock(
|
|
453
|
+
slot_key,
|
|
454
|
+
timeout=timeout,
|
|
455
|
+
blocking=False,
|
|
456
|
+
)
|
|
457
|
+
if token:
|
|
458
|
+
slot_token = token
|
|
459
|
+
acquired_slot = slot
|
|
460
|
+
yield True
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if not blocking:
|
|
464
|
+
yield False
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
# 检查是否超时
|
|
468
|
+
if blocking_timeout is not None:
|
|
469
|
+
elapsed = time.monotonic() - start_time
|
|
470
|
+
if elapsed >= blocking_timeout:
|
|
471
|
+
yield False
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
# 等待后重试
|
|
475
|
+
await asyncio.sleep(0.1)
|
|
476
|
+
finally:
|
|
477
|
+
if slot_token and acquired_slot is not None:
|
|
478
|
+
slot_key = f"{key}:slot:{acquired_slot}"
|
|
479
|
+
await self.release_lock(slot_key, slot_token)
|
|
480
|
+
|
|
331
481
|
def __repr__(self) -> str:
|
|
332
482
|
"""字符串表示。"""
|
|
333
483
|
backend_name = self.backend_type if self._backend else "未初始化"
|