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.
15 KiB
15 KiB
企业级缓存架构实施方案
1. 问题分析
当前 supply.html 页面存在以下性能问题:
- 无缓存机制:每次加载页面或刷新列表时都重新从API获取所有数据
- 一次性获取大量数据:使用
pageSize: 1000一次性获取所有数据 - 重复计算和过滤:每次都需要重新处理和过滤数据
- 无数据变更检测:无法检测数据是否发生变化,只能通过完全重新加载来更新
- 无虚拟滚动:即使使用分页,也没有实现虚拟滚动来优化DOM渲染
2. 解决方案
2.1 前端缓存架构
2.1.1 数据缓存层
// 缓存管理器
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 数据加载优化
// 优化后的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 虚拟滚动实现
// 虚拟滚动组件
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 优化
-
实现增量更新:
- 添加
lastUpdated参数,只返回比该时间更新的数据 - 实现
ETag和If-None-Match头,支持缓存验证
- 添加
-
数据压缩:
- 启用 Gzip/Brotli 压缩
- 对大型数据集使用分页和字段过滤
-
缓存层:
- 在后端添加 Redis 缓存
- 缓存热门查询结果
2.3 缓存策略设计
2.3.1 多级缓存策略
| 缓存级别 | 存储介质 | 缓存时间 | 适用场景 |
|---|---|---|---|
| L1 | 内存 (Map) | 5分钟 | 频繁访问的数据 |
| L2 | localStorage | 5分钟 | 页面刷新后的数据 |
| L3 | 后端 Redis | 10分钟 | 多用户共享的数据 |
2.3.2 缓存键设计
// 缓存键格式
const cacheKey = `supplies_${userId}_${role}_${timestamp}`;
// 示例:
supplies_user123_管理员_1678900000000
2.3.3 缓存失效策略
-
时间过期:
- 管理员:2分钟
- 普通用户:5分钟
-
主动失效:
- 数据变更时主动清除相关缓存
- 页面操作(如上架/下架)后清除对应缓存
-
批量失效:
- 使用前缀清除策略,如清除所有
supplies_${userId}_开头的缓存
- 使用前缀清除策略,如清除所有
3. 实施方案
3.1 实施步骤
-
第一步:添加缓存管理器
- 在
supply.html中添加CacheManager类 - 初始化缓存管理器实例
- 在
-
第二步:优化数据加载
- 修改
loadSupplies函数,添加缓存逻辑 - 实现增量加载和分页
- 修改
-
第三步:实现虚拟滚动
- 添加
VirtualScroll类 - 修改渲染逻辑,使用虚拟滚动
- 添加
-
第四步:添加缓存失效机制
- 在数据变更操作后添加缓存清除逻辑
- 实现缓存过期检查
-
第五步:测试和优化
- 测试不同数据量下的性能
- 调整缓存时间和分页大小
3.2 预期效果
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 首次加载时间 | 5-10秒 | 2-3秒 | ~60% |
| 页面刷新时间 | 3-5秒 | 0.5-1秒 | ~80% |
| 内存使用 | 500MB+ | 100-200MB | ~60% |
| 可支持数据量 | 500-1000条 | 5000+条 | ~5x |
4. 监控和维护
4.1 监控指标
-
前端指标:
- 页面加载时间
- 数据加载时间
- 内存使用情况
- 缓存命中率
-
后端指标:
- API 响应时间
- 数据库查询时间
- 缓存命中率
4.2 维护建议
-
定期清理:
- 定期清理过期缓存
- 监控 localStorage 使用情况
-
性能分析:
- 使用 Chrome DevTools 分析性能
- 识别瓶颈并优化
-
扩展性:
- 设计模块化的缓存系统
- 支持未来添加更多缓存策略
5. 代码集成示例
5.1 完整的缓存集成
// 在页面初始化时
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. 总结
本实施方案通过以下核心技术提升性能:
- 多级缓存:内存 + localStorage + 后端 Redis
- 增量加载:分页获取数据,减少一次性加载压力
- 虚拟滚动:只渲染可视区域的数据,减少 DOM 操作
- 智能缓存策略:基于用户角色和数据类型的差异化缓存
- 缓存失效机制:时间过期 + 主动失效
通过这些优化,系统可以支持:
- 管理员查看和管理大量货源数据
- 快速页面加载和响应
- 稳定的性能表现,即使在数据量增长的情况下
此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。