在空间分析系统中,缓存是提升性能的关键手段。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. 向上写入:数据生成后,逐层向上缓存
  3. 智能失效:数据变更时,逐层清除相关缓存

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 };
  }
}

设计要点深度解析

  1. 缓存键设计原理

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

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

    • 模板文件修改时(通过文件监听触发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         # 瓦片文件
│       └── ...

目录结构设计原理

  1. 按类型分离:不同类型的结果存储在不同目录,便于管理和清理
  2. 唯一ID命名:使用UUID或时间戳生成唯一ID,避免文件名冲突
  3. 层级化瓦片存储{z}/{x}/{y}.pbf 结构符合OGC标准,便于CDN缓存
  4. 元数据独立: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的失效策略设计

  1. 主动失效为主:删除瓦片集时同步清除相关缓存
  2. 时间过期为辅:文件系统缓存设置24小时过期
  3. 容量淘汰兜底:内存缓存满时自动淘汰
// 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次直接从缓存获取

缓存命中率提升策略

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

缓存预热原理

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

预热策略设计

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

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}`;
// 优势:数据变化时自动生成新键,无需手动清理缓存

缓存键设计原则

  1. 唯一性:必须包含足够的唯一标识信息
  2. 可读性:使用分隔符(如:)提高可读性
  3. 版本化:支持数据版本管理
  4. 可扩展性:预留扩展空间

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 添加缓存监控

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

  1. 评估缓存策略的有效性
  2. 及时发现缓存问题(如命中率过低)
  3. 指导缓存容量调整
  4. 支持性能优化决策

监控指标设计

  • 命中率(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 分布式缓存支持

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

  1. 缓存命中率降低(每个实例都需要独立预热)
  2. 内存资源浪费(相同数据存储在多个实例中)
  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 未来优化方向

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

结语

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

  1. 分层设计:从HTTP到数据库,每层缓存各司其职,形成完整的缓存体系
  2. 主动失效:数据变更时立即清除相关缓存,保证数据一致性
  3. 持久化备份:内存缓存同步到文件系统,支持服务重启恢复
  4. 容量管理:通过LRU淘汰策略控制内存占用,避免溢出

这套缓存机制在保证系统性能的同时,也兼顾了数据一致性和资源管理,是空间分析系统的典型缓存设计模式。通过合理的缓存策略,GeoAI-UP能够高效处理大量空间数据查询,为用户提供快速响应的分析体验。


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

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐