You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
500 lines
15 KiB
500 lines
15 KiB
|
4 weeks ago
|
# 企业级缓存架构实施方案
|
||
|
|
|
||
|
|
## 1. 问题分析
|
||
|
|
|
||
|
|
当前 `supply.html` 页面存在以下性能问题:
|
||
|
|
|
||
|
|
1. **无缓存机制**:每次加载页面或刷新列表时都重新从API获取所有数据
|
||
|
|
2. **一次性获取大量数据**:使用 `pageSize: 1000` 一次性获取所有数据
|
||
|
|
3. **重复计算和过滤**:每次都需要重新处理和过滤数据
|
||
|
|
4. **无数据变更检测**:无法检测数据是否发生变化,只能通过完全重新加载来更新
|
||
|
|
5. **无虚拟滚动**:即使使用分页,也没有实现虚拟滚动来优化DOM渲染
|
||
|
|
|
||
|
|
## 2. 解决方案
|
||
|
|
|
||
|
|
### 2.1 前端缓存架构
|
||
|
|
|
||
|
|
#### 2.1.1 数据缓存层
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 缓存管理器
|
||
|
|
class CacheManager {
|
||
|
|
constructor() {
|
||
|
|
this.cache = new Map();
|
||
|
|
this.defaultTTL = 5 * 60 * 1000; // 默认缓存5分钟
|
||
|
|
}
|
||
|
|
|
||
|
|
// 设置缓存
|
||
|
|
set(key, value, ttl = this.defaultTTL) {
|
||
|
|
const item = {
|
||
|
|
value,
|
||
|
|
expiry: Date.now() + ttl
|
||
|
|
};
|
||
|
|
this.cache.set(key, item);
|
||
|
|
localStorage.setItem(`cache_${key}`, JSON.stringify(item));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 获取缓存
|
||
|
|
get(key) {
|
||
|
|
// 先从内存缓存获取
|
||
|
|
let item = this.cache.get(key);
|
||
|
|
|
||
|
|
// 如果内存缓存不存在,从localStorage获取
|
||
|
|
if (!item) {
|
||
|
|
const stored = localStorage.getItem(`cache_${key}`);
|
||
|
|
if (stored) {
|
||
|
|
try {
|
||
|
|
item = JSON.parse(stored);
|
||
|
|
this.cache.set(key, item);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('解析缓存失败:', e);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 检查缓存是否过期
|
||
|
|
if (item && Date.now() < item.expiry) {
|
||
|
|
return item.value;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 缓存过期,清除缓存
|
||
|
|
this.delete(key);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 删除缓存
|
||
|
|
delete(key) {
|
||
|
|
this.cache.delete(key);
|
||
|
|
localStorage.removeItem(`cache_${key}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 清除所有缓存
|
||
|
|
clear() {
|
||
|
|
this.cache.clear();
|
||
|
|
// 清除所有以cache_开头的localStorage项
|
||
|
|
for (let key in localStorage) {
|
||
|
|
if (key.startsWith('cache_')) {
|
||
|
|
localStorage.removeItem(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 清除指定前缀的缓存
|
||
|
|
clearByPrefix(prefix) {
|
||
|
|
for (let key of this.cache.keys()) {
|
||
|
|
if (key.startsWith(prefix)) {
|
||
|
|
this.cache.delete(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (let key in localStorage) {
|
||
|
|
if (key.startsWith(`cache_${prefix}`)) {
|
||
|
|
localStorage.removeItem(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 实例化缓存管理器
|
||
|
|
const cacheManager = new CacheManager();
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.1.2 数据加载优化
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 优化后的loadSupplies函数
|
||
|
|
async function loadSupplies(forceRefresh = false) {
|
||
|
|
if (isLoadingSupplies) {
|
||
|
|
console.log('loadSupplies已在执行中,跳过当前请求');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
isLoadingSupplies = true;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const userInfo = checkLogin();
|
||
|
|
if (!userInfo) return;
|
||
|
|
|
||
|
|
const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`;
|
||
|
|
|
||
|
|
// 尝试从缓存获取数据
|
||
|
|
if (!forceRefresh) {
|
||
|
|
const cachedData = cacheManager.get(cacheKey);
|
||
|
|
if (cachedData) {
|
||
|
|
console.log('从缓存加载货源数据');
|
||
|
|
supplyData.supplies = cachedData.supplies;
|
||
|
|
supplyData.publishedSupplies = cachedData.publishedSupplies;
|
||
|
|
supplyData.pendingSupplies = cachedData.pendingSupplies;
|
||
|
|
supplyData.rejectedSupplies = cachedData.rejectedSupplies;
|
||
|
|
supplyData.draftSupplies = cachedData.draftSupplies;
|
||
|
|
renderSupplyLists();
|
||
|
|
isLoadingSupplies = false;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重置分页数据
|
||
|
|
resetPaginationData();
|
||
|
|
|
||
|
|
// 构建查询参数,使用合理的pageSize
|
||
|
|
const queryParams = new URLSearchParams({
|
||
|
|
page: 1,
|
||
|
|
pageSize: 100 // 合理的分页大小
|
||
|
|
});
|
||
|
|
|
||
|
|
// 非管理员添加sellerId参数
|
||
|
|
if (userInfo.projectName !== '管理员') {
|
||
|
|
queryParams.append('sellerId', userInfo.userId || userInfo.id);
|
||
|
|
}
|
||
|
|
|
||
|
|
const apiUrl = `/api/supplies?${queryParams}`;
|
||
|
|
console.log('加载货源列表API请求:', apiUrl);
|
||
|
|
|
||
|
|
// 实现增量加载
|
||
|
|
const allSupplies = [];
|
||
|
|
let currentPage = 1;
|
||
|
|
let hasMore = true;
|
||
|
|
|
||
|
|
while (hasMore) {
|
||
|
|
const response = await fetch(`/api/supplies?page=${currentPage}&pageSize=100${userInfo.projectName !== '管理员' ? `&sellerId=${userInfo.userId || userInfo.id}` : ''}`);
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (result.success) {
|
||
|
|
const pageSupplies = result.data.list;
|
||
|
|
allSupplies.push(...pageSupplies);
|
||
|
|
|
||
|
|
// 检查是否还有更多数据
|
||
|
|
hasMore = pageSupplies.length === 100;
|
||
|
|
currentPage++;
|
||
|
|
} else {
|
||
|
|
console.error('加载货源失败:', result.message);
|
||
|
|
hasMore = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 处理获取到的数据
|
||
|
|
processSupplyData(allSupplies);
|
||
|
|
|
||
|
|
// 缓存处理后的数据
|
||
|
|
const cacheData = {
|
||
|
|
supplies: supplyData.supplies,
|
||
|
|
publishedSupplies: supplyData.publishedSupplies,
|
||
|
|
pendingSupplies: supplyData.pendingSupplies,
|
||
|
|
rejectedSupplies: supplyData.rejectedSupplies,
|
||
|
|
draftSupplies: supplyData.draftSupplies,
|
||
|
|
timestamp: Date.now()
|
||
|
|
};
|
||
|
|
|
||
|
|
// 设置缓存,管理员缓存时间较短,因为数据变化更频繁
|
||
|
|
const cacheTTL = userInfo.projectName === '管理员' ? 2 * 60 * 1000 : 5 * 60 * 1000;
|
||
|
|
cacheManager.set(cacheKey, cacheData, cacheTTL);
|
||
|
|
|
||
|
|
// 渲染货源列表
|
||
|
|
renderSupplyLists();
|
||
|
|
} catch (error) {
|
||
|
|
console.error('加载货源失败:', error);
|
||
|
|
} finally {
|
||
|
|
isLoadingSupplies = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重置分页数据函数
|
||
|
|
function resetPaginationData() {
|
||
|
|
supplyData.pagination = {
|
||
|
|
published: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
|
||
|
|
pending: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
|
||
|
|
rejected: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false },
|
||
|
|
draft: { currentPage: 1, pageSize: 20, total: 0, hasMore: true, isLoading: false }
|
||
|
|
};
|
||
|
|
|
||
|
|
supplyData.paginatedSupplies = {
|
||
|
|
published: [],
|
||
|
|
pending: [],
|
||
|
|
rejected: [],
|
||
|
|
draft: []
|
||
|
|
};
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.1.3 虚拟滚动实现
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 虚拟滚动组件
|
||
|
|
class VirtualScroll {
|
||
|
|
constructor(container, items, renderItem, itemHeight = 300) {
|
||
|
|
this.container = container;
|
||
|
|
this.items = items;
|
||
|
|
this.renderItem = renderItem;
|
||
|
|
this.itemHeight = itemHeight;
|
||
|
|
this.visibleItems = [];
|
||
|
|
this.startIndex = 0;
|
||
|
|
this.endIndex = 0;
|
||
|
|
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
init() {
|
||
|
|
// 设置容器高度
|
||
|
|
this.container.style.height = '600px';
|
||
|
|
this.container.style.overflow = 'auto';
|
||
|
|
this.container.style.position = 'relative';
|
||
|
|
|
||
|
|
// 创建占位元素
|
||
|
|
this.placeholder = document.createElement('div');
|
||
|
|
this.placeholder.style.height = `${this.items.length * this.itemHeight}px`;
|
||
|
|
this.placeholder.style.position = 'absolute';
|
||
|
|
this.placeholder.style.top = '0';
|
||
|
|
this.placeholder.style.left = '0';
|
||
|
|
this.placeholder.style.width = '100%';
|
||
|
|
this.container.appendChild(this.placeholder);
|
||
|
|
|
||
|
|
// 创建可见项目容器
|
||
|
|
this.itemsContainer = document.createElement('div');
|
||
|
|
this.itemsContainer.style.position = 'absolute';
|
||
|
|
this.itemsContainer.style.top = '0';
|
||
|
|
this.itemsContainer.style.left = '0';
|
||
|
|
this.itemsContainer.style.width = '100%';
|
||
|
|
this.container.appendChild(this.itemsContainer);
|
||
|
|
|
||
|
|
// 绑定滚动事件
|
||
|
|
this.container.addEventListener('scroll', this.handleScroll.bind(this));
|
||
|
|
|
||
|
|
// 初始渲染
|
||
|
|
this.updateVisibleItems();
|
||
|
|
}
|
||
|
|
|
||
|
|
handleScroll() {
|
||
|
|
this.updateVisibleItems();
|
||
|
|
}
|
||
|
|
|
||
|
|
updateVisibleItems() {
|
||
|
|
const scrollTop = this.container.scrollTop;
|
||
|
|
const containerHeight = this.container.clientHeight;
|
||
|
|
|
||
|
|
// 计算可见项的范围
|
||
|
|
const newStartIndex = Math.max(0, Math.floor(scrollTop / this.itemHeight) - 2);
|
||
|
|
const newEndIndex = Math.min(
|
||
|
|
this.items.length - 1,
|
||
|
|
Math.ceil((scrollTop + containerHeight) / this.itemHeight) + 2
|
||
|
|
);
|
||
|
|
|
||
|
|
// 如果范围没有变化,不更新
|
||
|
|
if (newStartIndex === this.startIndex && newEndIndex === this.endIndex) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
this.startIndex = newStartIndex;
|
||
|
|
this.endIndex = newEndIndex;
|
||
|
|
|
||
|
|
// 渲染可见项
|
||
|
|
this.renderVisibleItems();
|
||
|
|
}
|
||
|
|
|
||
|
|
renderVisibleItems() {
|
||
|
|
// 清空容器
|
||
|
|
this.itemsContainer.innerHTML = '';
|
||
|
|
|
||
|
|
// 设置容器位置
|
||
|
|
this.itemsContainer.style.transform = `translateY(${this.startIndex * this.itemHeight}px)`;
|
||
|
|
|
||
|
|
// 渲染可见项
|
||
|
|
for (let i = this.startIndex; i <= this.endIndex; i++) {
|
||
|
|
const item = this.renderItem(this.items[i]);
|
||
|
|
this.itemsContainer.appendChild(item);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 更新数据源
|
||
|
|
updateItems(items) {
|
||
|
|
this.items = items;
|
||
|
|
this.placeholder.style.height = `${this.items.length * this.itemHeight}px`;
|
||
|
|
this.updateVisibleItems();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 使用虚拟滚动
|
||
|
|
function initVirtualScroll() {
|
||
|
|
const publishedList = document.getElementById('publishedList');
|
||
|
|
if (publishedList) {
|
||
|
|
const virtualScroll = new VirtualScroll(
|
||
|
|
publishedList,
|
||
|
|
supplyData.publishedSupplies,
|
||
|
|
(supply) => {
|
||
|
|
const item = document.createElement('div');
|
||
|
|
item.className = 'supply-item';
|
||
|
|
item.innerHTML = generateSupplyItemHTML(supply);
|
||
|
|
return item;
|
||
|
|
}
|
||
|
|
);
|
||
|
|
|
||
|
|
// 保存虚拟滚动实例
|
||
|
|
supplyData.virtualScroll = virtualScroll;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 后端优化建议
|
||
|
|
|
||
|
|
#### 2.2.1 API 优化
|
||
|
|
|
||
|
|
1. **实现增量更新**:
|
||
|
|
- 添加 `lastUpdated` 参数,只返回比该时间更新的数据
|
||
|
|
- 实现 `ETag` 和 `If-None-Match` 头,支持缓存验证
|
||
|
|
|
||
|
|
2. **数据压缩**:
|
||
|
|
- 启用 Gzip/Brotli 压缩
|
||
|
|
- 对大型数据集使用分页和字段过滤
|
||
|
|
|
||
|
|
3. **缓存层**:
|
||
|
|
- 在后端添加 Redis 缓存
|
||
|
|
- 缓存热门查询结果
|
||
|
|
|
||
|
|
### 2.3 缓存策略设计
|
||
|
|
|
||
|
|
#### 2.3.1 多级缓存策略
|
||
|
|
|
||
|
|
| 缓存级别 | 存储介质 | 缓存时间 | 适用场景 |
|
||
|
|
|---------|---------|---------|----------|
|
||
|
|
| L1 | 内存 (Map) | 5分钟 | 频繁访问的数据 |
|
||
|
|
| L2 | localStorage | 5分钟 | 页面刷新后的数据 |
|
||
|
|
| L3 | 后端 Redis | 10分钟 | 多用户共享的数据 |
|
||
|
|
|
||
|
|
#### 2.3.2 缓存键设计
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 缓存键格式
|
||
|
|
const cacheKey = `supplies_${userId}_${role}_${timestamp}`;
|
||
|
|
|
||
|
|
// 示例:
|
||
|
|
supplies_user123_管理员_1678900000000
|
||
|
|
```
|
||
|
|
|
||
|
|
#### 2.3.3 缓存失效策略
|
||
|
|
|
||
|
|
1. **时间过期**:
|
||
|
|
- 管理员:2分钟
|
||
|
|
- 普通用户:5分钟
|
||
|
|
|
||
|
|
2. **主动失效**:
|
||
|
|
- 数据变更时主动清除相关缓存
|
||
|
|
- 页面操作(如上架/下架)后清除对应缓存
|
||
|
|
|
||
|
|
3. **批量失效**:
|
||
|
|
- 使用前缀清除策略,如清除所有 `supplies_${userId}_` 开头的缓存
|
||
|
|
|
||
|
|
### 3. 实施方案
|
||
|
|
|
||
|
|
#### 3.1 实施步骤
|
||
|
|
|
||
|
|
1. **第一步:添加缓存管理器**
|
||
|
|
- 在 `supply.html` 中添加 `CacheManager` 类
|
||
|
|
- 初始化缓存管理器实例
|
||
|
|
|
||
|
|
2. **第二步:优化数据加载**
|
||
|
|
- 修改 `loadSupplies` 函数,添加缓存逻辑
|
||
|
|
- 实现增量加载和分页
|
||
|
|
|
||
|
|
3. **第三步:实现虚拟滚动**
|
||
|
|
- 添加 `VirtualScroll` 类
|
||
|
|
- 修改渲染逻辑,使用虚拟滚动
|
||
|
|
|
||
|
|
4. **第四步:添加缓存失效机制**
|
||
|
|
- 在数据变更操作后添加缓存清除逻辑
|
||
|
|
- 实现缓存过期检查
|
||
|
|
|
||
|
|
5. **第五步:测试和优化**
|
||
|
|
- 测试不同数据量下的性能
|
||
|
|
- 调整缓存时间和分页大小
|
||
|
|
|
||
|
|
#### 3.2 预期效果
|
||
|
|
|
||
|
|
| 指标 | 优化前 | 优化后 | 提升幅度 |
|
||
|
|
|------|--------|--------|----------|
|
||
|
|
| 首次加载时间 | 5-10秒 | 2-3秒 | ~60% |
|
||
|
|
| 页面刷新时间 | 3-5秒 | 0.5-1秒 | ~80% |
|
||
|
|
| 内存使用 | 500MB+ | 100-200MB | ~60% |
|
||
|
|
| 可支持数据量 | 500-1000条 | 5000+条 | ~5x |
|
||
|
|
|
||
|
|
### 4. 监控和维护
|
||
|
|
|
||
|
|
#### 4.1 监控指标
|
||
|
|
|
||
|
|
1. **前端指标**:
|
||
|
|
- 页面加载时间
|
||
|
|
- 数据加载时间
|
||
|
|
- 内存使用情况
|
||
|
|
- 缓存命中率
|
||
|
|
|
||
|
|
2. **后端指标**:
|
||
|
|
- API 响应时间
|
||
|
|
- 数据库查询时间
|
||
|
|
- 缓存命中率
|
||
|
|
|
||
|
|
#### 4.2 维护建议
|
||
|
|
|
||
|
|
1. **定期清理**:
|
||
|
|
- 定期清理过期缓存
|
||
|
|
- 监控 localStorage 使用情况
|
||
|
|
|
||
|
|
2. **性能分析**:
|
||
|
|
- 使用 Chrome DevTools 分析性能
|
||
|
|
- 识别瓶颈并优化
|
||
|
|
|
||
|
|
3. **扩展性**:
|
||
|
|
- 设计模块化的缓存系统
|
||
|
|
- 支持未来添加更多缓存策略
|
||
|
|
|
||
|
|
### 5. 代码集成示例
|
||
|
|
|
||
|
|
#### 5.1 完整的缓存集成
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
// 在页面初始化时
|
||
|
|
window.onload = function() {
|
||
|
|
// 初始化缓存管理器
|
||
|
|
const cacheManager = new CacheManager();
|
||
|
|
|
||
|
|
// 初始化应用
|
||
|
|
init();
|
||
|
|
|
||
|
|
// 加载数据(优先使用缓存)
|
||
|
|
loadSupplies();
|
||
|
|
|
||
|
|
// 定时刷新缓存
|
||
|
|
setInterval(() => {
|
||
|
|
loadSupplies(false); // 不强制刷新
|
||
|
|
}, 300000); // 5分钟
|
||
|
|
};
|
||
|
|
|
||
|
|
// 在数据变更操作后
|
||
|
|
async function publishSupply(id) {
|
||
|
|
// 执行上架操作
|
||
|
|
const success = await apiCall('/api/supplies/' + id + '/publish');
|
||
|
|
|
||
|
|
if (success) {
|
||
|
|
// 清除相关缓存
|
||
|
|
const userInfo = checkLogin();
|
||
|
|
const cacheKey = `supplies_${userInfo.userId || userInfo.id}_${userInfo.projectName}`;
|
||
|
|
cacheManager.delete(cacheKey);
|
||
|
|
|
||
|
|
// 重新加载数据
|
||
|
|
loadSupplies(true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## 4. 总结
|
||
|
|
|
||
|
|
本实施方案通过以下核心技术提升性能:
|
||
|
|
|
||
|
|
1. **多级缓存**:内存 + localStorage + 后端 Redis
|
||
|
|
2. **增量加载**:分页获取数据,减少一次性加载压力
|
||
|
|
3. **虚拟滚动**:只渲染可视区域的数据,减少 DOM 操作
|
||
|
|
4. **智能缓存策略**:基于用户角色和数据类型的差异化缓存
|
||
|
|
5. **缓存失效机制**:时间过期 + 主动失效
|
||
|
|
|
||
|
|
通过这些优化,系统可以支持:
|
||
|
|
- 管理员查看和管理大量货源数据
|
||
|
|
- 快速页面加载和响应
|
||
|
|
- 稳定的性能表现,即使在数据量增长的情况下
|
||
|
|
|
||
|
|
此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。
|