欢迎光临
我们一直在努力

结果缓存机制:GeoAI-UP的执行结果缓存与失效策略

在空间分析系统中,缓存是提升性能的关键手段。GeoAI-UP实现了多层缓存架构,本文深入剖析其设计思想、实现细节和失效策略。


一、缓存架构全景

1.1 为什么需要多层缓存?

缓存的本质是一种空间换时间的策略,其核心目标是减少重复计算、降低响应延迟、优化资源利用。在空间分析系统中,数据处理涉及多个层次,不同层次的缓存有着不同的特点和适用场景。

缓存理论基础:

  • 局部性原理:程序访问的数据在时间和空间上具有局部性,近期访问的数据很可能被再次访问(时间局部性),相邻位置的数据也可能被访问(空间局部性)
  • 阿姆达尔定律:系统性能提升 = 1 / ((1 – 并行化比例) + 并行化比例 / 加速比)
  • 缓存命中率公式:命中率 = 命中次数 / (命中次数 + 未命中次数) × 100%

在空间分析场景中,多层缓存尤为重要:

  • 瓦片访问模式:用户浏览地图时,通常在一个区域内缩放和平移,热点瓦片会被频繁访问
  • 分析结果复用:相同的分析参数可能被多次使用
  • LLM交互成本高:Prompt模板加载和LLM调用都需要时间,缓存可以显著降低延迟
层次延迟(ms)容量持久性适用场景
HTTP客户端 <1 小 会话级 静态资源、频繁访问的结果
服务端内存 1-10 中 进程级 热点数据、临时计算结果
文件系统 10-100 大 持久化 分析结果、瓦片存储
数据库 100-1000 超大 持久化 空间数据、索引

多层缓存的设计原理:

缓存层次设计遵循金字塔结构,从顶层到底层,延迟逐渐增加,容量逐渐增大:

  • L1缓存(HTTP客户端缓存):

    • 位于用户浏览器或CDN边缘节点
    • 延迟最低(<1ms),直接响应用户请求
    • 存储静态资源和频繁访问的结果
    • 设计目的:减少网络传输,降低服务器负载
  • L2缓存(服务端内存缓存):

    • 位于应用进程内存中
    • 延迟较低(1-10ms),O(1)读写复杂度
    • 存储热点数据和临时计算结果
    • 设计目的:避免重复计算,提升响应速度
  • L3缓存(文件系统缓存):

    • 位于磁盘存储
    • 延迟中等(10-100ms),持久化存储
    • 存储分析结果和瓦片文件
    • 设计目的:跨进程共享,支持服务重启恢复
  • L4缓存(数据库缓存):

    • 位于PostGIS数据库
    • 延迟较高(100-1000ms),容量最大
    • 存储原始空间数据和空间索引
    • 设计目的:支持复杂空间查询和事务

多层缓存协作原则:

  • 向下查找:请求从顶层开始,逐层向下查找
  • 向上写入:数据生成后,逐层向上缓存
  • 智能失效:数据变更时,逐层清除相关缓存
  • 1.2 多层缓存体系

    ┌─────────────────────────────────────────────────────────────────┐
    │ GeoAI-UP缓存架构 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ ┌─────────────────────────────────────────────────────────┐ │
    │ │ 第1层:HTTP客户端缓存 │ │
    │ │ – Cache-Control响应头 (max-age控制过期时间) │ │
    │ │ – 浏览器本地缓存 (减少重复请求) │ │
    │ │ – CDN缓存 (全球加速) │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ 未命中 │
    │ ┌─────────────────────────────────────────────────────────┐ │
    │ │ 第2层:服务端内存缓存 │ │
    │ │ – Prompt模板缓存 (减少文件IO) │ │
    │ │ – MVT瓦片缓存 (加速瓦片请求) │ │
    │ │ – 数据访问层缓存 (减少数据库查询) │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ 未命中 │
    │ ┌─────────────────────────────────────────────────────────┐ │
    │ │ 第3层:文件系统缓存 │ │
    │ │ – MVT瓦片文件存储 (持久化) │ │
    │ │ – GeoJSON/GeoTIFF结果文件 (分析结果持久化) │ │
    │ │ – 元数据JSON文件 (配置信息) │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ▼ 未命中 │
    │ ┌─────────────────────────────────────────────────────────┐ │
    │ │ 第4层:数据库缓存 │ │
    │ │ – PostGIS临时表 (中间结果) │ │
    │ │ – 空间索引 (加速空间查询) │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────────┘

    1.3 缓存类型对比

    缓存层存储介质生命周期容量限制失效策略典型延迟
    HTTP客户端 浏览器/CDN max-age控制 浏览器决定 时间过期 <1ms
    内存缓存 RAM 进程存活期 配置上限(10000瓦片) LRU淘汰 1-10ms
    文件系统 磁盘 持久化 磁盘容量 定时清理 10-100ms
    数据库 PostGIS 连接存活期 数据库限制 连接断开/DROP 100-1000ms

    二、服务端内存缓存

    2.1 Prompt模板缓存

    原理说明:Prompt模板是LLM交互的核心,每次请求都需要加载模板。由于模板文件存储在磁盘上,频繁的文件IO会成为性能瓶颈。内存缓存将模板加载到内存中,避免重复读取。

    性能分析:

    • 磁盘IO延迟:约100ms/次
    • 内存访问延迟:约1ms/次
    • 性能提升:约100倍

    缓存适用性判断:

    • 数据大小:模板文件通常较小(KB级),适合内存存储
    • 访问频率:高频访问,每次LLM调用都需要加载模板
    • 更新频率:低频更新,通常在部署时更新
    • 一致性要求:可接受短暂不一致,支持热更新机制

    缓存设计模式选择:

    • Singleton模式:全局唯一实例,避免重复创建
    • Cache-Aside模式:先查缓存,未命中再加载,然后存入缓存
    • Write-Through模式:更新时同时更新缓存(用于热更新)

    // PromptManager.ts – 核心缓存实现

    /**
    * Prompt模板管理器
    *
    * 核心职责:
    * 1. 从文件系统加载Prompt模板(Markdown格式)
    * 2. 将模板解析后缓存到内存中,避免重复IO
    * 3. 支持多语言模板的加载和切换(en-US, zh-CN等)
    * 4. 提供缓存清理接口,支持热更新机制
    *
    * 缓存设计原理:
    * – 使用TypeScript Map作为缓存容器,提供O(1)的查找和插入复杂度
    * – 缓存键设计为 `${language}/${templateId}`,确保多语言模板隔离
    * – 缓存失效策略:采用主动失效模式,模板文件修改后手动触发清理
    * – 线程安全:Node.js单线程模型天然保证Map操作的原子性
    */

    export class PromptManager {
    private promptsDir: string; // 模板文件存放目录,如:workspace/llm/prompts/
    private templateCache: Map<string, PromptTemplate> = new Map(); // 内存缓存容器

    /**
    * 构造函数
    * @param promptsDir – Prompt模板文件所在目录的绝对路径
    */

    constructor(promptsDir: string) {
    this.promptsDir = promptsDir;
    console.log(`[PromptManager] Initialized with prompts directory: ${promptsDir}`);
    }

    /**
    * 加载Prompt模板(核心方法)
    *
    * @param templateId – 模板标识符,对应文件名(不含扩展名)
    * @param language – 语言代码,如 'en-US', 'zh-CN'(默认英文)
    * @returns Promise<PromptTemplate> – 加载并解析后的模板对象
    *
    * 执行流程:
    * 1. 构建缓存键:language/templateId
    * 2. 检查内存缓存(O(1))
    * 3. 缓存命中:直接返回缓存的模板对象
    * 4. 缓存未命中:从文件系统读取 → 解析 → 存入缓存 → 返回
    */

    async loadTemplate(templateId: string, language: string = 'en-US'): Promise<PromptTemplate> {
    // 构建缓存键:语言+模板ID,确保多语言隔离
    // 例如:zh-CN/goal-splitting, en-US/task-planning
    const cacheKey = `${language}/${templateId}`;

    // 缓存命中检查:Map.has() 复杂度 O(1)
    if (this.templateCache.has(cacheKey)) {
    console.log(`[PromptManager] Cache hit for: ${cacheKey}`);
    return this.templateCache.get(cacheKey)!; // 非空断言:已确认存在
    }

    // 缓存未命中:需要从文件系统加载
    console.log(`[PromptManager] Cache miss for: ${cacheKey}, loading from file…`);

    // 构建文件路径:{promptsDir}/{language}/{templateId}.md
    const filePath = path.join(this.promptsDir, language, `${templateId}.md`);

    // 加载并解析模板文件
    // loadFromFile内部会处理文件读取、Markdown解析、变量提取等操作
    const template = await this.loadFromFile(filePath, templateId, language);

    // 存入缓存:后续相同请求直接从内存获取
    this.templateCache.set(cacheKey, template);

    return template;
    }

    /**
    * 从文件系统加载模板(内部方法)
    * @param filePath – 模板文件的绝对路径
    * @param templateId – 模板标识符
    * @param language – 语言代码
    * @returns Promise<PromptTemplate> – 解析后的模板对象
    */

    private async loadFromFile(filePath: string, templateId: string, language: string): Promise<PromptTemplate> {
    // 实际实现会包含:
    // 1. 文件存在性检查
    // 2. 读取文件内容(utf-8编码)
    // 3. 解析Markdown格式
    // 4. 提取模板变量(如 {{input}}, {{context}})
    // 5. 返回结构化的PromptTemplate对象

    // 简化实现示例:
    const content = await fs.promises.readFile(filePath, 'utf-8');
    return {
    id: templateId,
    language,
    content,
    variables: this.extractVariables(content), // 提取模板变量
    compiled: this.compileTemplate(content) // 预编译模板
    };
    }

    /**
    * 提取模板中的变量
    * @param content – 模板内容
    * @returns string[] – 变量名数组
    */

    private extractVariables(content: string): string[] {
    const matches = content.match(/\\{\\{(\\w+)\\}\\}/g) || [];
    return matches.map(m => m.slice(2, –2)); // 去除 {{ 和 }}
    }

    /**
    * 编译模板为可执行函数
    * @param content – 模板内容
    * @returns (data: Record<string, string>) => string – 编译后的函数
    */

    private compileTemplate(content: string): (data: Record<string, string>) => string {
    // 使用正则替换实现简单的模板编译
    return (data: Record<string, string>) => {
    return content.replace(/\\{\\{(\\w+)\\}\\}/g, (_, key) => data[key] || '');
    };
    }

    /**
    * 缓存清理方法(支持热更新)
    *
    * @param templateId – 可选,指定清理的模板ID,不指定则清理所有
    *
    * 设计意图:
    * – 支持单个模板清理:用于热更新场景,模板文件修改后清理对应缓存
    * – 支持全部清理:用于系统重启或大规模更新场景
    * – 原子操作:遍历并删除,避免部分清理导致的不一致
    */

    clearCache(templateId?: string): void {
    if (templateId) {
    // 清除特定模板的所有语言版本
    console.log(`[PromptManager] Clearing cache for template: ${templateId}`);

    // 收集需要删除的键,避免在遍历过程中修改Map
    const keysToDelete: string[] = [];
    for (const key of this.templateCache.keys()) {
    if (key.endsWith(`/${templateId}`)) {
    keysToDelete.push(key);
    }
    }

    // 批量删除
    for (const key of keysToDelete) {
    this.templateCache.delete(key);
    }
    } else {
    // 清除所有缓存
    console.log(`[PromptManager] Clearing all cache (${this.templateCache.size} entries)`);
    this.templateCache.clear();
    }
    }

    /**
    * 获取缓存统计信息
    * @returns {size: number} – 当前缓存条目数量
    */

    getCacheStats(): { size: number } {
    return { size: this.templateCache.size };
    }
    }

    设计要点深度解析:

  • 缓存键设计原理

    • 使用${language}/${templateId}作为键,确保不同语言的同名模板不会冲突
    • 例如:en-US/goal-splitting 和 zh-CN/goal-splitting 是两个独立的缓存条目
  • 缓存命中率计算

    • 命中率 = 命中次数 / (命中次数 + 未命中次数)
    • 对于Prompt模板,由于模板数量有限且访问频繁,命中率通常在95%以上
  • 缓存失效时机

    • 模板文件修改时(通过文件监听触发clearCache)
    • 系统启动时(缓存为空,首次加载)
    • 手动触发热更新
  • 2.2 MVT瓦片缓存

    原理说明:MVT(Mapbox Vector Tile)瓦片是地图渲染的核心数据格式,每个瓦片需要从PostGIS数据库查询空间数据并编码为Protocol Buffers格式。由于地图浏览具有强局部性特征(用户通常在一个区域内缩放和平移),热点瓦片会被频繁访问。内存缓存可以显著减少重复计算,提升地图加载性能。

    MVT瓦片生成性能分析:

    • 从PostGIS查询并生成一个瓦片:约500ms
    • 内存缓存访问:约1ms
    • 性能提升:约500倍

    瓦片访问模式分析:

    • 空间局部性:用户在地图上平移时,相邻瓦片会被连续访问
    • 时间局部性:同一区域的瓦片在缩放操作中会被多次访问
    • 金字塔访问:缩放时会访问不同级别的瓦片

    缓存容量计算:

    • 单个瓦片平均大小:~50KB
    • 缓存容量:10000个瓦片
    • 内存占用:~500MB
    • 设计权衡:平衡内存占用和缓存命中率,500MB是大多数服务器可接受的内存开销

    // MVTOnDemandPublisher.ts – 瓦片缓存实现

    /**
    * 瓦片缓存接口定义
    *
    * 设计原则:
    * 1. **简洁性**:只暴露必要的操作(get/set/has/clear),保持接口最小化
    * 2. **类型安全**:使用TypeScript接口约束实现,确保类型一致性
    * 3. **可替换性**:支持不同缓存实现(内存、Redis、文件系统等)
    * 4. **解耦性**:发布器依赖抽象接口而非具体实现,便于测试和替换
    */

    export interface TileCache {
    /**
    * 获取缓存数据
    * @param key – 缓存键
    * @returns Buffer | null – 瓦片数据(Buffer格式)或null(未命中)
    */

    get(key: string): Buffer | null;

    /**
    * 设置缓存数据
    * @param key – 缓存键
    * @param data – 瓦片数据(Buffer格式)
    */

    set(key: string, data: Buffer): void;

    /**
    * 检查缓存是否存在
    * @param key – 缓存键
    * @returns boolean – 是否存在
    */

    has(key: string): boolean;

    /**
    * 清空所有缓存
    */

    clear(): void;
    }

    /**
    * 内存瓦片缓存实现(FIFO版本)
    *
    * 核心特性:
    * – 基于ES6 Map实现,提供O(1)的读写复杂度
    * – 容量限制:防止内存溢出,保护系统稳定性
    * – FIFO淘汰策略:优先删除最早加入的条目
    *
    * 设计权衡:
    * – 使用FIFO而非真正的LRU:实现简单,性能更好,无需维护访问顺序
    * – 容量设为10000:平衡内存占用(~500MB)和缓存命中率
    * – 适用于瓦片访问模式相对均匀的场景
    *
    * 数据结构选择:
    * – Map vs Object:Map保持插入顺序(ES6规范),适合FIFO实现
    * – Buffer存储:瓦片数据为二进制格式,Buffer是Node.js最佳选择
    */

    class InMemoryTileCache implements TileCache {
    private cache: Map<string, Buffer> = new Map(); // 缓存容器,键为瓦片标识,值为二进制数据
    private maxSize: number; // 缓存容量上限,防止内存无限增长

    /**
    * 构造函数
    * @param maxSize – 缓存最大容量(默认10000个瓦片)
    *
    * 容量选择原理:
    * – 单个MVT瓦片平均大小:~50KB(根据数据复杂度有所差异)
    * – 10000个瓦片 × 50KB = ~500MB内存占用
    * – 500MB是合理的平衡点:既保证较高命中率,又不会占用过多内存
    * – 可根据服务器配置调整此参数
    */

    constructor(maxSize: number = 10000) {
    this.maxSize = maxSize;
    console.log(`[InMemoryTileCache] Initialized with max size: ${maxSize} tiles`);
    console.log(`[InMemoryTileCache] Estimated memory usage: ${(maxSize * 50 / 1024).toFixed(1)} MB`);
    }

    /**
    * 获取缓存数据
    * @param key – 缓存键,格式:${tilesetId}:${z}:${x}:${y}
    * @returns Buffer | null – 瓦片数据或null(未命中)
    *
    * 执行流程:
    * 1. 使用Map.get()查找(O(1)复杂度)
    * 2. 记录调试日志(生产环境可关闭)
    * 3. 返回数据或null
    */

    get(key: string): Buffer | null {
    const data = this.cache.get(key);
    if (data) {
    console.debug(`[InMemoryTileCache] Hit: ${key}`);
    } else {
    console.debug(`[InMemoryTileCache] Miss: ${key}`);
    }
    return data || null;
    }

    /**
    * 设置缓存数据
    * @param key – 缓存键,格式:${tilesetId}:${z}:${x}:${y}
    * @param data – 瓦片数据(Buffer格式)
    *
    * 执行流程:
    * 1. 检查缓存容量是否达到上限
    * 2. 若达到上限,执行FIFO淘汰(删除最早插入的条目)
    * 3. 将新数据插入缓存(Map会自动将其放在最后)
    *
    * FIFO淘汰策略原理:
    * – ES6 Map保持插入顺序,keys()返回的第一个元素即为最早插入的
    * – 使用Iterator API获取第一个键:this.cache.keys().next().value
    * – 时间复杂度:O(1)(获取第一个元素) + O(1)(删除) + O(1)(插入)
    */

    set(key: string, data: Buffer): void {
    // 容量检查:超过上限时执行FIFO淘汰
    if (this.cache.size >= this.maxSize) {
    // 获取第一个键(最早插入的)
    const firstKey = this.cache.keys().next().value;
    if (firstKey) {
    console.debug(`[InMemoryTileCache] Evicting (FIFO): ${firstKey}`);
    this.cache.delete(firstKey);
    }
    }
    // 插入新条目(Map会自动将其放在最后,成为最新的)
    this.cache.set(key, data);
    console.debug(`[InMemoryTileCache] Added: ${key}, size: ${data.length} bytes`);
    }

    /**
    * 检查缓存是否存在
    * @param key – 缓存键
    * @returns boolean – 是否存在
    *
    * 注意:此方法不记录访问时间,仅用于存在性检查
    */

    has(key: string): boolean {
    return this.cache.has(key);
    }

    /**
    * 清空所有缓存
    *
    * 使用场景:
    * – 系统维护期间
    * – 大规模数据更新后
    * – 内存占用过高时
    */

    clear(): void {
    console.log(`[InMemoryTileCache] Cleared ${this.cache.size} entries`);
    this.cache.clear();
    }

    /**
    * 获取缓存统计信息
    * @returns {size: number, maxSize: number} – 当前缓存大小和容量上限
    */

    getStats(): { size: number; maxSize: number } {
    return {
    size: this.cache.size,
    maxSize: this.maxSize
    };
    }
    }

    瓦片缓存键设计原理:

    // 缓存键 = tilesetId:zoom:x:y
    const cacheKey = `${tilesetId}:${z}:${x}:${y}`;

    /**
    * 键设计原理:
    *
    * 1. tilesetId:瓦片集唯一标识,由UUID生成或数据库主键
    * – 作用:区分不同的数据源和分析结果
    * – 示例:mvt_abc123, mvt_buffer_result_xyz
    *
    * 2. zoom(z):缩放级别,范围0-18(Web Mercator标准)
    * – 作用:标识地图缩放层级
    * – 级别0:全球视图,级别18:最大缩放(约1米精度)
    *
    * 3. x/y:瓦片坐标(TMS/OSM坐标系)
    * – TMS(Tile Map Service)坐标系
    * – 原点在左下角,从0,0开始
    * – 每个缩放级别下,瓦片数量为 2^z × 2^z
    *
    * 示例:mvt_abc123:10:345:678
    * – 瓦片集:mvt_abc123
    * – 缩放级别:10(约150米精度)
    * – X坐标:345
    * – Y坐标:678
    *
    * 唯一性保证:
    * – tilesetId全局唯一(UUID或业务主键)
    * – (z,x,y)在同一瓦片集内唯一标识一个瓦片
    * – 四部分组合后确保全局唯一
    *
    * 命名规范:
    * – 使用冒号(:)作为分隔符(URI-safe,无需编码)
    * – 避免使用特殊字符,确保跨平台兼容性
    * – 格式简洁,便于日志阅读和调试
    */

    缓存键设计的工程考量:

    设计要素考虑内容决策理由
    唯一性 使用UUID作为tilesetId 避免冲突,支持分布式部署
    可读性 使用冒号分隔,结构清晰 便于调试和日志分析
    可扩展性 预留版本号位置 支持未来版本化缓存
    性能 字符串拼接开销小 Map查找为O(1),键长度影响可忽略
    兼容性 不使用特殊字符 支持文件系统存储(部分系统对文件名有限制)

    缓存键演变路径:

    // 第一版:简单但不够唯一
    const v1Key = `${z}–${x}–${y}`; // ❌ 不同瓦片集冲突

    // 第二版:增加瓦片集标识
    const v2Key = `${tilesetId}:${z}:${x}:${y}`; // ✅ 当前实现

    // 第三版:支持版本管理(未来扩展)
    const v3Key = `${tilesetId}:v1.0:${z}:${x}:${y}`; // 支持数据版本

    // 第四版:支持数据校验(高级)
    const v4Key = `${tilesetId}:${dataHash}:${z}:${x}:${y}`; // 数据变化自动失效

    缓存使用流程详解:

    ┌─────────────────────────────────────────────────────────────────┐
    │ 瓦片请求缓存流程 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ 1. Client Request: /api/mvt/{tilesetId}/{z}/{x}/{y}.pbf │
    │ │ │
    │ ▼ │
    │ 2. 构建缓存键: `${tilesetId}:${z}:${x}:${y}` │
    │ │ │
    │ ▼ │
    │ 3. 内存缓存命中? │
    │ │ │
    │ Yes │ No │
    │ │ │ │ │
    │ ▼ │ ▼ │
    │ 返回缓存 │ 检查文件系统缓存 │
    │ 数据 │ │ │
    │ │ ▼ │
    │ │ 文件缓存命中? │
    │ │ │ │
    │ │ Yes │ No │
    │ │ │ │ │ │
    │ │ ▼ │ ▼ │
    │ │ 返回 │ 查询PostGIS生成瓦片 │
    │ │ 文件 │ │ │
    │ │ │ ▼ │
    │ │ │ 存入内存缓存 + 文件缓存 │
    │ │ │ │ │
    │ └───────┴─────┘ │
    │ ▼ │
    │ 返回瓦片数据给客户端 │
    │ │
    └─────────────────────────────────────────────────────────────────┘


    三、HTTP客户端缓存

    3.1 Cache-Control策略原理

    原理说明:HTTP缓存通过响应头控制浏览器和CDN的缓存行为。合理的缓存策略可以显著减少重复请求,提升用户体验。

    // ResultController.ts – 不同资源的缓存策略

    /**
    * 服务GeoJSON分析结果
    *
    * 缓存策略设计:
    * – max-age=3600(1小时):分析结果相对稳定,但可能被更新
    * – public:允许CDN和代理缓存
    * – Access-Control-Allow-Origin:支持跨域访问
    */

    async serveGeoJSON(req: Request, res: Response): Promise<void> {
    try {
    const { id } = req.params;
    const filePath = path.join(this.workspaceBase, 'results', 'geojson', `${id}.geojson`);

    // 文件存在检查
    if (!fs.existsSync(filePath)) {
    res.status(404).json({
    success: false,
    error: 'Result not found'
    });
    return;
    }

    // 读取文件内容
    const content = fs.readFileSync(filePath, 'utf-8');

    // 设置HTTP缓存头
    // public: 允许任何缓存(浏览器、CDN、代理)存储此响应
    // max-age=3600: 缓存有效时间为1小时(3600秒)
    res.setHeader('Cache-Control', 'public, max-age=3600');
    // 允许跨域访问
    res.setHeader('Access-Control-Allow-Origin', '*');
    // 设置正确的Content-Type
    res.setHeader('Content-Type', 'application/json');

    res.send(content);
    } catch (error) {
    console.error('[Result Controller] Error serving GeoJSON:', error);
    res.status(500).json({ success: false, error: 'Internal server error' });
    }
    }

    /**
    * 服务GeoTIFF栅格数据
    *
    * 缓存策略设计:
    * – max-age=3600(1小时):栅格数据通常较大,缓存可减少带宽消耗
    * – 使用流式传输:避免一次性加载大文件到内存
    */

    async serveGeoTIFF(req: Request, res: Response): Promise<void> {
    try {
    const { id } = req.params;
    const filePath = path.join(this.workspaceBase, 'results', 'geotiff', `${id}.tif`);

    if (!fs.existsSync(filePath)) {
    res.status(404).json({ success: false, error: 'GeoTIFF not found' });
    return;
    }

    // 设置缓存头
    res.setHeader('Cache-Control', 'public, max-age=3600');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Content-Type', 'image/tiff');
    res.setHeader('Content-Disposition', `inline; filename="${id}.tif"`);

    // 流式传输:适合大文件,减少内存占用
    const stream = fs.createReadStream(filePath);
    stream.pipe(res);

    stream.on('error', (error) => {
    console.error('[Result Controller] Stream error:', error);
    if (!res.headersSent) {
    res.status(500).json({ success: false, error: 'File read error' });
    }
    });
    } catch (error) {
    console.error('[Result Controller] Error serving GeoTIFF:', error);
    res.status(500).json({ success: false, error: 'Internal server error' });
    }
    }

    不同资源的缓存策略对比:

    资源类型Cache-Control时长设计原理
    GeoJSON结果 public, max-age=3600 1小时 分析结果相对稳定,1小时内重复请求可直接返回
    GeoTIFF结果 public, max-age=3600 1小时 栅格数据较大,缓存减少带宽消耗
    Heatmap结果 public, max-age=3600 1小时 热力图计算成本高,缓存避免重复计算
    HTML报告 public, max-age=3600 1小时 报告生成后通常不会变更
    WMS服务 public, max-age=86400 24小时 影像数据更新频率低
    MVT瓦片 public, max-age=86400 24小时 空间数据相对稳定,瓦片复用率高
    Chat API no-cache 实时 对话消息需要最新,禁止缓存

    Cache-Control指令详解:

    指令含义适用场景
    public 允许任何缓存存储 公共资源、静态文件
    private 仅允许浏览器缓存 敏感数据、用户特定内容
    no-cache 必须向服务器验证 实时数据、动态内容
    no-store 禁止任何缓存 敏感信息、一次性数据
    max-age=N 缓存有效时间(秒) 所有需要缓存的资源
    must-revalidate 过期后必须验证 需要 freshness 保证的资源

    3.2 缓存策略设计原则

    ┌─────────────────────────────────────────────────────────────────┐
    │ 缓存策略决策矩阵 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ 资源类型 变化频率 缓存时长 设计原理 │
    │ ───────────────────────────────────────────────────────────── │
    │ │
    │ 分析结果 中等 1小时 分析任务完成后稳定 │
    │ (可能被更新) 但需要支持更新通知 │
    │ │
    │ 地图瓦片 低 24小时 空间数据相对稳定 │
    │ (极少更新) 瓦片复用率高 │
    │ │
    │ WMS服务 低 24小时 影像数据更新频率低 │
    │ (每日/每周) 减少服务器压力 │
    │ │
    │ Chat消息 高 no-cache 实时对话需最新 │
    │ (每条都新) 禁止缓存 │
    │ │
    │ Prompt模板 低 进程周期 部署时更新,运行时稳定 │
    │ (部署更新) 内存缓存即可 │
    │ │
    └─────────────────────────────────────────────────────────────────┘


    四、文件系统缓存

    4.1 结果文件组织原理

    原理说明:文件系统缓存是持久化存储层,用于保存分析结果和瓦片数据。合理的目录结构设计可以提高文件管理效率和访问性能。

    // BaseMVTPublisher.ts – 文件系统组织

    /**
    * MVT发布器基类
    *
    * 核心职责:
    * 1. 管理输出目录结构
    * 2. 提供元数据持久化能力
    * 3. 统一瓦片存储和读取接口
    *
    * 目录结构设计原理:
    * – 将不同类型的结果分开存储
    * – 使用唯一ID作为文件名,避免冲突
    * – 支持跨平台路径处理
    */

    export class BaseMVTPublisher {
    protected workspaceBase: string; // 工作空间根目录
    protected mvtOutputDir: string; // MVT瓦片输出目录

    /**
    * 构造函数
    * @param workspaceBase – 工作空间根目录
    * @param outputSubdir – 输出子目录名(默认'mvt')
    */

    constructor(workspaceBase: string, outputSubdir: string = 'mvt') {
    this.workspaceBase = workspaceBase;
    this.mvtOutputDir = this.ensureOutputDir(outputSubdir);
    console.log(`[BaseMVTPublisher] Initialized with output dir: ${this.mvtOutputDir}`);
    }

    /**
    * 确保输出目录存在
    * @param subdir – 子目录名
    * @returns string – 完整目录路径
    *
    * 设计原理:
    * – 使用path.join处理跨平台路径
    * – recursive: true 确保创建所有父目录
    * – 幂等操作:多次调用不会出错
    */

    protected ensureOutputDir(subdir: string): string {
    // 构建完整路径:{workspace}/results/{subdir}
    const dir = path.join(this.workspaceBase, 'results', subdir);

    // 如果目录不存在则创建
    if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
    console.log(`[BaseMVTPublisher] Created directory: ${dir}`);
    }

    return dir;
    }
    }

    文件系统目录结构设计:

    workspace/ # 工作空间根目录
    ├── results/ # 所有结果文件根目录
    │ ├── geojson/ # GeoJSON分析结果
    │ │ ├── {resultId}.geojson # 分析结果文件
    │ │ └── …
    │ ├── geotiff/ # GeoTIFF栅格结果
    │ │ ├── {resultId}.tif # 栅格数据文件
    │ │ └── …
    │ ├── heatmaps/ # 热力图结果
    │ │ ├── {resultId}.geojson # 热力图GeoJSON
    │ │ └── …
    │ ├── reports/ # HTML报告
    │ │ ├── {resultId}.html # 报告文件
    │ │ └── …
    │ └── mvt/ # MVT瓦片
    │ ├── {tilesetId}/ # 瓦片集目录
    │ │ ├── metadata.json # 瓦片集元数据
    │ │ ├── tilejson.json # TileJSON配置(可选)
    │ │ └── {z}/ # 缩放级别目录
    │ │ └── {x}/ # X坐标目录
    │ │ └── {y}.pbf # 瓦片文件
    │ └── …

    目录结构设计原理:

  • 按类型分离:不同类型的结果存储在不同目录,便于管理和清理
  • 唯一ID命名:使用UUID或时间戳生成唯一ID,避免文件名冲突
  • 层级化瓦片存储:{z}/{x}/{y}.pbf 结构符合OGC标准,便于CDN缓存
  • 元数据独立:metadata.json存储瓦片集信息,便于快速检索
  • 4.2 元数据持久化原理

    // BaseMVTPublisher.ts – 元数据管理

    /**
    * 保存瓦片集元数据到磁盘
    * @param tilesetId – 瓦片集唯一标识
    * @param metadata – 元数据对象
    *
    * 设计原理:
    * – 将元数据序列化为JSON格式
    * – 存储在瓦片集目录下,与瓦片文件同目录
    * – 便于加载和管理
    */

    protected saveMetadata(tilesetId: string, metadata: any): void {
    // 确保瓦片集目录存在
    const tilesetDir = path.join(this.mvtOutputDir, tilesetId);
    if (!fs.existsSync(tilesetDir)) {
    fs.mkdirSync(tilesetDir, { recursive: true });
    }

    // 构建元数据文件路径
    const metadataPath = path.join(tilesetDir, 'metadata.json');

    // 序列化并写入文件(格式化输出,便于阅读)
    fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
    console.log(`[BaseMVTPublisher] Saved metadata: ${metadataPath}`);
    }

    /**
    * 从磁盘加载瓦片集元数据
    * @param tilesetId – 瓦片集唯一标识
    * @returns any | null – 元数据对象或null(不存在时)
    *
    * 设计原理:
    * – 读取JSON文件并解析
    * – 处理文件不存在和解析错误的情况
    * – 返回null表示元数据不存在
    */

    protected loadMetadata(tilesetId: string): any | null {
    const metadataPath = path.join(this.mvtOutputDir, tilesetId, 'metadata.json');

    // 检查文件是否存在
    if (fs.existsSync(metadataPath)) {
    try {
    // 读取并解析JSON
    const content = fs.readFileSync(metadataPath, 'utf-8');
    const metadata = JSON.parse(content);
    console.log(`[BaseMVTPublisher] Loaded metadata: ${tilesetId}`);
    return metadata;
    } catch (error) {
    // 解析错误处理
    console.error(`[BaseMVTPublisher] Failed to load metadata for ${tilesetId}:`, error);
    return null;
    }
    }

    // 文件不存在
    return null;
    }

    元数据结构详解:

    {
    "tilesetId": "mvt_1234567890_abc",
    "name": "缓冲区分析结果", // 显示名称
    "description": "500米缓冲区分析", // 描述信息
    "dataSource": "postgis://localhost:5432/db/table", // 数据源
    "createdAt": "2024-01-15T10:30:00Z", // 创建时间
    "updatedAt": "2024-01-15T10:35:00Z", // 更新时间
    "tileInfo": {
    "minZoom": 0, // 最小缩放级别
    "maxZoom": 18, // 最大缩放级别
    "bounds": [–180, –90, 180, 90] // 地理范围
    },
    "properties": {
    "sourceType": "postgis", // 数据源类型
    "featureCount": 1000, // 要素数量
    "geometryType": "Polygon" // 几何类型
    }
    }

    元数据字段设计原理:

    字段用途设计意图
    tilesetId 唯一标识 用于URL和文件路径
    name/description 显示信息 前端展示用
    dataSource 数据源信息 追踪数据来源
    createdAt/updatedAt 时间戳 缓存失效判断
    tileInfo 瓦片配置 前端渲染配置
    properties 扩展属性 灵活存储额外信息

    五、缓存失效策略

    5.1 内存缓存失效原理

    原理说明:缓存失效(Cache Invalidation)是保证数据一致性的关键机制。当底层数据源发生变化时,必须清除相关的缓存条目,否则用户会获取到过期(stale)数据。

    缓存一致性模型:

    • 强一致性:缓存与数据源实时同步,任何修改立即反映到缓存
    • 最终一致性:缓存与数据源在一段时间后达到一致
    • GeoAI-UP选择:采用主动失效+时间过期的混合策略,平衡一致性和性能

    缓存失效策略对比:

    策略实现方式一致性复杂度适用场景
    主动失效 数据变更时显式清除缓存 强一致性 中 写少读多的场景
    时间过期 设置TTL自动过期 最终一致性 低 数据变化不频繁
    版本管理 缓存键包含版本号 强一致性 高 需要精确控制
    事件驱动 监听数据变更事件 强一致性 高 分布式系统

    失效时机分析:

    • 立即失效:数据修改后立即清除缓存,适用于一致性要求高的场景
    • 延迟失效:数据修改后延迟一段时间再清除,适用于写频繁场景
    • 异步失效:通过消息队列异步清除,适用于分布式系统

    GeoAI-UP的失效策略设计:

  • 主动失效为主:删除瓦片集时同步清除相关缓存
  • 时间过期为辅:文件系统缓存设置24小时过期
  • 容量淘汰兜底:内存缓存满时自动淘汰
  • // MVTOnDemandPublisher.ts – 缓存失效机制

    /**
    * MVT按需发布器(单例模式)
    *
    * 核心职责:
    * 1. 管理瓦片缓存生命周期(创建、读取、更新、删除)
    * 2. 处理缓存失效和清理,保证数据一致性
    * 3. 协调内存缓存和文件缓存的协作
    * 4. 提供单例访问,确保全局缓存一致性
    *
    * 设计模式:
    * – **Singleton模式**:全局唯一实例,避免重复创建缓存容器
    * – **Facade模式**:提供统一接口管理多种缓存类型
    * – **Observer模式**:可扩展为监听数据变更事件
    */

    export class MVTOnDemandPublisher extends BaseMVTPublisher {
    private tileCache: TileCache; // 瓦片内存缓存(核心)
    private geojsonTileIndexes: Map<string, any>; // GeoJSON瓦片索引缓存
    private postgisConfigs: Map<string, PostGISDataSource>; // PostGIS配置缓存
    private tilesetMetadata: Map<string, MVTPublishMetadata>; // 元数据内存缓存

    // 单例实例(私有静态)
    private static instance: MVTOnDemandPublisher | null = null;

    /**
    * 获取单例实例
    * @returns MVTOnDemandPublisher – 单例实例
    */

    public static getInstance(): MVTOnDemandPublisher {
    if (!MVTOnDemandPublisher.instance) {
    MVTOnDemandPublisher.instance = new MVTOnDemandPublisher();
    }
    return MVTOnDemandPublisher.instance;
    }

    /**
    * 删除瓦片集(完整流程)
    *
    * @param tilesetId – 瓦片集ID
    * @returns boolean – 是否删除成功
    *
    * 设计原理:
    * – **两步删除策略**:先删除持久化存储,再清除内存缓存
    * – **顺序保证**:文件系统删除成功后才清除内存,避免数据丢失
    * – **原子性保障**:要么全部删除,要么全部保留
    *
    * 执行流程:
    * 1. 删除文件系统中的瓦片集目录
    * 2. 清除内存中该瓦片集的所有瓦片缓存
    * 3. 清除相关的索引、配置和元数据
    * 4. 记录日志
    */

    deleteTileset(tilesetId: string): boolean {
    console.log(`[MVT On-Demand Publisher] Deleting tileset: ${tilesetId}`);

    // 第一步:调用父类方法删除文件系统中的瓦片集
    // 父类会递归删除瓦片集目录及其所有文件
    const deleted = super.deleteTileset(tilesetId);

    if (deleted) {
    // 第二步:清除内存中该瓦片集的所有瓦片缓存
    this.clearTilesetCache(tilesetId);

    // 第三步:清除相关的辅助缓存
    this.geojsonTileIndexes.delete(tilesetId);
    this.postgisConfigs.delete(tilesetId);
    this.tilesetMetadata.delete(tilesetId);

    console.log(`[MVT On-Demand Publisher] Successfully deleted tileset: ${tilesetId}`);
    } else {
    console.warn(`[MVT On-Demand Publisher] Tileset not found or failed to delete: ${tilesetId}`);
    }

    return deleted;
    }

    /**
    * 清除特定瓦片集的内存缓存(核心方法)
    *
    * @param tilesetId – 瓦片集ID
    *
    * 设计原理:
    * – **前缀匹配删除**:缓存键格式为 `${tilesetId}:${z}:${x}:${y}`
    * – **分两步操作**:先收集再删除,避免遍历过程中修改Map
    * – **高效删除**:批量删除比逐个删除更高效
    *
    * 时间复杂度分析:
    * – 遍历缓存:O(n),n为缓存条目数
    * – 批量删除:O(k),k为匹配的条目数
    * – 总体:O(n + k)
    */

    private clearTilesetCache(tilesetId: string): void {
    // 收集需要删除的缓存键(避免在遍历过程中修改Map)
    const keysToDelete: string[] = [];
    const prefix = `${tilesetId}:`;

    // 遍历所有缓存键,匹配前缀
    for (const key of this.tileCache.keys()) {
    if (key.startsWith(prefix)) {
    keysToDelete.push(key);
    }
    }

    // 批量删除匹配的缓存条目
    const deletedCount = keysToDelete.length;
    for (const key of keysToDelete) {
    this.tileCache.delete(key);
    }

    console.log(`[MVT On-Demand Publisher] Cleared ${deletedCount} tiles from cache for tileset: ${tilesetId}`);
    }

    /**
    * 清空所有内存缓存
    *
    * 使用场景:
    * – 系统维护期间
    * – 大规模数据更新后
    * – 内存占用过高时
    * – 测试环境重置
    *
    * 设计原理:
    * – 重置所有缓存容器为初始状态
    * – 不影响文件系统缓存(持久化数据保留)
    * – 用于快速恢复系统状态
    */

    clearAllCache(): void {
    const tileCount = this.tileCache.getStats ? this.tileCache.getStats().size : 0;
    const indexCount = this.geojsonTileIndexes.size;
    const configCount = this.postgisConfigs.size;
    const metadataCount = this.tilesetMetadata.size;

    // 清除所有缓存容器
    this.tileCache.clear();
    this.geojsonTileIndexes.clear();
    this.postgisConfigs.clear();
    this.tilesetMetadata.clear();

    console.log(`[MVT On-Demand Publisher] All cache cleared:`);
    console.log(` – Tiles: ${tileCount}`);
    console.log(` – GeoJSON Indexes: ${indexCount}`);
    console.log(` – PostGIS Configs: ${configCount}`);
    console.log(` – Metadata: ${metadataCount}`);
    }

    /**
    * 清除单个瓦片缓存
    * @param tilesetId – 瓦片集ID
    * @param z – 缩放级别
    * @param x – X坐标
    * @param y – Y坐标
    */

    clearTileCache(tilesetId: string, z: number, x: number, y: number): void {
    const cacheKey = `${tilesetId}:${z}:${x}:${y}`;
    if (this.tileCache.has(cacheKey)) {
    this.tileCache.delete(cacheKey);
    console.debug(`[MVT On-Demand Publisher] Cleared tile: ${cacheKey}`);
    }
    }
    }

    5.2 文件系统清理原理

    原理说明:文件系统缓存是持久化的,不会随进程结束而消失。由于分析结果和瓦片文件可能占用大量磁盘空间,需要定期清理过期文件以释放存储资源。

    文件系统缓存特点:

    • 持久性:数据存储在磁盘上,进程重启后仍然存在
    • 大容量:可利用磁盘的大容量特性存储大量数据
    • 慢速访问:磁盘IO延迟远高于内存
    • 需要管理:不会自动过期,需要人工干预或定时任务清理

    清理策略设计:

    • 基于时间的清理:删除超过一定时间未访问的文件
    • 基于大小的清理:当磁盘占用超过阈值时清理
    • 基于类型的清理:不同类型文件设置不同的过期时间
    • 混合策略:结合时间和大小进行清理

    过期时间选择:

    • 分析结果(GeoJSON/GeoTIFF):24小时,分析任务完成后结果相对稳定,但用户通常在短时间内查看
    • 瓦片文件:7天,瓦片复用率高,但也会随着数据更新而失效
    • 临时文件:1小时,临时数据生命周期短

    清理时机:

    • 定时任务:每天凌晨3点(低峰期)执行
    • 按需触发:磁盘空间不足时触发
    • 事件驱动:分析任务完成后触发清理

    /**
    * 定时清理过期结果文件
    *
    * @param maxAgeHours – 文件最大存活时间(小时),默认24小时
    * @returns Promise<void>
    *
    * 设计原理:
    * – **时间判断**:使用文件修改时间(mtime)判断是否过期
    * – **分层遍历**:遍历所有结果类型目录
    * – **递归删除**:支持删除目录及其所有子文件
    * – **幂等操作**:重复执行不会产生错误
    *
    * 清理策略:
    * – **基于修改时间**:mtime反映文件最后修改时间
    * – **递归删除目录**:使用 fs.rmSync 的 recursive 选项
    * – **保留最近使用**:仅删除超过maxAgeHours的文件
    * – **跳过不存在**:目录不存在时跳过,避免错误
    *
    * 执行流程:
    * 1. 计算过期时间戳(当前时间 – maxAgeHours)
    * 2. 遍历所有结果类型目录
    * 3. 检查每个文件/目录的修改时间
    * 4. 删除过期的文件或目录
    * 5. 记录清理日志
    */

    async cleanupExpiredResults(maxAgeHours: number = 24): Promise<void> {
    // 构建结果目录路径
    const resultsDir = path.join(this.workspaceBase, 'results');

    // 目录不存在则直接返回(幂等性保障)
    if (!fs.existsSync(resultsDir)) {
    console.log(`[Cleanup] Results directory not found: ${resultsDir}`);
    return;
    }

    // 计算过期时间戳
    // 当前时间减去最大存活时间,得到过期阈值
    const expireTime = Date.now() – maxAgeHours * 3600 * 1000;
    console.log(`[Cleanup] Starting cleanup – expiring files older than: ${new Date(expireTime).toISOString()}`);

    // 需要清理的目录列表(按类型分类)
    // 每种类型可以设置不同的过期策略(当前统一使用maxAgeHours)
    const dirsToClean = [
    'geojson', // GeoJSON分析结果
    'geotiff', // GeoTIFF栅格结果
    'heatmaps', // 热力图结果
    'reports', // HTML报告
    'mvt' // MVT瓦片
    ];

    let totalFilesRemoved = 0;
    let totalDirsRemoved = 0;

    // 遍历每个类型目录
    for (const dirName of dirsToClean) {
    const fullPath = path.join(resultsDir, dirName);

    // 子目录不存在则跳过
    if (!fs.existsSync(fullPath)) {
    console.debug(`[Cleanup] Skipping non-existent directory: ${fullPath}`);
    continue;
    }

    // 获取目录下所有条目(使用withFileTypes避免二次stat调用)
    const entries = fs.readdirSync(fullPath, { withFileTypes: true });

    // 遍历每个条目
    for (const entry of entries) {
    const entryPath = path.join(fullPath, entry.name);

    try {
    // 获取文件/目录的stat信息
    const stats = fs.statSync(entryPath);

    // 判断是否过期:修改时间早于过期时间戳
    if (stats.mtime.getTime() < expireTime) {
    if (entry.isDirectory()) {
    // 递归删除目录(包含所有子文件和子目录)
    // force: true 忽略不存在的文件错误
    fs.rmSync(entryPath, { recursive: true, force: true });
    console.log(`[Cleanup] Removed expired directory: ${entryPath}`);
    totalDirsRemoved++;
    } else {
    // 删除单个文件
    fs.unlinkSync(entryPath);
    console.log(`[Cleanup] Removed expired file: ${entryPath}`);
    totalFilesRemoved++;
    }
    }
    } catch (error) {
    // 处理可能的错误(权限问题、文件已被删除等)
    console.error(`[Cleanup] Error processing ${entryPath}:`, error);
    }
    }
    }

    console.log(`[Cleanup] Completed. Removed ${totalFilesRemoved} files and ${totalDirsRemoved} directories.`);
    }

    /**
    * 启动定时清理任务
    * @param intervalHours – 清理间隔(小时),默认24小时
    * @param maxAgeHours – 文件最大存活时间(小时),默认24小时
    * @returns NodeJS.Timeout – 定时器ID,用于取消定时任务
    *
    * 使用示例:
    * ```typescript
    * const cleanupTimer = startCleanupScheduler(24, 24);
    * // 取消定时任务
    * // clearTimeout(cleanupTimer);
    * ```
    */

    function startCleanupScheduler(intervalHours: number = 24, maxAgeHours: number = 24): NodeJS.Timeout {
    console.log(`[Cleanup Scheduler] Starting cleanup scheduler. Interval: ${intervalHours}h, Max age: ${maxAgeHours}h`);

    // 立即执行一次清理(启动时)
    cleanupExpiredResults(maxAgeHours);

    // 设置定时任务
    const intervalMs = intervalHours * 3600 * 1000;
    return setInterval(() => {
    console.log(`[Cleanup Scheduler] Running scheduled cleanup…`);
    cleanupExpiredResults(maxAgeHours);
    }, intervalMs);
    }

    5.3 缓存失效策略总结

    ┌─────────────────────────────────────────────────────────────────┐
    │ 缓存失效策略矩阵 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ 缓存类型 失效触发条件 失效方式 │
    │ ───────────────────────────────────────────────────────────── │
    │ │
    │ HTTP客户端 max-age过期 浏览器自动清理 │
    │ 用户手动刷新 CDN自动清理 │
    │ │
    │ Prompt缓存 手动调用clearCache() 内存删除 │
    │ 模板文件修改触发 内存删除 │
    │ │
    │ 瓦片缓存 LRU淘汰(容量满时) 内存删除 │
    │ deleteTileset()调用 内存删除 │
    │ clearAllCache()调用 内存删除 │
    │ │
    │ 文件系统 定时任务清理(默认24小时) 文件删除 │
    │ deleteTileset()调用 文件删除 │
    │ │
    │ 数据库临时表 连接断开 自动清理 │
    │ 手动DROP TABLE 数据库删除 │
    │ 会话结束 自动清理 │
    │ │
    └─────────────────────────────────────────────────────────────────┘

    缓存一致性保证:

    场景数据变更缓存处理一致性保证
    分析结果更新 重新运行分析任务 新结果生成新文件,旧文件保留 文件名唯一,新结果新ID
    瓦片集删除 用户删除瓦片集 删除文件 + 清除内存缓存 立即生效
    模板更新 修改模板文件 触发clearCache() 下次请求重新加载
    数据库更新 修改原始数据 临时表重新创建 连接级隔离

    六、性能优化与最佳实践

    6.1 缓存命中率优化

    原理说明:缓存命中率(Cache Hit Rate)是衡量缓存效果的关键指标,计算公式为:

    命中率 = 命中次数 / (命中次数 + 未命中次数) × 100%

    命中率直接影响系统性能:

    • 命中率90%:10次请求中9次直接从缓存获取
    • 命中率99%:100次请求中99次直接从缓存获取

    缓存命中率提升策略:

  • 缓存预热:系统启动时预先加载热门数据
  • 智能缓存:基于访问模式动态调整缓存策略
  • 多级缓存:利用多层缓存体系提升整体命中率
  • 缓存合并:合并相似请求,减少重复计算
  • 缓存预热原理:

    • 在系统空闲时预先生成热门数据
    • 减少用户首次访问时的等待时间
    • 针对用户经常访问的区域和缩放级别进行预热

    预热策略设计:

    • 启动时预热:系统启动后立即执行
    • 定时预热:周期性预热热门数据
    • 预测预热:基于访问日志预测热门数据

    // 优化:缓存预热

    export class MVTOnDemandPublisher extends BaseMVTPublisher {
    /**
    * 缓存预热:在系统启动时加载常用瓦片
    *
    * @param tilesetIds – 需要预热的瓦片集ID列表
    * @param zoomLevels – 需要预热的缩放级别列表
    * @param bbox – 可选,限定预热的地理范围
    * @returns Promise<void>
    *
    * 设计原理:
    * – **预加载热门数据**:在系统空闲时预先生成热门瓦片
    * – **减少首屏等待**:用户首次访问时直接从缓存获取
    * – **针对性预热**:针对用户经常访问的区域和缩放级别
    * – **后台执行**:不影响正常请求处理
    *
    * 预热策略:
    * 1. **关键缩放级别**:通常预热中间级别(如4-12级)
    * 2. **热门区域**:基于访问日志确定热门区域
    * 3. **增量预热**:只预热新增或更新的瓦片集
    *
    * 性能影响:
    * – 启动时间增加:需要预生成瓦片
    * – 内存占用增加:预热的瓦片会占用内存
    * – 用户体验提升:首次访问响应更快
    */

    async prewarmCache(
    tilesetIds: string[],
    zoomLevels: number[],
    bbox?: [number, number, number, number] // [minLon, minLat, maxLon, maxLat]
    ): Promise<void> {
    console.log(`[MVT On-Demand Publisher] Starting cache prewarming…`);
    console.log(`[MVT On-Demand Publisher] Tilesets: ${tilesetIds.join(', ')}`);
    console.log(`[MVT On-Demand Publisher] Zoom levels: ${zoomLevels.join(', ')}`);
    if (bbox) {
    console.log(`[MVT On-Demand Publisher] Bounding box: ${bbox.join(', ')}`);
    }

    let totalCached = 0;
    let totalSkipped = 0;

    // 遍历每个瓦片集
    for (const tilesetId of tilesetIds) {
    // 获取瓦片集元数据(包含边界范围)
    const metadata = this.tilesetMetadata.get(tilesetId);
    if (!metadata) {
    console.warn(`[MVT On-Demand Publisher] Skipping prewarm for tileset ${tilesetId}: metadata not found`);
    totalSkipped++;
    continue;
    }

    // 遍历每个缩放级别
    for (const z of zoomLevels) {
    try {
    // 生成指定级别的瓦片
    const tiles = await this.generateTilesForZoom(tilesetId, z, bbox);

    // 存入内存缓存
    for (const tile of tiles) {
    const cacheKey = `${tilesetId}:${tile.z}:${tile.x}:${tile.y}`;
    this.tileCache.set(cacheKey, tile.data);
    totalCached++;
    }

    console.debug(`[MVT On-Demand Publisher] Prewarmed ${tiles.length} tiles for ${tilesetId} at zoom ${z}`);
    } catch (error) {
    console.error(`[MVT On-Demand Publisher] Error prewarming tileset ${tilesetId} at zoom ${z}:`, error);
    totalSkipped++;
    }
    }
    }

    console.log(`[MVT On-Demand Publisher] Cache prewarming completed.`);
    console.log(`[MVT On-Demand Publisher] – Cached: ${totalCached} tiles`);
    console.log(`[MVT On-Demand Publisher] – Skipped: ${totalSkipped} tilesets/levels`);
    }

    /**
    * 生成指定缩放级别的瓦片
    * @param tilesetId – 瓦片集ID
    * @param z – 缩放级别
    * @param bbox – 可选,限定地理范围
    * @returns Promise<Array<{z: number, x: number, y: number, data: Buffer}>>
    */

    private async generateTilesForZoom(
    tilesetId: string,
    z: number,
    bbox?: [number, number, number, number]
    ): Promise<Array<{z: number; x: number; y: number; data: Buffer}>> {
    const tiles: Array<{z: number; x: number; y: number; data: Buffer}> = [];

    // 根据瓦片集范围和缩放级别计算需要生成的瓦片坐标
    // 简化实现:实际需要根据空间索引和范围查询
    const metadata = this.tilesetMetadata.get(tilesetId);
    if (!metadata) {
    return tiles;
    }

    // 计算瓦片范围(简化实现)
    const { minX, maxX, minY, maxY } = this.calculateTileRange(metadata.tileInfo.bounds, z);

    // 遍历所有需要生成的瓦片
    for (let x = minX; x <= maxX; x++) {
    for (let y = minY; y <= maxY; y++) {
    // 如果指定了bbox,检查瓦片是否在范围内
    if (bbox && !this.tileIntersectsBbox(z, x, y, bbox)) {
    continue;
    }

    // 生成瓦片数据
    const tileData = await this.generateTile(tilesetId, z, x, y);
    if (tileData) {
    tiles.push({ z, x, y, data: tileData });
    }
    }
    }

    return tiles;
    }

    /**
    * 计算瓦片范围
    * @param bounds – 地理边界 [minLon, minLat, maxLon, maxLat]
    * @param z – 缩放级别
    * @returns {minX: number, maxX: number, minY: number, maxY: number}
    */

    private calculateTileRange(bounds: [number, number, number, number], z: number) {
    const [minLon, minLat, maxLon, maxLat] = bounds;
    return {
    minX: this.lonToTileX(minLon, z),
    maxX: this.lonToTileX(maxLon, z),
    minY: this.latToTileY(maxLat, z),
    maxY: this.latToTileY(minLat, z)
    };
    }

    /**
    * 经度转瓦片X坐标
    */

    private lonToTileX(lon: number, z: number): number {
    return Math.floor((lon + 180) / 360 * Math.pow(2, z));
    }

    /**
    * 纬度转瓦片Y坐标
    */

    private latToTileY(lat: number, z: number): number {
    const rad = lat * Math.PI / 180;
    return Math.floor((1 – Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * Math.pow(2, z));
    }

    /**
    * 检查瓦片是否与bbox相交
    */

    private tileIntersectsBbox(z: number, x: number, y: number, bbox: [number, number, number, number]): boolean {
    // 简化实现:实际需要计算瓦片边界并检查交集
    return true;
    }
    }

    缓存预热策略:

    策略适用场景实现方式
    启动时预热 热门瓦片集 系统启动后立即执行
    定时预热 周期性热门数据 定时任务触发
    预测预热 基于访问模式 分析访问日志预测

    6.2 缓存键设计最佳实践

    // 缓存键设计原则

    // ❌ 不好:过于简单,容易冲突
    const badKey = `${z}–${x}–${y}`;
    // 问题:不同瓦片集的相同坐标会冲突

    // ✅ 好:包含唯一标识符
    const goodKey = `${tilesetId}:${z}:${x}:${y}`;
    // 优势:tilesetId保证唯一性

    // ✅ 更好:包含版本信息
    const betterKey = `${tilesetId}:v1:${z}:${x}:${y}`;
    // 优势:支持版本管理,发布新版本时自动失效旧缓存

    // ✅ 最佳:包含校验和
    const bestKey = `${tilesetId}:${dataHash}:${z}:${x}:${y}`;
    // 优势:数据变化时自动生成新键,无需手动清理缓存

    缓存键设计原则:

  • 唯一性:必须包含足够的唯一标识信息
  • 可读性:使用分隔符(如:)提高可读性
  • 版本化:支持数据版本管理
  • 可扩展性:预留扩展空间
  • 6.3 多级缓存协作流程

    ┌─────────────────────────────────────────────────────────────────┐
    │ 多级缓存协作流程 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ Client Request │
    │ │ │
    │ ▼ │
    │ ┌─────────────┐ │
    │ │ HTTP缓存 │ ← 检查浏览器缓存(最快,<1ms) │
    │ └─────┬───────┘ │
    │ │ Miss │
    │ ▼ │
    │ ┌─────────────┐ │
    │ │ 内存缓存 │ ← 检查服务端内存(快,1-10ms) │
    │ └─────┬───────┘ │
    │ │ Miss │
    │ ▼ │
    │ ┌─────────────┐ │
    │ │ 文件系统 │ ← 检查磁盘文件(中等,10-100ms) │
    │ └─────┬───────┘ │
    │ │ Miss │
    │ ▼ │
    │ ┌─────────────┐ │
    │ │ 数据库 │ ← 查询PostGIS(慢,100-1000ms) │
    │ └─────┬───────┘ │
    │ │ │
    │ └───────► 生成结果 │
    │ │ │
    │ ┌────────┼────────┐ │
    │ ▼ ▼ ▼ │
    │ 数据库 文件系统 内存缓存 HTTP响应 │
    │ 更新 更新 更新 │
    │ │
    └─────────────────────────────────────────────────────────────────┘

    多级缓存性能对比:

    层级延迟命中率目标容量
    HTTP缓存 <1ms 99% 浏览器决定
    内存缓存 1-10ms 95% 10000瓦片
    文件系统 10-100ms 90% 磁盘容量
    数据库 100-1000ms – 数据库限制

    七、代码优化建议

    7.1 增强LRU缓存实现

    原理说明:当前实现使用简单的FIFO(先进先出)淘汰策略,但FIFO无法区分数据的访问频率。真正的LRU(Least Recently Used,最近最少使用)策略能够更好地保留热门数据,提高缓存命中率。

    FIFO vs LRU对比:

    策略淘汰对象优点缺点适用场景
    FIFO 最早插入的 实现简单,性能好 不考虑访问频率 数据访问均匀
    LRU 最久未使用的 保留热门数据,命中率高 实现稍复杂 数据访问不均匀

    LRU原理:

    • 核心思想:优先保留最近访问的数据
    • 当缓存满时,淘汰最久未被访问的条目
    • 基于"最近使用的数据未来被访问的概率更高"的假设

    实现方式:

    • 方式一:使用Map保持插入顺序(ES6特性)
    • 方式二:使用双向链表+哈希表(经典实现)
    • 方式三:使用OrderedDict(Python)

    本实现选择:使用ES6 Map的插入顺序特性,实现简洁且性能良好。

    // 当前实现:简单FIFO淘汰(存在局限性)

    // 优化:真正的LRU实现(推荐)

    /**
    * LRU缓存实现
    *
    * LRU = Least Recently Used(最近最少使用)
    *
    * 设计原理:
    * – **核心思想**:当缓存满时,删除最久未使用的条目
    * – **数据结构**:利用ES6 Map的有序特性(保持插入顺序)
    * – **访问标记**:访问时将条目删除并重新插入,移到末尾
    * – **淘汰策略**:删除Map的第一个元素(最久未使用)
    *
    * 复杂度分析:
    * – 插入:O(1)
    * – 查询:O(1)
    * – 淘汰:O(1)
    *
    * 适用场景:
    * – 数据访问具有时间局部性
    * – 热门数据需要优先保留
    * – 缓存容量有限,需要高效利用
    */

    class LRUCache implements TileCache {
    private cache: Map<string, Buffer> = new Map(); // 缓存容器,保持插入顺序
    private maxSize: number; // 缓存容量上限

    /**
    * 构造函数
    * @param maxSize – 缓存最大容量(默认10000个瓦片)
    */

    constructor(maxSize: number = 10000) {
    this.maxSize = maxSize;
    console.log(`[LRUCache] Initialized with max size: ${maxSize}`);
    }

    /**
    * 获取缓存数据
    *
    * @param key – 缓存键
    * @returns Buffer | null
    *
    * LRU核心逻辑:
    * 1. 查找数据
    * 2. 如果命中,删除并重新插入(移到末尾,标记为最近使用)
    * 3. 返回数据或null
    *
    * 为什么要删除再插入?
    * – ES6 Map保持插入顺序
    * – 删除再插入会将条目移到Map的末尾
    * – 末尾的条目是最近使用的
    * – 开头的条目是最久未使用的
    */

    get(key: string): Buffer | null {
    const value = this.cache.get(key);

    if (value) {
    // LRU关键操作:删除并重新插入,标记为最近使用
    this.cache.delete(key);
    this.cache.set(key, value);
    console.debug(`[LRUCache] Hit: ${key}`);
    } else {
    console.debug(`[LRUCache] Miss: ${key}`);
    }

    return value || null;
    }

    /**
    * 设置缓存数据
    *
    * @param key – 缓存键
    * @param data – 瓦片数据(Buffer格式)
    *
    * 执行流程:
    * 1. 如果已存在,先删除(保证重新插入后在末尾)
    * 2. 检查容量,超过上限则淘汰最久未使用的
    * 3. 插入新数据(自动放在末尾)
    */

    set(key: string, data: Buffer): void {
    // 如果已存在,先删除(保证重新插入后位置正确)
    if (this.cache.has(key)) {
    this.cache.delete(key);
    }

    // 容量检查:超过上限时执行LRU淘汰
    if (this.cache.size >= this.maxSize) {
    // 获取第一个键(最久未使用的)
    const firstKey = this.cache.keys().next().value;
    if (firstKey) {
    console.debug(`[LRUCache] Evicting LRU: ${firstKey}`);
    this.cache.delete(firstKey);
    }
    }

    // 插入到末尾(标记为最近使用)
    this.cache.set(key, data);
    console.debug(`[LRUCache] Added: ${key}, size: ${data.length} bytes`);
    }

    /**
    * 检查缓存是否存在
    * @param key – 缓存键
    * @returns boolean – 是否存在
    *
    * 注意:此方法不更新访问时间,仅用于存在性检查
    */

    has(key: string): boolean {
    return this.cache.has(key);
    }

    /**
    * 清空所有缓存
    */

    clear(): void {
    console.log(`[LRUCache] Cleared ${this.cache.size} entries`);
    this.cache.clear();
    }

    /**
    * 获取缓存统计信息
    * @returns {size: number, maxSize: number}
    */

    getStats(): { size: number; maxSize: number } {
    return {
    size: this.cache.size,
    maxSize: this.maxSize
    };
    }
    }

    7.2 添加缓存监控

    原理说明:监控缓存命中率是评估缓存效果的关键手段。通过实时监控命中率、命中次数、未命中次数等指标,可以:

  • 评估缓存策略的有效性
  • 及时发现缓存问题(如命中率过低)
  • 指导缓存容量调整
  • 支持性能优化决策
  • 监控指标设计:

    • 命中率(Hit Rate):命中次数 / 总请求次数
    • 命中次数(Hits):缓存命中的总次数
    • 未命中次数(Misses):缓存未命中的总次数
    • 缓存大小(Size):当前缓存条目数
    • 淘汰次数(Evictions):缓存淘汰的总次数

    监控用途:

    • 告警触发:命中率低于阈值时告警
    • 容量调整:根据缓存使用情况调整容量
    • 策略优化:根据访问模式优化缓存策略
    • 容量规划:预测内存需求

    设计模式:使用装饰器模式(Decorator Pattern)包装原始缓存,在不修改原有实现的情况下添加监控功能。

    // 缓存监控增强(装饰器模式)

    /**
    * 带监控的瓦片缓存包装器
    *
    * 设计原理:
    * – 使用装饰器模式包装原始缓存
    * – 统计命中/未命中次数
    * – 计算命中率
    */

    class MonitoredTileCache implements TileCache {
    private cache: TileCache; // 内部缓存实例
    private hits: number = 0; // 命中次数
    private misses: number = 0; // 未命中次数

    constructor(innerCache: TileCache) {
    this.cache = innerCache;
    console.log('[MonitoredTileCache] Initialized');
    }

    /**
    * 获取缓存数据
    * @param key – 缓存键
    * @returns Buffer | null
    */

    get(key: string): Buffer | null {
    const result = this.cache.get(key);

    // 统计命中/未命中
    if (result) {
    this.hits++;
    } else {
    this.misses++;
    }

    return result;
    }

    set(key: string, data: Buffer): void {
    this.cache.set(key, data);
    }

    has(key: string): boolean {
    return this.cache.has(key);
    }

    clear(): void {
    this.cache.clear();
    // 重置统计
    this.hits = 0;
    this.misses = 0;
    console.log('[MonitoredTileCache] Cache cleared, stats reset');
    }

    /**
    * 获取命中率
    * @returns number – 命中率(0-1)
    */

    getHitRate(): number {
    const total = this.hits + this.misses;
    return total > 0 ? this.hits / total : 0;
    }

    /**
    * 获取统计信息
    * @returns {hits: number, misses: number, hitRate: number}
    */

    getStats(): { hits: number; misses: number; hitRate: number } {
    return {
    hits: this.hits,
    misses: this.misses,
    hitRate: this.getHitRate()
    };
    }

    /**
    * 重置统计
    */

    resetStats(): void {
    this.hits = 0;
    this.misses = 0;
    console.log('[MonitoredTileCache] Stats reset');
    }
    }

    // 使用示例
    const innerCache = new LRUCache(10000);
    const monitoredCache = new MonitoredTileCache(innerCache);

    // 定期输出统计
    setInterval(() => {
    const stats = monitoredCache.getStats();
    console.log(`[Cache Stats] Hits: ${stats.hits}, Misses: ${stats.misses}, Hit Rate: ${(stats.hitRate * 100).toFixed(2)}%`);
    }, 60000); // 每分钟输出一次

    7.3 分布式缓存支持

    原理说明:在单实例部署场景下,内存缓存可以满足需求。但在多实例部署(如负载均衡、容器集群)场景下,每个实例都有独立的内存缓存,无法共享数据,导致:

  • 缓存命中率降低(每个实例都需要独立预热)
  • 内存资源浪费(相同数据存储在多个实例中)
  • 数据不一致(不同实例可能返回不同版本的数据)
  • 分布式缓存解决方案:使用Redis等分布式缓存系统,实现多实例共享缓存。

    分布式缓存优势:

    • 缓存共享:所有实例共享同一个缓存
    • 内存节省:避免重复存储
    • 数据一致:所有实例访问相同的数据
    • 水平扩展:缓存容量可独立扩展

    架构设计:

    ┌─────────────────────────────────────────────────────────────┐
    │ 分布式缓存架构 │
    ├─────────────────────────────────────────────────────────────┤
    │ │
    │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
    │ │ 实例1 │ │ 实例2 │ │ 实例N │ │
    │ │ (Node.js)│ │ (Node.js)│ │ (Node.js)│ │
    │ └────┬────┘ └────┬────┘ └────┬────┘ │
    │ │ │ │ │
    │ └──────────────┼──────────────┘ │
    │ ▼ │
    │ ┌─────────────────┐ │
    │ │ Redis集群 │ ← 分布式缓存后端 │
    │ │ (共享缓存) │ │
    │ └─────────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────┘

    数据序列化方案:

    • Base64编码:将Buffer转换为字符串存储
    • MessagePack:更高效的二进制序列化(推荐)
    • Redis Binary:直接存储二进制数据(Redis 6+支持)

    // Redis缓存适配器

    /**
    * Redis瓦片缓存实现
    *
    * 设计原理:
    * – **分布式存储**:使用Redis作为分布式缓存后端
    * – **多实例共享**:所有应用实例共享同一个缓存
    * – **自动过期**:设置TTL自动清理过期数据
    * – **键前缀隔离**:使用前缀避免不同应用的键冲突
    *
    * 适用场景:
    * – 多实例部署(负载均衡)
    * – 容器化部署(Kubernetes)
    * – 需要缓存共享的场景
    *
    * 性能考虑:
    * – 网络延迟:Redis访问存在网络开销(需部署在同一局域网)
    * – 序列化开销:Buffer与字符串之间的转换
    * – 集群模式:支持Redis Cluster实现高可用和水平扩展
    */

    class RedisTileCache implements TileCache {
    private client: RedisClientType; // Redis客户端
    private prefix: string; // 键前缀,避免冲突

    /**
    * 构造函数
    * @param client – Redis客户端实例
    * @param prefix – 键前缀(默认'mvt:')
    */

    constructor(client: RedisClientType, prefix: string = 'mvt:') {
    this.client = client;
    this.prefix = prefix;
    console.log(`[RedisTileCache] Initialized with prefix: ${prefix}`);
    }

    /**
    * 获取缓存数据
    * @param key – 缓存键
    * @returns Promise<Buffer | null>
    */

    async get(key: string): Promise<Buffer | null> {
    const fullKey = `${this.prefix}${key}`;
    const value = await this.client.get(fullKey);

    if (value) {
    // Redis存储的是Base64编码,需要解码
    return Buffer.from(value, 'base64');
    }

    return null;
    }

    /**
    * 设置缓存数据
    * @param key – 缓存键
    * @param data – 瓦片数据(Buffer)
    */

    async set(key: string, data: Buffer): Promise<void> {
    const fullKey = `${this.prefix}${key}`;
    // 将Buffer转换为Base64字符串存储
    // 设置过期时间为24小时(86400秒)
    await this.client.setEx(fullKey, 86400, data.toString('base64'));
    }

    /**
    * 检查缓存是否存在
    * @param key – 缓存键
    * @returns Promise<boolean>
    */

    async has(key: string): Promise<boolean> {
    const fullKey = `${this.prefix}${key}`;
    const exists = await this.client.exists(fullKey);
    return exists === 1;
    }

    /**
    * 清空所有缓存
    */

    async clear(): Promise<void> {
    // 获取所有匹配前缀的键
    const keys = await this.client.keys(`${this.prefix}*`);
    if (keys.length > 0) {
    await this.client.del(keys);
    console.log(`[RedisTileCache] Cleared ${keys.length} keys`);
    }
    }
    }


    八、缓存策略总结

    8.1 核心设计原则

    ┌─────────────────────────────────────────────────────────────────┐
    │ 缓存设计核心原则 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ 1. 多级缓存:HTTP → 内存 → 文件 → 数据库 │
    │ – 每一层都有明确的职责和生命周期 │
    │ – 从快到慢,从易失到持久 │
    │ │
    │ 2. 缓存键设计:包含唯一标识符 │
    │ – 避免冲突,支持版本管理 │
    │ – 格式:${唯一标识}:${其他信息} │
    │ │
    │ 3. 主动失效:删除资源时同步清除缓存 │
    │ – 保证数据一致性 │
    │ – 避免脏数据返回 │
    │ │
    │ 4. 容量限制:内存缓存设置上限 │
    │ – 防止内存溢出 │
    │ – 使用LRU/FIFO策略淘汰 │
    │ │
    │ 5. 持久化备份:内存缓存同步到文件系统 │
    │ – 保证服务重启后缓存不丢失 │
    │ – 支持跨进程共享 │
    │ │
    └─────────────────────────────────────────────────────────────────┘

    8.2 性能提升效果

    指标无缓存有缓存提升倍数
    Prompt加载 100ms/file 1ms/hit 100x
    瓦片请求 500ms/tile 1ms/hit 500x
    结果下载 100ms/file 1ms/hit 100x

    8.3 未来优化方向

  • 分布式缓存:引入Redis支持多实例共享缓存
  • 智能预热:基于访问模式预测预缓存热门瓦片
  • 缓存分层:根据访问频率动态调整缓存层级
  • 监控告警:添加缓存命中率监控和告警
  • 自适应缓存大小:根据内存使用动态调整缓存容量

  • 结语

    GeoAI-UP的缓存体系体现了以下核心思想:

  • 分层设计:从HTTP到数据库,每层缓存各司其职,形成完整的缓存体系
  • 主动失效:数据变更时立即清除相关缓存,保证数据一致性
  • 持久化备份:内存缓存同步到文件系统,支持服务重启恢复
  • 容量管理:通过LRU淘汰策略控制内存占用,避免溢出
  • 这套缓存机制在保证系统性能的同时,也兼顾了数据一致性和资源管理,是空间分析系统的典型缓存设计模式。通过合理的缓存策略,GeoAI-UP能够高效处理大量空间数据查询,为用户提供快速响应的分析体验。


    参考代码文件: 代码在gitee geo-ai-universal-platform 核心文件:

    • PromptManager.ts
    • MVTOnDemandPublisher.ts
    • BaseMVTPublisher.ts
    • ResultController.ts

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » 结果缓存机制:GeoAI-UP的执行结果缓存与失效策略
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址