4 changed files with 805 additions and 24 deletions
@ -0,0 +1,500 @@ |
|||||
|
# 企业级缓存架构实施方案 |
||||
|
|
||||
|
## 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. **缓存失效机制**:时间过期 + 主动失效 |
||||
|
|
||||
|
通过这些优化,系统可以支持: |
||||
|
- 管理员查看和管理大量货源数据 |
||||
|
- 快速页面加载和响应 |
||||
|
- 稳定的性能表现,即使在数据量增长的情况下 |
||||
|
|
||||
|
此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。 |
||||
Loading…
Reference in new issue