Skip to content

Commit d67519e

Browse files
添加数据库缓存。 (#85)
* 添加数据库缓存。 * 添加配置项 * 修复类型错误 * 修复缓存刷新错误 * 使用海象表达式
1 parent 7750886 commit d67519e

File tree

16 files changed

+356
-55
lines changed

16 files changed

+356
-55
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"python.analysis.autoImportCompletions": false
2+
"python.analysis.autoImportCompletions": true
33
}

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- 🔍 时间范围审计日志: 从时间范围获取交易记录
3939
- 🚀 导出数据: 支持从 Json 文件导入/导出到 Json 文件
4040
- 🔧 依赖注入: 支持依赖注入模式调用
41+
- ⚡️ 高性能: LRU淘汰策略应用层缓存
4142

4243
### 快速开始
4344

@@ -74,6 +75,12 @@ plugins = ["nonebot_plugin_value"]
7475

7576
### [API Docs](https://docs.suggar.top/project/value/docs/api)
7677

78+
### 配置项
79+
80+
```dotenv
81+
VALUE_PRE_BUILD_CACHE = true # 是否在启动时预构建缓存
82+
```
83+
7784
### 更新迁移
7885

7986
1. 升级 nonebot_plugin_value 到最新版本

nonebot_plugin_value/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from nonebot import get_driver
1+
from nonebot import get_driver, get_plugin_config, logger
22
from nonebot.plugin import PluginMetadata, require
33

44
require("nonebot_plugin_orm")
55
require("nonebot_plugin_localstore")
66

77
from .api import api_balance, api_currency, api_transaction
8-
from .api.api_currency import get_or_create_currency
8+
from .api.api_currency import get_or_create_currency, list_currencies
99
from .api.depends import factory
10+
from .config import Config
1011
from .hook import context, hooks_manager, hooks_type
1112
from .models import currency
1213
from .pyd_models import balance_pyd, base_pyd, currency_pyd
@@ -44,3 +45,9 @@ async def init_db():
4445
初始化数据库
4546
"""
4647
await get_or_create_currency(CurrencyData(id=DEFAULT_CURRENCY_UUID.hex))
48+
if get_plugin_config(Config).value_pre_build_cache:
49+
logger.info("正在初始化缓存...")
50+
logger.info("正在初始化货币缓存...")
51+
await list_currencies()
52+
logger.info("正在初始化账户缓存...")
53+
await api_balance.list_accounts()

nonebot_plugin_value/_cache.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
from __future__ import annotations
2+
3+
import enum
4+
from asyncio import Lock
5+
from collections import OrderedDict
6+
from collections.abc import Hashable
7+
from dataclasses import dataclass, field
8+
from functools import lru_cache
9+
from typing import Any, Generic, TypeVar
10+
11+
from typing_extensions import Self
12+
13+
from .pyd_models.balance_pyd import UserAccountData
14+
from .pyd_models.base_pyd import BaseData
15+
from .pyd_models.currency_pyd import CurrencyData
16+
from .pyd_models.transaction_pyd import TransactionData
17+
18+
19+
class CacheCategoryEnum(str, enum.Enum):
20+
CURRENCY = "currency"
21+
ACCOUNT = "account"
22+
TRANSACTION = "transaction"
23+
24+
25+
T = TypeVar("T", BaseData, CurrencyData, TransactionData, UserAccountData)
26+
27+
28+
@dataclass
29+
class Cache(Generic[T]):
30+
"""Cache存储模型"""
31+
32+
# 默认缓存最大条目数
33+
max_size: int = 1000
34+
35+
# LRU实现
36+
_cache: OrderedDict[str, BaseData] = field(default_factory=lambda: OrderedDict())
37+
38+
def __post_init__(self):
39+
if self.max_size <= 0:
40+
self.max_size = 1000
41+
42+
async def update(self, *, data: BaseData) -> bool:
43+
data_id = data.uni_id if isinstance(data, UserAccountData) else data.id
44+
async with self._lock(data_id):
45+
if existing := self._cache.get(data_id):
46+
existing.model_validate(data, from_attributes=True)
47+
self._cache.move_to_end(data_id)
48+
return True
49+
50+
# 添加新数据
51+
self._cache[data_id] = data
52+
self._cache.move_to_end(data_id)
53+
54+
# 如果超出最大大小,删除最久未使用的项(第一个)
55+
if len(self._cache) > self.max_size:
56+
self._cache.popitem(last=False)
57+
58+
return False
59+
60+
async def get(self, *, data_id: str) -> BaseData | None:
61+
async with self._lock(data_id):
62+
item = self._cache.get(data_id)
63+
if item is not None:
64+
# 访问后移到末尾(标记为最近使用)
65+
self._cache.move_to_end(data_id)
66+
return item
67+
68+
async def get_all(self) -> list[BaseData]:
69+
async with self._lock():
70+
# 返回所有缓存项的副本
71+
return list(self._cache.values())
72+
73+
async def delete(self, *, data_id: str):
74+
async with self._lock(data_id):
75+
self._cache.pop(data_id, None)
76+
77+
async def clear(self):
78+
async with self._lock(0):
79+
self._cache.clear()
80+
81+
@staticmethod
82+
@lru_cache(1024)
83+
def _lock(*args: Hashable) -> Lock:
84+
return Lock()
85+
86+
87+
class CacheManager:
88+
_instance = None
89+
_cached: dict[CacheCategoryEnum, Cache[Any]]
90+
91+
def __new__(cls) -> Self:
92+
if cls._instance is None:
93+
cls._instance = super().__new__(cls)
94+
cls._cached = {}
95+
return cls._instance
96+
97+
async def get_cache(
98+
self, category: CacheCategoryEnum, max_size: int = 1000
99+
) -> Cache[Any]:
100+
# 为不同类别创建具有不同大小的缓存
101+
if category not in self._cached:
102+
self._cached[category] = Cache(max_size=max_size)
103+
return self._cached[category]
104+
105+
async def update_cache(
106+
self,
107+
*,
108+
category: CacheCategoryEnum,
109+
data: BaseData,
110+
max_size: int = 1000,
111+
) -> Self:
112+
"""更新缓存
113+
114+
Args:
115+
category (CacheCategoryEnum): 缓存板块
116+
data (BaseData): 数据
117+
max_size (int): 缓存最大大小
118+
"""
119+
async with self._get_lock(category):
120+
cache = await self.get_cache(category, max_size)
121+
await cache.update(data=data)
122+
return self
123+
124+
async def expire_cache(
125+
self, *, category: CacheCategoryEnum, data_id: str | None = None
126+
) -> Self:
127+
"""使缓存过期(当数据库操作中该条删除时使用)
128+
129+
Args:
130+
category (CacheCategoryEnum): 缓存板块
131+
data_id (str | None, optional): 数据ID. Defaults to None.
132+
"""
133+
async with self._get_lock(category):
134+
if category in self._cached:
135+
if data_id is not None:
136+
cache = await self.get_cache(category)
137+
await cache.delete(data_id=data_id)
138+
else:
139+
self._cached.pop(category, None)
140+
return self
141+
142+
async def expire_all_cache(self) -> Self:
143+
"""使所有缓存过期"""
144+
145+
self._cached.clear()
146+
return self
147+
148+
@staticmethod
149+
@lru_cache(1024)
150+
def _get_lock(*args: Hashable) -> Lock:
151+
return Lock()

0 commit comments

Comments
 (0)