nb-cache 0.2__tar.gz → 0.4__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.
- {nb_cache-0.2 → nb_cache-0.4}/PKG-INFO +207 -12
- {nb_cache-0.2 → nb_cache-0.4}/README.md +203 -3
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/cache.py +19 -4
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/key.py +27 -12
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/wrapper.py +10 -3
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache.egg-info/PKG-INFO +208 -13
- {nb_cache-0.2 → nb_cache-0.4}/pyproject.toml +3 -2
- {nb_cache-0.2 → nb_cache-0.4}/LICENSE +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/__init__.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/_compat.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/backends/__init__.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/backends/base.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/backends/dual.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/backends/memory.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/backends/redis.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/condition.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/__init__.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/bloom.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/circuit_breaker.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/early.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/failover.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/hit.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/iterator.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/locked.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/rate_limit.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/decorators/soft.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/exceptions.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/helpers.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/middleware.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/serialize.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/tags.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/transaction.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache/ttl.py +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache.egg-info/SOURCES.txt +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache.egg-info/dependency_links.txt +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache.egg-info/requires.txt +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/nb_cache.egg-info/top_level.txt +0 -0
- {nb_cache-0.2 → nb_cache-0.4}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: nb_cache
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: `nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。
|
|
5
5
|
Author: ydf0509
|
|
6
6
|
Project-URL: Homepage, https://github.com/ydf0509/nb_cache
|
|
7
7
|
Project-URL: Repository, https://github.com/ydf0509/nb_cache
|
|
@@ -18,22 +18,56 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Requires-Python: >=3.6
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
Provides-Extra: redis
|
|
25
|
-
Requires-Dist: redis>=3.0; extra == "redis"
|
|
26
26
|
Provides-Extra: speedup
|
|
27
|
-
Requires-Dist: xxhash; extra == "speedup"
|
|
28
|
-
Requires-Dist: hiredis; extra == "speedup"
|
|
29
27
|
Provides-Extra: all
|
|
30
|
-
Requires-Dist: redis>=3.0; extra == "all"
|
|
31
|
-
Requires-Dist: xxhash; extra == "all"
|
|
32
|
-
Requires-Dist: hiredis; extra == "all"
|
|
33
28
|
|
|
34
|
-
# nb_cache
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
# 🚀 nb_cache: Python 缓存界的“瑞士军刀”
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/nb-cache/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
> **`nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。**
|
|
36
|
+
|
|
37
|
+
在如今混杂着 Sync 和 Async 的 Python 项目中,开发者往往需要为同步代码和异步代码寻找不同的缓存解决方案。`nb_cache` 彻底抹平了这一差异——**只需同一个装饰器,即可完美兼容同步与异步函数**。
|
|
38
|
+
|
|
39
|
+
如果你曾经使用过 `cashews`,你会对 `nb_cache` 感到非常亲切。`nb_cache` 吸收了其优秀的特性,并弥补了其最大的短板:**全面支持同/异步场景,且所有底层操作兼具 Sync 和 Async 两种 API**。
|
|
40
|
+
|
|
41
|
+
## ✨ 核心特性
|
|
42
|
+
|
|
43
|
+
- ☯️ **同/异步无缝统一**:无需区分 `@cache` 或 `@acache`,同一个装饰器自动识别普通函数与协程,内部自动路由。同一套上下文管理器同时支持 `with` 和 `async with`。
|
|
44
|
+
- 🚀 **丰富的高级后端**:
|
|
45
|
+
- **Memory (`mem://`)**: 极速的本地 LRU 内存缓存。
|
|
46
|
+
- **Redis (`redis://`)**: 分布式 Redis 缓存,支持连接池。
|
|
47
|
+
- **双层缓存 (`dual://`)**: **(杀手级特性)** 本地内存(L1) + Redis(L2) 的透明双重缓存。读取时优先击中内存,写入时双写,彻底释放 Redis 压力。
|
|
48
|
+
- 🛡️ **企业级高可用防护**:
|
|
49
|
+
- **防缓存击穿 (Stampede)**:只需一个参数 `lock=True`,即可在并发未命中时让多余请求等待,只放行一个请求去查库。
|
|
50
|
+
- **防缓存雪崩 (Avalanche)**:提供 `@cache.early` (提前后台刷新) 和 `@cache.soft` (软过期,返回旧值并异步刷新) 完美解决雪崩。
|
|
51
|
+
- **服务降级与失败回退**:提供 `@cache.failover`,当数据库或下游接口挂掉时,自动返回缓存的旧值兜底。
|
|
52
|
+
- 🔧 **极其丰富的“微服务”级装饰器**:除了缓存,还内置了**限流** (`rate_limit`, `slice_rate_limit`)、**熔断** (`circuit_breaker`)、**并发防抖** (`thunder_protection`)、**布隆过滤器** (`bloom`, `dual_bloom`)。
|
|
53
|
+
- 🔑 **智能 Key 路由与模板**:告别繁琐的 key 拼接。支持 `{user_id}`、`{user.name}` (直接读取对象属性)、`{data:hash}` (自动对大字典算md5) 等高级格式化模板。
|
|
54
|
+
- 🔒 **数据安全与压缩**:自带序列化流水线。一行配置即可开启 JSON/Pickle 序列化、Gzip/Zlib 压缩,以及 HMAC 签名(防止缓存数据被恶意篡改)。
|
|
55
|
+
- 🏷️ **标签系统与事务**:支持给缓存打标签 (Tags) 实现按业务模块批量失效,支持类似数据库的缓存事务 (Transaction) 自动回滚。
|
|
56
|
+
|
|
57
|
+
## 💡 为什么选择 nb_cache?
|
|
58
|
+
|
|
59
|
+
| 特性 | 传统自带 `lru_cache` | `redis-py` 原生 | `cashews` | 🏆 `nb_cache` |
|
|
60
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
61
|
+
| **同步函数支持** | ✅ | ✅ | ❌ | **✅ 完美支持** |
|
|
62
|
+
| **异步函数 (Asyncio) 支持** | ❌ | ✅ | ✅ | **✅ 完美支持** |
|
|
63
|
+
| **内存/Redis 双重缓存** | ❌ | ❌ | ✅ | **✅ 开箱即用** |
|
|
64
|
+
| **防击穿 (分布式锁合并请求)** | ❌ | 需手写代码 | ✅ | **✅ `lock=True`** |
|
|
65
|
+
| **防雪崩/后台自动刷新** | ❌ | 需手写代码 | ✅ | **✅ `@cache.early`** |
|
|
66
|
+
| **限流与熔断** | ❌ | 需手写代码 | ✅ | **✅ 内置支持** |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 👉 接下来,请看下方的【安装】与【快速开始】,体验一行代码带来的架构升级:
|
|
37
71
|
|
|
38
72
|
## 安装
|
|
39
73
|
|
|
@@ -66,12 +100,20 @@ def get_user(user_id):
|
|
|
66
100
|
async def get_user_async(user_id):
|
|
67
101
|
return await db.query_async(user_id)
|
|
68
102
|
|
|
69
|
-
#
|
|
103
|
+
# 加锁防止缓存击穿,如果123这个入参没有缓存,但是同一秒请求123这个入参1万次,
|
|
104
|
+
# 加上lock=True后,只有第一次请求会真正执行函数,其余请求等待并复用第一次请求的结果,避免"击穿"。
|
|
70
105
|
@cache.cache(ttl=60, lock=True)
|
|
71
106
|
def get_hot_data(key):
|
|
107
|
+
time.sleep(20)
|
|
72
108
|
return expensive_query(key)
|
|
73
109
|
```
|
|
74
110
|
|
|
111
|
+
## 不想吃苦,如何使用ai掌握nb_cache?
|
|
112
|
+
|
|
113
|
+
`nb_cache_all_docs_and_codes.md` 这个文件包含了nb_cache 的教程和全部源码。
|
|
114
|
+
你把这个文件发送给deepseek ai [https://chat.deepseek.com/](https://chat.deepseek.com/) ,ai就能自动帮你掌握 `nb_cache` 的用法。
|
|
115
|
+
|
|
116
|
+
|
|
75
117
|
## 对比 cashews
|
|
76
118
|
|
|
77
119
|
如果你不懂 `nb_cache` 用法,可以参考 `cashews` 的用法。ai很熟练 `cashews`的用法。
|
|
@@ -665,6 +707,94 @@ def check_permission(user, action):
|
|
|
665
707
|
return db.query_permission(user['id'], action)
|
|
666
708
|
```
|
|
667
709
|
|
|
710
|
+
### key_include_func 参数说明
|
|
711
|
+
|
|
712
|
+
默认情况下,`nb_cache` 会把 **模块路径 + 函数名** 自动拼入 cache key,以确保不同模块的同名函数不会冲突:
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
# 默认生成的 key(含函数信息)
|
|
716
|
+
testp2:myapp.services:get_user:user_id:42
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
如果你已经通过 `key=` 参数自己指定了业务 key 模板,这段模块+函数前缀往往是多余的噪音。
|
|
720
|
+
设置 `key_include_func=False` 后,key 只保留业务部分:
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
# key_include_func=False 后生成的 key
|
|
724
|
+
testp2:user:42
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### 设置级别
|
|
728
|
+
|
|
729
|
+
`key_include_func` 支持两个层级,**装饰器上的值优先于 `setup()` 的默认值**。
|
|
730
|
+
|
|
731
|
+
**1. `setup()` 级别 —— 影响该实例下所有装饰器**
|
|
732
|
+
|
|
733
|
+
```python
|
|
734
|
+
from nb_cache import Cache
|
|
735
|
+
|
|
736
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp", key_include_func=False)
|
|
737
|
+
|
|
738
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
739
|
+
def get_user(user_id):
|
|
740
|
+
return db.query(user_id)
|
|
741
|
+
# final_key → myapp:user:42
|
|
742
|
+
|
|
743
|
+
@cache.cache(ttl=60, key="order:{order_id}")
|
|
744
|
+
def get_order(order_id):
|
|
745
|
+
return db.query_order(order_id)
|
|
746
|
+
# final_key → myapp:order:100
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**2. 装饰器级别 —— 覆盖 `setup()` 的默认值,只影响当前函数**
|
|
750
|
+
|
|
751
|
+
```python
|
|
752
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp")
|
|
753
|
+
# 默认 key_include_func=True
|
|
754
|
+
|
|
755
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
756
|
+
def get_user(user_id):
|
|
757
|
+
...
|
|
758
|
+
# final_key → myapp:mymodule:get_user:user:{user_id} → myapp:mymodule:get_user:user:42
|
|
759
|
+
|
|
760
|
+
@cache.cache(ttl=60, key="order:{order_id}", key_include_func=False)
|
|
761
|
+
def get_order(order_id):
|
|
762
|
+
...
|
|
763
|
+
# final_key → myapp:order:100 (单独关闭,不含函数名)
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### 不指定 key= 时的行为
|
|
767
|
+
|
|
768
|
+
当不传 `key=` 参数,`nb_cache` 会根据函数签名自动生成 key。
|
|
769
|
+
此时 `key_include_func=False` 意味着 key 只由参数值组成,**极易碰撞**,不推荐在此场景下使用:
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
# 不推荐:不指定 key= 且 key_include_func=False
|
|
773
|
+
@cache.cache(ttl=60, key_include_func=False)
|
|
774
|
+
def get_user(user_id):
|
|
775
|
+
...
|
|
776
|
+
# final_key → myapp:user_id=42 ← 与其他相同签名函数会冲突
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
#### 如何预览生成的 key(不调用函数)
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
783
|
+
|
|
784
|
+
# 方式1:从已装饰函数取模板(推荐)
|
|
785
|
+
template = get_user._cache_key_template
|
|
786
|
+
logic_key = get_cache_key(get_user, template, (42,), {})
|
|
787
|
+
final_key = cache._backend._make_key(logic_key)
|
|
788
|
+
print(final_key) # myapp:user:42
|
|
789
|
+
|
|
790
|
+
# 方式2:直接用工具函数生成(不需要先装饰)
|
|
791
|
+
tpl = get_cache_key_template(get_user, key="user:{user_id}", key_include_func=False)
|
|
792
|
+
key = get_cache_key(get_user, tpl, (42,), {})
|
|
793
|
+
print(key) # user:42
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
668
798
|
## 锁(上下文管理器)
|
|
669
799
|
|
|
670
800
|
```python
|
|
@@ -775,6 +905,71 @@ def get_data():
|
|
|
775
905
|
return {"name": "test"}
|
|
776
906
|
```
|
|
777
907
|
|
|
908
|
+
## 常见问题解答
|
|
909
|
+
|
|
910
|
+
### 问题1:如何查看缓存最终生成的key是什么?
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
#### 方式一:通过日志查看
|
|
914
|
+
```
|
|
915
|
+
因为nb_cache 已经在 nb_cache.cache 日志命名空间,用debug 日志级别打印了最终生成的key。
|
|
916
|
+
|
|
917
|
+
所以你可以通过nb_log来查看:
|
|
918
|
+
nb_log.get_logger('nb_cache.cache')
|
|
919
|
+
|
|
920
|
+
也可以通过 原生logging 来查看:
|
|
921
|
+
logger = logging.getLogger("nb_cache.cache")
|
|
922
|
+
logger.setLevel(logging.DEBUG)
|
|
923
|
+
logger.addHandler(logging.StreamHandler())
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
日志例子:
|
|
927
|
+
```
|
|
928
|
+
2026-02-28 18:46:01 - nb_cache.cache - "D:\codes\nb_cache\nb_cache\decorators\cache.py:61" - async_wrapper - DEBUG - [nb_cache] func=__main__:aio_fun final_key=testp2:__main__:aio_fun:aiof:3_4 ttl=700.0
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
#### 方式二:不调用函数,直接预览 cache key
|
|
932
|
+
|
|
933
|
+
```python
|
|
934
|
+
# -*- coding: utf-8 -*-
|
|
935
|
+
"""nb_cache key 生成 Demo"""
|
|
936
|
+
import sys
|
|
937
|
+
import asyncio
|
|
938
|
+
from nb_cache import Cache
|
|
939
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
940
|
+
import nb_log
|
|
941
|
+
|
|
942
|
+
nb_log.get_logger("nb_cache.cache")
|
|
943
|
+
|
|
944
|
+
if sys.platform == "win32":
|
|
945
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
946
|
+
|
|
947
|
+
cache = Cache()
|
|
948
|
+
cache.setup("redis://", prefix="testp2")
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@cache.cache(ttl=700,key='sf:{a}_{b}')
|
|
952
|
+
def simple_func(a, b):
|
|
953
|
+
return a + b
|
|
954
|
+
|
|
955
|
+
print(simple_func(1, 2))
|
|
956
|
+
|
|
957
|
+
# --- 不调用函数,直接预览 cache key ---
|
|
958
|
+
# 方式1:从装饰后的函数取模板属性(推荐)
|
|
959
|
+
template = simple_func._cache_key_template
|
|
960
|
+
logic_key = get_cache_key(simple_func, template, (1, 2), {})
|
|
961
|
+
final_key = cache._backend._make_key(logic_key)
|
|
962
|
+
print("logic_key:", logic_key) # 不含 prefix
|
|
963
|
+
print("final_key:", final_key) # 含 prefix,与 Redis 中一致
|
|
964
|
+
|
|
965
|
+
# 方式2:用工具函数直接生成,不需要先装饰
|
|
966
|
+
def raw_func(a, b):
|
|
967
|
+
return a + b
|
|
968
|
+
tpl = get_cache_key_template(raw_func, key='sf:{a}_{b}')
|
|
969
|
+
key2 = get_cache_key(raw_func, tpl, (1, 2), {})
|
|
970
|
+
print("key2 (无prefix):", key2)
|
|
971
|
+
```
|
|
972
|
+
|
|
778
973
|
## 许可证
|
|
779
974
|
|
|
780
975
|
MIT License
|
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
# nb_cache
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
# 🚀 nb_cache: Python 缓存界的“瑞士军刀”
|
|
3
|
+
|
|
4
|
+
[](https://pypi.org/project/nb-cache/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
> **`nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。**
|
|
8
|
+
|
|
9
|
+
在如今混杂着 Sync 和 Async 的 Python 项目中,开发者往往需要为同步代码和异步代码寻找不同的缓存解决方案。`nb_cache` 彻底抹平了这一差异——**只需同一个装饰器,即可完美兼容同步与异步函数**。
|
|
10
|
+
|
|
11
|
+
如果你曾经使用过 `cashews`,你会对 `nb_cache` 感到非常亲切。`nb_cache` 吸收了其优秀的特性,并弥补了其最大的短板:**全面支持同/异步场景,且所有底层操作兼具 Sync 和 Async 两种 API**。
|
|
12
|
+
|
|
13
|
+
## ✨ 核心特性
|
|
14
|
+
|
|
15
|
+
- ☯️ **同/异步无缝统一**:无需区分 `@cache` 或 `@acache`,同一个装饰器自动识别普通函数与协程,内部自动路由。同一套上下文管理器同时支持 `with` 和 `async with`。
|
|
16
|
+
- 🚀 **丰富的高级后端**:
|
|
17
|
+
- **Memory (`mem://`)**: 极速的本地 LRU 内存缓存。
|
|
18
|
+
- **Redis (`redis://`)**: 分布式 Redis 缓存,支持连接池。
|
|
19
|
+
- **双层缓存 (`dual://`)**: **(杀手级特性)** 本地内存(L1) + Redis(L2) 的透明双重缓存。读取时优先击中内存,写入时双写,彻底释放 Redis 压力。
|
|
20
|
+
- 🛡️ **企业级高可用防护**:
|
|
21
|
+
- **防缓存击穿 (Stampede)**:只需一个参数 `lock=True`,即可在并发未命中时让多余请求等待,只放行一个请求去查库。
|
|
22
|
+
- **防缓存雪崩 (Avalanche)**:提供 `@cache.early` (提前后台刷新) 和 `@cache.soft` (软过期,返回旧值并异步刷新) 完美解决雪崩。
|
|
23
|
+
- **服务降级与失败回退**:提供 `@cache.failover`,当数据库或下游接口挂掉时,自动返回缓存的旧值兜底。
|
|
24
|
+
- 🔧 **极其丰富的“微服务”级装饰器**:除了缓存,还内置了**限流** (`rate_limit`, `slice_rate_limit`)、**熔断** (`circuit_breaker`)、**并发防抖** (`thunder_protection`)、**布隆过滤器** (`bloom`, `dual_bloom`)。
|
|
25
|
+
- 🔑 **智能 Key 路由与模板**:告别繁琐的 key 拼接。支持 `{user_id}`、`{user.name}` (直接读取对象属性)、`{data:hash}` (自动对大字典算md5) 等高级格式化模板。
|
|
26
|
+
- 🔒 **数据安全与压缩**:自带序列化流水线。一行配置即可开启 JSON/Pickle 序列化、Gzip/Zlib 压缩,以及 HMAC 签名(防止缓存数据被恶意篡改)。
|
|
27
|
+
- 🏷️ **标签系统与事务**:支持给缓存打标签 (Tags) 实现按业务模块批量失效,支持类似数据库的缓存事务 (Transaction) 自动回滚。
|
|
28
|
+
|
|
29
|
+
## 💡 为什么选择 nb_cache?
|
|
30
|
+
|
|
31
|
+
| 特性 | 传统自带 `lru_cache` | `redis-py` 原生 | `cashews` | 🏆 `nb_cache` |
|
|
32
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
33
|
+
| **同步函数支持** | ✅ | ✅ | ❌ | **✅ 完美支持** |
|
|
34
|
+
| **异步函数 (Asyncio) 支持** | ❌ | ✅ | ✅ | **✅ 完美支持** |
|
|
35
|
+
| **内存/Redis 双重缓存** | ❌ | ❌ | ✅ | **✅ 开箱即用** |
|
|
36
|
+
| **防击穿 (分布式锁合并请求)** | ❌ | 需手写代码 | ✅ | **✅ `lock=True`** |
|
|
37
|
+
| **防雪崩/后台自动刷新** | ❌ | 需手写代码 | ✅ | **✅ `@cache.early`** |
|
|
38
|
+
| **限流与熔断** | ❌ | 需手写代码 | ✅ | **✅ 内置支持** |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### 👉 接下来,请看下方的【安装】与【快速开始】,体验一行代码带来的架构升级:
|
|
4
43
|
|
|
5
44
|
## 安装
|
|
6
45
|
|
|
@@ -33,12 +72,20 @@ def get_user(user_id):
|
|
|
33
72
|
async def get_user_async(user_id):
|
|
34
73
|
return await db.query_async(user_id)
|
|
35
74
|
|
|
36
|
-
#
|
|
75
|
+
# 加锁防止缓存击穿,如果123这个入参没有缓存,但是同一秒请求123这个入参1万次,
|
|
76
|
+
# 加上lock=True后,只有第一次请求会真正执行函数,其余请求等待并复用第一次请求的结果,避免"击穿"。
|
|
37
77
|
@cache.cache(ttl=60, lock=True)
|
|
38
78
|
def get_hot_data(key):
|
|
79
|
+
time.sleep(20)
|
|
39
80
|
return expensive_query(key)
|
|
40
81
|
```
|
|
41
82
|
|
|
83
|
+
## 不想吃苦,如何使用ai掌握nb_cache?
|
|
84
|
+
|
|
85
|
+
`nb_cache_all_docs_and_codes.md` 这个文件包含了nb_cache 的教程和全部源码。
|
|
86
|
+
你把这个文件发送给deepseek ai [https://chat.deepseek.com/](https://chat.deepseek.com/) ,ai就能自动帮你掌握 `nb_cache` 的用法。
|
|
87
|
+
|
|
88
|
+
|
|
42
89
|
## 对比 cashews
|
|
43
90
|
|
|
44
91
|
如果你不懂 `nb_cache` 用法,可以参考 `cashews` 的用法。ai很熟练 `cashews`的用法。
|
|
@@ -632,6 +679,94 @@ def check_permission(user, action):
|
|
|
632
679
|
return db.query_permission(user['id'], action)
|
|
633
680
|
```
|
|
634
681
|
|
|
682
|
+
### key_include_func 参数说明
|
|
683
|
+
|
|
684
|
+
默认情况下,`nb_cache` 会把 **模块路径 + 函数名** 自动拼入 cache key,以确保不同模块的同名函数不会冲突:
|
|
685
|
+
|
|
686
|
+
```
|
|
687
|
+
# 默认生成的 key(含函数信息)
|
|
688
|
+
testp2:myapp.services:get_user:user_id:42
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
如果你已经通过 `key=` 参数自己指定了业务 key 模板,这段模块+函数前缀往往是多余的噪音。
|
|
692
|
+
设置 `key_include_func=False` 后,key 只保留业务部分:
|
|
693
|
+
|
|
694
|
+
```
|
|
695
|
+
# key_include_func=False 后生成的 key
|
|
696
|
+
testp2:user:42
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### 设置级别
|
|
700
|
+
|
|
701
|
+
`key_include_func` 支持两个层级,**装饰器上的值优先于 `setup()` 的默认值**。
|
|
702
|
+
|
|
703
|
+
**1. `setup()` 级别 —— 影响该实例下所有装饰器**
|
|
704
|
+
|
|
705
|
+
```python
|
|
706
|
+
from nb_cache import Cache
|
|
707
|
+
|
|
708
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp", key_include_func=False)
|
|
709
|
+
|
|
710
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
711
|
+
def get_user(user_id):
|
|
712
|
+
return db.query(user_id)
|
|
713
|
+
# final_key → myapp:user:42
|
|
714
|
+
|
|
715
|
+
@cache.cache(ttl=60, key="order:{order_id}")
|
|
716
|
+
def get_order(order_id):
|
|
717
|
+
return db.query_order(order_id)
|
|
718
|
+
# final_key → myapp:order:100
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
**2. 装饰器级别 —— 覆盖 `setup()` 的默认值,只影响当前函数**
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp")
|
|
725
|
+
# 默认 key_include_func=True
|
|
726
|
+
|
|
727
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
728
|
+
def get_user(user_id):
|
|
729
|
+
...
|
|
730
|
+
# final_key → myapp:mymodule:get_user:user:{user_id} → myapp:mymodule:get_user:user:42
|
|
731
|
+
|
|
732
|
+
@cache.cache(ttl=60, key="order:{order_id}", key_include_func=False)
|
|
733
|
+
def get_order(order_id):
|
|
734
|
+
...
|
|
735
|
+
# final_key → myapp:order:100 (单独关闭,不含函数名)
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
#### 不指定 key= 时的行为
|
|
739
|
+
|
|
740
|
+
当不传 `key=` 参数,`nb_cache` 会根据函数签名自动生成 key。
|
|
741
|
+
此时 `key_include_func=False` 意味着 key 只由参数值组成,**极易碰撞**,不推荐在此场景下使用:
|
|
742
|
+
|
|
743
|
+
```python
|
|
744
|
+
# 不推荐:不指定 key= 且 key_include_func=False
|
|
745
|
+
@cache.cache(ttl=60, key_include_func=False)
|
|
746
|
+
def get_user(user_id):
|
|
747
|
+
...
|
|
748
|
+
# final_key → myapp:user_id=42 ← 与其他相同签名函数会冲突
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
#### 如何预览生成的 key(不调用函数)
|
|
752
|
+
|
|
753
|
+
```python
|
|
754
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
755
|
+
|
|
756
|
+
# 方式1:从已装饰函数取模板(推荐)
|
|
757
|
+
template = get_user._cache_key_template
|
|
758
|
+
logic_key = get_cache_key(get_user, template, (42,), {})
|
|
759
|
+
final_key = cache._backend._make_key(logic_key)
|
|
760
|
+
print(final_key) # myapp:user:42
|
|
761
|
+
|
|
762
|
+
# 方式2:直接用工具函数生成(不需要先装饰)
|
|
763
|
+
tpl = get_cache_key_template(get_user, key="user:{user_id}", key_include_func=False)
|
|
764
|
+
key = get_cache_key(get_user, tpl, (42,), {})
|
|
765
|
+
print(key) # user:42
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
635
770
|
## 锁(上下文管理器)
|
|
636
771
|
|
|
637
772
|
```python
|
|
@@ -742,6 +877,71 @@ def get_data():
|
|
|
742
877
|
return {"name": "test"}
|
|
743
878
|
```
|
|
744
879
|
|
|
880
|
+
## 常见问题解答
|
|
881
|
+
|
|
882
|
+
### 问题1:如何查看缓存最终生成的key是什么?
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
#### 方式一:通过日志查看
|
|
886
|
+
```
|
|
887
|
+
因为nb_cache 已经在 nb_cache.cache 日志命名空间,用debug 日志级别打印了最终生成的key。
|
|
888
|
+
|
|
889
|
+
所以你可以通过nb_log来查看:
|
|
890
|
+
nb_log.get_logger('nb_cache.cache')
|
|
891
|
+
|
|
892
|
+
也可以通过 原生logging 来查看:
|
|
893
|
+
logger = logging.getLogger("nb_cache.cache")
|
|
894
|
+
logger.setLevel(logging.DEBUG)
|
|
895
|
+
logger.addHandler(logging.StreamHandler())
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
日志例子:
|
|
899
|
+
```
|
|
900
|
+
2026-02-28 18:46:01 - nb_cache.cache - "D:\codes\nb_cache\nb_cache\decorators\cache.py:61" - async_wrapper - DEBUG - [nb_cache] func=__main__:aio_fun final_key=testp2:__main__:aio_fun:aiof:3_4 ttl=700.0
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
#### 方式二:不调用函数,直接预览 cache key
|
|
904
|
+
|
|
905
|
+
```python
|
|
906
|
+
# -*- coding: utf-8 -*-
|
|
907
|
+
"""nb_cache key 生成 Demo"""
|
|
908
|
+
import sys
|
|
909
|
+
import asyncio
|
|
910
|
+
from nb_cache import Cache
|
|
911
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
912
|
+
import nb_log
|
|
913
|
+
|
|
914
|
+
nb_log.get_logger("nb_cache.cache")
|
|
915
|
+
|
|
916
|
+
if sys.platform == "win32":
|
|
917
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
918
|
+
|
|
919
|
+
cache = Cache()
|
|
920
|
+
cache.setup("redis://", prefix="testp2")
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
@cache.cache(ttl=700,key='sf:{a}_{b}')
|
|
924
|
+
def simple_func(a, b):
|
|
925
|
+
return a + b
|
|
926
|
+
|
|
927
|
+
print(simple_func(1, 2))
|
|
928
|
+
|
|
929
|
+
# --- 不调用函数,直接预览 cache key ---
|
|
930
|
+
# 方式1:从装饰后的函数取模板属性(推荐)
|
|
931
|
+
template = simple_func._cache_key_template
|
|
932
|
+
logic_key = get_cache_key(simple_func, template, (1, 2), {})
|
|
933
|
+
final_key = cache._backend._make_key(logic_key)
|
|
934
|
+
print("logic_key:", logic_key) # 不含 prefix
|
|
935
|
+
print("final_key:", final_key) # 含 prefix,与 Redis 中一致
|
|
936
|
+
|
|
937
|
+
# 方式2:用工具函数直接生成,不需要先装饰
|
|
938
|
+
def raw_func(a, b):
|
|
939
|
+
return a + b
|
|
940
|
+
tpl = get_cache_key_template(raw_func, key='sf:{a}_{b}')
|
|
941
|
+
key2 = get_cache_key(raw_func, tpl, (1, 2), {})
|
|
942
|
+
print("key2 (无prefix):", key2)
|
|
943
|
+
```
|
|
944
|
+
|
|
745
945
|
## 许可证
|
|
746
946
|
|
|
747
947
|
MIT License
|
|
@@ -2,18 +2,27 @@
|
|
|
2
2
|
"""Basic cache decorator with sync/async support and optional locking."""
|
|
3
3
|
import asyncio
|
|
4
4
|
import functools
|
|
5
|
-
import
|
|
5
|
+
import logging
|
|
6
6
|
|
|
7
7
|
from nb_cache._compat import is_coroutine_function
|
|
8
8
|
from nb_cache.condition import get_cache_condition
|
|
9
|
-
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
9
|
+
from nb_cache.key import get_cache_key, get_cache_key_template, get_func_name
|
|
10
10
|
from nb_cache.serialize import default_serializer, _SENTINEL
|
|
11
11
|
from nb_cache.ttl import ttl_to_seconds
|
|
12
12
|
|
|
13
|
+
logger = logging.getLogger("nb_cache.cache")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _final_key(be, cache_key):
|
|
17
|
+
"""通过 backend 的 _make_key 方法获取最终写入存储的完整 key。"""
|
|
18
|
+
if hasattr(be, '_make_key'):
|
|
19
|
+
return be._make_key(cache_key)
|
|
20
|
+
return cache_key
|
|
21
|
+
|
|
13
22
|
|
|
14
23
|
def cache(ttl, key=None, condition=None, prefix="", lock=False,
|
|
15
24
|
lock_ttl=None, tags=(), backend=None, serializer=None,
|
|
16
|
-
tag_registry=None):
|
|
25
|
+
tag_registry=None, key_include_func=True):
|
|
17
26
|
"""Basic cache decorator.
|
|
18
27
|
|
|
19
28
|
Supports both sync and async functions transparently.
|
|
@@ -29,6 +38,8 @@ def cache(ttl, key=None, condition=None, prefix="", lock=False,
|
|
|
29
38
|
backend: Cache backend instance. If None, uses the global default.
|
|
30
39
|
serializer: Serializer instance. If None, uses default.
|
|
31
40
|
tag_registry: TagRegistry instance for tag-based invalidation.
|
|
41
|
+
key_include_func: If False, module path and function name are excluded
|
|
42
|
+
from the generated key. Default True.
|
|
32
43
|
"""
|
|
33
44
|
_condition = get_cache_condition(condition)
|
|
34
45
|
_serializer = serializer or default_serializer
|
|
@@ -36,7 +47,7 @@ def cache(ttl, key=None, condition=None, prefix="", lock=False,
|
|
|
36
47
|
_lock_ttl = ttl_to_seconds(lock_ttl) if lock_ttl else _ttl_seconds
|
|
37
48
|
|
|
38
49
|
def decorator(func):
|
|
39
|
-
_key_template = get_cache_key_template(func, key, prefix)
|
|
50
|
+
_key_template = get_cache_key_template(func, key, prefix, key_include_func=key_include_func)
|
|
40
51
|
_backend_ref = [backend]
|
|
41
52
|
_registry = tag_registry
|
|
42
53
|
|
|
@@ -49,6 +60,8 @@ def cache(ttl, key=None, condition=None, prefix="", lock=False,
|
|
|
49
60
|
async def async_wrapper(*args, **kwargs):
|
|
50
61
|
be = _get_backend()
|
|
51
62
|
cache_key = get_cache_key(func, _key_template, args, kwargs)
|
|
63
|
+
logger.debug("[nb_cache] func=%s final_key=%s ttl=%s",
|
|
64
|
+
get_func_name(func), _final_key(be, cache_key), _ttl_seconds)
|
|
52
65
|
|
|
53
66
|
raw = await be.get(cache_key)
|
|
54
67
|
if raw is not None:
|
|
@@ -91,6 +104,8 @@ def cache(ttl, key=None, condition=None, prefix="", lock=False,
|
|
|
91
104
|
def sync_wrapper(*args, **kwargs):
|
|
92
105
|
be = _get_backend()
|
|
93
106
|
cache_key = get_cache_key(func, _key_template, args, kwargs)
|
|
107
|
+
logger.debug("[nb_cache] func=%s final_key=%s ttl=%s",
|
|
108
|
+
get_func_name(func), _final_key(be, cache_key), _ttl_seconds)
|
|
94
109
|
|
|
95
110
|
raw = be.get_sync(cache_key)
|
|
96
111
|
if raw is not None:
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
"""Cache key generation and template utilities."""
|
|
3
3
|
import hashlib
|
|
4
4
|
import inspect
|
|
5
|
-
import logging
|
|
6
5
|
import re
|
|
7
6
|
|
|
8
7
|
# Matches {param}, {param:fmt}, {param.attr}, {param.attr:fmt}
|
|
9
8
|
_TEMPLATE_PARAM_RE = re.compile(r'\{([\w.]+)(?::(\w+))?\}')
|
|
10
9
|
|
|
11
|
-
logger = logging.getLogger("nb_cache.key")
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
def get_func_name(func):
|
|
15
12
|
"""Get a stable, qualified name for a function."""
|
|
@@ -35,24 +32,37 @@ def get_cache_key(func, key_template, args, kwargs):
|
|
|
35
32
|
else:
|
|
36
33
|
key = _render_template(func, key_template, args, kwargs)
|
|
37
34
|
|
|
38
|
-
logger.debug("[nb_cache] func=%s key=%s", get_func_name(func), key)
|
|
39
35
|
return key
|
|
40
36
|
|
|
41
37
|
|
|
42
|
-
def get_cache_key_template(func, key=None, prefix=""):
|
|
38
|
+
def get_cache_key_template(func, key=None, prefix="", key_include_func=True):
|
|
43
39
|
"""Build a key template (string or callable) for a function.
|
|
44
40
|
|
|
45
41
|
When key is a callable, it is stored as-is and will be called at cache time.
|
|
46
|
-
When key is a string template, it is prefixed with func_name.
|
|
42
|
+
When key is a string template, it is prefixed with func_name (if include_func_name=True).
|
|
47
43
|
When key is None, auto-generate a template from the function signature.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
func: The decorated function.
|
|
47
|
+
key: Key template string or callable. None means auto-generate.
|
|
48
|
+
prefix: Key prefix string (from decorator or setup).
|
|
49
|
+
key_include_func: If False, the module path and function name are NOT
|
|
50
|
+
included in the generated key. Useful when you want a short,
|
|
51
|
+
purely business-logic key (e.g. ``aiof:3_4`` instead of
|
|
52
|
+
``__main__:aio_fun:aiof:3_4``).
|
|
48
53
|
"""
|
|
49
54
|
func_name = get_func_name(func)
|
|
50
55
|
if key is not None:
|
|
51
56
|
if callable(key):
|
|
52
57
|
return key
|
|
53
|
-
if
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
if key_include_func:
|
|
59
|
+
if prefix:
|
|
60
|
+
return "{}:{}:{}".format(prefix, func_name, key)
|
|
61
|
+
return "{}:{}".format(func_name, key)
|
|
62
|
+
else:
|
|
63
|
+
if prefix:
|
|
64
|
+
return "{}:{}".format(prefix, key)
|
|
65
|
+
return key
|
|
56
66
|
|
|
57
67
|
sig = inspect.signature(func)
|
|
58
68
|
parts = []
|
|
@@ -62,9 +72,14 @@ def get_cache_key_template(func, key=None, prefix=""):
|
|
|
62
72
|
parts.append("{{{name}}}".format(name=name))
|
|
63
73
|
|
|
64
74
|
template = ":".join(parts) if parts else ""
|
|
65
|
-
if
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
if key_include_func:
|
|
76
|
+
if prefix:
|
|
77
|
+
return "{}:{}:{}".format(prefix, func_name, template)
|
|
78
|
+
return "{}:{}".format(func_name, template)
|
|
79
|
+
else:
|
|
80
|
+
if prefix:
|
|
81
|
+
return "{}:{}".format(prefix, template) if template else prefix
|
|
82
|
+
return template
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def _resolve_attr(val, attr_path):
|
|
@@ -148,6 +148,7 @@ class Cache(object):
|
|
|
148
148
|
self._tag_registry = get_default_tag_registry()
|
|
149
149
|
self._serializer = default_serializer
|
|
150
150
|
self._is_setup = False
|
|
151
|
+
self._key_include_func = True
|
|
151
152
|
|
|
152
153
|
@property
|
|
153
154
|
def is_setup(self):
|
|
@@ -159,13 +160,16 @@ class Cache(object):
|
|
|
159
160
|
|
|
160
161
|
# --- Setup ---
|
|
161
162
|
|
|
162
|
-
def setup(self, settings_url, middlewares=None, prefix="", **kwargs):
|
|
163
|
+
def setup(self, settings_url, middlewares=None, prefix="", key_include_func=True, **kwargs):
|
|
163
164
|
"""Configure the cache backend from a URL.
|
|
164
165
|
|
|
165
166
|
Args:
|
|
166
167
|
settings_url: URL like 'mem://', 'redis://host:port/db'.
|
|
167
168
|
middlewares: List of Middleware instances.
|
|
168
169
|
prefix: Global key prefix.
|
|
170
|
+
key_include_func: If False, the module path and function name are NOT
|
|
171
|
+
included in auto-generated cache keys. Useful for short, business-logic-only
|
|
172
|
+
keys. Default is True.
|
|
169
173
|
**kwargs: Extra arguments passed to the backend.
|
|
170
174
|
|
|
171
175
|
Supported URL schemes: mem://, redis://, rediss://, dual://
|
|
@@ -200,6 +204,7 @@ class Cache(object):
|
|
|
200
204
|
comp = NullCompressor()
|
|
201
205
|
self._serializer = Serializer(serializer=ser, compressor=comp, signer=signer)
|
|
202
206
|
|
|
207
|
+
self._key_include_func = key_include_func
|
|
203
208
|
self._backend.init_sync()
|
|
204
209
|
self._is_setup = True
|
|
205
210
|
return self
|
|
@@ -480,12 +485,14 @@ class Cache(object):
|
|
|
480
485
|
# --- Decorator shortcuts ---
|
|
481
486
|
|
|
482
487
|
def cache(self, ttl, key=None, condition=None, prefix="", lock=False,
|
|
483
|
-
lock_ttl=None, tags=(), serializer=None):
|
|
488
|
+
lock_ttl=None, tags=(), serializer=None, key_include_func:bool=None):
|
|
484
489
|
from nb_cache.decorators.cache import cache as _cache
|
|
490
|
+
_kif = self._key_include_func if key_include_func is None else key_include_func
|
|
485
491
|
return _cache(ttl, key=key, condition=condition, prefix=prefix,
|
|
486
492
|
lock=lock, lock_ttl=lock_ttl, tags=tags,
|
|
487
493
|
backend=self._backend, serializer=serializer or self._serializer,
|
|
488
|
-
tag_registry=self._tag_registry if tags else None
|
|
494
|
+
tag_registry=self._tag_registry if tags else None,
|
|
495
|
+
key_include_func=_kif)
|
|
489
496
|
|
|
490
497
|
def failover(self, ttl, key=None, exceptions=None, condition=None,
|
|
491
498
|
prefix="fail", tags=(), serializer=None):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
2
|
-
Name:
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: nb-cache
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: `nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。
|
|
5
5
|
Author: ydf0509
|
|
6
6
|
Project-URL: Homepage, https://github.com/ydf0509/nb_cache
|
|
7
7
|
Project-URL: Repository, https://github.com/ydf0509/nb_cache
|
|
@@ -18,22 +18,56 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Requires-Python: >=3.6
|
|
23
24
|
Description-Content-Type: text/markdown
|
|
24
25
|
Provides-Extra: redis
|
|
25
|
-
Requires-Dist: redis>=3.0; extra == "redis"
|
|
26
26
|
Provides-Extra: speedup
|
|
27
|
-
Requires-Dist: xxhash; extra == "speedup"
|
|
28
|
-
Requires-Dist: hiredis; extra == "speedup"
|
|
29
27
|
Provides-Extra: all
|
|
30
|
-
Requires-Dist: redis>=3.0; extra == "all"
|
|
31
|
-
Requires-Dist: xxhash; extra == "all"
|
|
32
|
-
Requires-Dist: hiredis; extra == "all"
|
|
33
28
|
|
|
34
|
-
# nb_cache
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
# 🚀 nb_cache: Python 缓存界的“瑞士军刀”
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/nb-cache/)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
> **`nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。**
|
|
36
|
+
|
|
37
|
+
在如今混杂着 Sync 和 Async 的 Python 项目中,开发者往往需要为同步代码和异步代码寻找不同的缓存解决方案。`nb_cache` 彻底抹平了这一差异——**只需同一个装饰器,即可完美兼容同步与异步函数**。
|
|
38
|
+
|
|
39
|
+
如果你曾经使用过 `cashews`,你会对 `nb_cache` 感到非常亲切。`nb_cache` 吸收了其优秀的特性,并弥补了其最大的短板:**全面支持同/异步场景,且所有底层操作兼具 Sync 和 Async 两种 API**。
|
|
40
|
+
|
|
41
|
+
## ✨ 核心特性
|
|
42
|
+
|
|
43
|
+
- ☯️ **同/异步无缝统一**:无需区分 `@cache` 或 `@acache`,同一个装饰器自动识别普通函数与协程,内部自动路由。同一套上下文管理器同时支持 `with` 和 `async with`。
|
|
44
|
+
- 🚀 **丰富的高级后端**:
|
|
45
|
+
- **Memory (`mem://`)**: 极速的本地 LRU 内存缓存。
|
|
46
|
+
- **Redis (`redis://`)**: 分布式 Redis 缓存,支持连接池。
|
|
47
|
+
- **双层缓存 (`dual://`)**: **(杀手级特性)** 本地内存(L1) + Redis(L2) 的透明双重缓存。读取时优先击中内存,写入时双写,彻底释放 Redis 压力。
|
|
48
|
+
- 🛡️ **企业级高可用防护**:
|
|
49
|
+
- **防缓存击穿 (Stampede)**:只需一个参数 `lock=True`,即可在并发未命中时让多余请求等待,只放行一个请求去查库。
|
|
50
|
+
- **防缓存雪崩 (Avalanche)**:提供 `@cache.early` (提前后台刷新) 和 `@cache.soft` (软过期,返回旧值并异步刷新) 完美解决雪崩。
|
|
51
|
+
- **服务降级与失败回退**:提供 `@cache.failover`,当数据库或下游接口挂掉时,自动返回缓存的旧值兜底。
|
|
52
|
+
- 🔧 **极其丰富的“微服务”级装饰器**:除了缓存,还内置了**限流** (`rate_limit`, `slice_rate_limit`)、**熔断** (`circuit_breaker`)、**并发防抖** (`thunder_protection`)、**布隆过滤器** (`bloom`, `dual_bloom`)。
|
|
53
|
+
- 🔑 **智能 Key 路由与模板**:告别繁琐的 key 拼接。支持 `{user_id}`、`{user.name}` (直接读取对象属性)、`{data:hash}` (自动对大字典算md5) 等高级格式化模板。
|
|
54
|
+
- 🔒 **数据安全与压缩**:自带序列化流水线。一行配置即可开启 JSON/Pickle 序列化、Gzip/Zlib 压缩,以及 HMAC 签名(防止缓存数据被恶意篡改)。
|
|
55
|
+
- 🏷️ **标签系统与事务**:支持给缓存打标签 (Tags) 实现按业务模块批量失效,支持类似数据库的缓存事务 (Transaction) 自动回滚。
|
|
56
|
+
|
|
57
|
+
## 💡 为什么选择 nb_cache?
|
|
58
|
+
|
|
59
|
+
| 特性 | 传统自带 `lru_cache` | `redis-py` 原生 | `cashews` | 🏆 `nb_cache` |
|
|
60
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
61
|
+
| **同步函数支持** | ✅ | ✅ | ❌ | **✅ 完美支持** |
|
|
62
|
+
| **异步函数 (Asyncio) 支持** | ❌ | ✅ | ✅ | **✅ 完美支持** |
|
|
63
|
+
| **内存/Redis 双重缓存** | ❌ | ❌ | ✅ | **✅ 开箱即用** |
|
|
64
|
+
| **防击穿 (分布式锁合并请求)** | ❌ | 需手写代码 | ✅ | **✅ `lock=True`** |
|
|
65
|
+
| **防雪崩/后台自动刷新** | ❌ | 需手写代码 | ✅ | **✅ `@cache.early`** |
|
|
66
|
+
| **限流与熔断** | ❌ | 需手写代码 | ✅ | **✅ 内置支持** |
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### 👉 接下来,请看下方的【安装】与【快速开始】,体验一行代码带来的架构升级:
|
|
37
71
|
|
|
38
72
|
## 安装
|
|
39
73
|
|
|
@@ -66,12 +100,20 @@ def get_user(user_id):
|
|
|
66
100
|
async def get_user_async(user_id):
|
|
67
101
|
return await db.query_async(user_id)
|
|
68
102
|
|
|
69
|
-
#
|
|
103
|
+
# 加锁防止缓存击穿,如果123这个入参没有缓存,但是同一秒请求123这个入参1万次,
|
|
104
|
+
# 加上lock=True后,只有第一次请求会真正执行函数,其余请求等待并复用第一次请求的结果,避免"击穿"。
|
|
70
105
|
@cache.cache(ttl=60, lock=True)
|
|
71
106
|
def get_hot_data(key):
|
|
107
|
+
time.sleep(20)
|
|
72
108
|
return expensive_query(key)
|
|
73
109
|
```
|
|
74
110
|
|
|
111
|
+
## 不想吃苦,如何使用ai掌握nb_cache?
|
|
112
|
+
|
|
113
|
+
`nb_cache_all_docs_and_codes.md` 这个文件包含了nb_cache 的教程和全部源码。
|
|
114
|
+
你把这个文件发送给deepseek ai [https://chat.deepseek.com/](https://chat.deepseek.com/) ,ai就能自动帮你掌握 `nb_cache` 的用法。
|
|
115
|
+
|
|
116
|
+
|
|
75
117
|
## 对比 cashews
|
|
76
118
|
|
|
77
119
|
如果你不懂 `nb_cache` 用法,可以参考 `cashews` 的用法。ai很熟练 `cashews`的用法。
|
|
@@ -665,6 +707,94 @@ def check_permission(user, action):
|
|
|
665
707
|
return db.query_permission(user['id'], action)
|
|
666
708
|
```
|
|
667
709
|
|
|
710
|
+
### key_include_func 参数说明
|
|
711
|
+
|
|
712
|
+
默认情况下,`nb_cache` 会把 **模块路径 + 函数名** 自动拼入 cache key,以确保不同模块的同名函数不会冲突:
|
|
713
|
+
|
|
714
|
+
```
|
|
715
|
+
# 默认生成的 key(含函数信息)
|
|
716
|
+
testp2:myapp.services:get_user:user_id:42
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
如果你已经通过 `key=` 参数自己指定了业务 key 模板,这段模块+函数前缀往往是多余的噪音。
|
|
720
|
+
设置 `key_include_func=False` 后,key 只保留业务部分:
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
# key_include_func=False 后生成的 key
|
|
724
|
+
testp2:user:42
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
#### 设置级别
|
|
728
|
+
|
|
729
|
+
`key_include_func` 支持两个层级,**装饰器上的值优先于 `setup()` 的默认值**。
|
|
730
|
+
|
|
731
|
+
**1. `setup()` 级别 —— 影响该实例下所有装饰器**
|
|
732
|
+
|
|
733
|
+
```python
|
|
734
|
+
from nb_cache import Cache
|
|
735
|
+
|
|
736
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp", key_include_func=False)
|
|
737
|
+
|
|
738
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
739
|
+
def get_user(user_id):
|
|
740
|
+
return db.query(user_id)
|
|
741
|
+
# final_key → myapp:user:42
|
|
742
|
+
|
|
743
|
+
@cache.cache(ttl=60, key="order:{order_id}")
|
|
744
|
+
def get_order(order_id):
|
|
745
|
+
return db.query_order(order_id)
|
|
746
|
+
# final_key → myapp:order:100
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**2. 装饰器级别 —— 覆盖 `setup()` 的默认值,只影响当前函数**
|
|
750
|
+
|
|
751
|
+
```python
|
|
752
|
+
cache = Cache().setup("redis://localhost:6379/0", prefix="myapp")
|
|
753
|
+
# 默认 key_include_func=True
|
|
754
|
+
|
|
755
|
+
@cache.cache(ttl=300, key="user:{user_id}")
|
|
756
|
+
def get_user(user_id):
|
|
757
|
+
...
|
|
758
|
+
# final_key → myapp:mymodule:get_user:user:{user_id} → myapp:mymodule:get_user:user:42
|
|
759
|
+
|
|
760
|
+
@cache.cache(ttl=60, key="order:{order_id}", key_include_func=False)
|
|
761
|
+
def get_order(order_id):
|
|
762
|
+
...
|
|
763
|
+
# final_key → myapp:order:100 (单独关闭,不含函数名)
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
#### 不指定 key= 时的行为
|
|
767
|
+
|
|
768
|
+
当不传 `key=` 参数,`nb_cache` 会根据函数签名自动生成 key。
|
|
769
|
+
此时 `key_include_func=False` 意味着 key 只由参数值组成,**极易碰撞**,不推荐在此场景下使用:
|
|
770
|
+
|
|
771
|
+
```python
|
|
772
|
+
# 不推荐:不指定 key= 且 key_include_func=False
|
|
773
|
+
@cache.cache(ttl=60, key_include_func=False)
|
|
774
|
+
def get_user(user_id):
|
|
775
|
+
...
|
|
776
|
+
# final_key → myapp:user_id=42 ← 与其他相同签名函数会冲突
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
#### 如何预览生成的 key(不调用函数)
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
783
|
+
|
|
784
|
+
# 方式1:从已装饰函数取模板(推荐)
|
|
785
|
+
template = get_user._cache_key_template
|
|
786
|
+
logic_key = get_cache_key(get_user, template, (42,), {})
|
|
787
|
+
final_key = cache._backend._make_key(logic_key)
|
|
788
|
+
print(final_key) # myapp:user:42
|
|
789
|
+
|
|
790
|
+
# 方式2:直接用工具函数生成(不需要先装饰)
|
|
791
|
+
tpl = get_cache_key_template(get_user, key="user:{user_id}", key_include_func=False)
|
|
792
|
+
key = get_cache_key(get_user, tpl, (42,), {})
|
|
793
|
+
print(key) # user:42
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
668
798
|
## 锁(上下文管理器)
|
|
669
799
|
|
|
670
800
|
```python
|
|
@@ -775,6 +905,71 @@ def get_data():
|
|
|
775
905
|
return {"name": "test"}
|
|
776
906
|
```
|
|
777
907
|
|
|
908
|
+
## 常见问题解答
|
|
909
|
+
|
|
910
|
+
### 问题1:如何查看缓存最终生成的key是什么?
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
#### 方式一:通过日志查看
|
|
914
|
+
```
|
|
915
|
+
因为nb_cache 已经在 nb_cache.cache 日志命名空间,用debug 日志级别打印了最终生成的key。
|
|
916
|
+
|
|
917
|
+
所以你可以通过nb_log来查看:
|
|
918
|
+
nb_log.get_logger('nb_cache.cache')
|
|
919
|
+
|
|
920
|
+
也可以通过 原生logging 来查看:
|
|
921
|
+
logger = logging.getLogger("nb_cache.cache")
|
|
922
|
+
logger.setLevel(logging.DEBUG)
|
|
923
|
+
logger.addHandler(logging.StreamHandler())
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
日志例子:
|
|
927
|
+
```
|
|
928
|
+
2026-02-28 18:46:01 - nb_cache.cache - "D:\codes\nb_cache\nb_cache\decorators\cache.py:61" - async_wrapper - DEBUG - [nb_cache] func=__main__:aio_fun final_key=testp2:__main__:aio_fun:aiof:3_4 ttl=700.0
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
#### 方式二:不调用函数,直接预览 cache key
|
|
932
|
+
|
|
933
|
+
```python
|
|
934
|
+
# -*- coding: utf-8 -*-
|
|
935
|
+
"""nb_cache key 生成 Demo"""
|
|
936
|
+
import sys
|
|
937
|
+
import asyncio
|
|
938
|
+
from nb_cache import Cache
|
|
939
|
+
from nb_cache.key import get_cache_key, get_cache_key_template
|
|
940
|
+
import nb_log
|
|
941
|
+
|
|
942
|
+
nb_log.get_logger("nb_cache.cache")
|
|
943
|
+
|
|
944
|
+
if sys.platform == "win32":
|
|
945
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
946
|
+
|
|
947
|
+
cache = Cache()
|
|
948
|
+
cache.setup("redis://", prefix="testp2")
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@cache.cache(ttl=700,key='sf:{a}_{b}')
|
|
952
|
+
def simple_func(a, b):
|
|
953
|
+
return a + b
|
|
954
|
+
|
|
955
|
+
print(simple_func(1, 2))
|
|
956
|
+
|
|
957
|
+
# --- 不调用函数,直接预览 cache key ---
|
|
958
|
+
# 方式1:从装饰后的函数取模板属性(推荐)
|
|
959
|
+
template = simple_func._cache_key_template
|
|
960
|
+
logic_key = get_cache_key(simple_func, template, (1, 2), {})
|
|
961
|
+
final_key = cache._backend._make_key(logic_key)
|
|
962
|
+
print("logic_key:", logic_key) # 不含 prefix
|
|
963
|
+
print("final_key:", final_key) # 含 prefix,与 Redis 中一致
|
|
964
|
+
|
|
965
|
+
# 方式2:用工具函数直接生成,不需要先装饰
|
|
966
|
+
def raw_func(a, b):
|
|
967
|
+
return a + b
|
|
968
|
+
tpl = get_cache_key_template(raw_func, key='sf:{a}_{b}')
|
|
969
|
+
key2 = get_cache_key(raw_func, tpl, (1, 2), {})
|
|
970
|
+
print("key2 (无prefix):", key2)
|
|
971
|
+
```
|
|
972
|
+
|
|
778
973
|
## 许可证
|
|
779
974
|
|
|
780
975
|
MIT License
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nb_cache"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.4"
|
|
8
|
+
description = "`nb_cache` 不仅是一个基础的缓存装饰器,它在彻底抹平 Python 同步与异步代码差异的同时,开箱即用地提供了内存/Redis双层缓存、防击穿、防雪崩、限流与熔断等企业级高可用特性。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# license = {text = "MIT"}
|
|
11
11
|
requires-python = ">=3.6"
|
|
@@ -26,6 +26,7 @@ classifiers = [
|
|
|
26
26
|
"Programming Language :: Python :: 3.11",
|
|
27
27
|
"Programming Language :: Python :: 3.12",
|
|
28
28
|
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
29
30
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
31
|
# "Topic :: System :: Caching",
|
|
31
32
|
]
|
|
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
|