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

企业级缓存架构实施方案

1. 问题分析

当前 supply.html 页面存在以下性能问题:

  1. 无缓存机制:每次加载页面或刷新列表时都重新从API获取所有数据
  2. 一次性获取大量数据:使用 pageSize: 1000 一次性获取所有数据
  3. 重复计算和过滤:每次都需要重新处理和过滤数据
  4. 无数据变更检测:无法检测数据是否发生变化,只能通过完全重新加载来更新
  5. 无虚拟滚动:即使使用分页,也没有实现虚拟滚动来优化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 优化

  1. 实现增量更新

    • 添加 lastUpdated 参数,只返回比该时间更新的数据
    • 实现 ETagIf-None-Match 头,支持缓存验证
  2. 数据压缩

    • 启用 Gzip/Brotli 压缩
    • 对大型数据集使用分页和字段过滤
  3. 缓存层

    • 在后端添加 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 缓存失效策略

  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 完整的缓存集成

// 在页面初始化时
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. 缓存失效机制:时间过期 + 主动失效

通过这些优化,系统可以支持:

  • 管理员查看和管理大量货源数据
  • 快速页面加载和响应
  • 稳定的性能表现,即使在数据量增长的情况下

此方案不仅解决了当前的性能问题,也为未来的系统扩展奠定了基础。