6月5日 21:43

TypeORM 查询缓存实战:Redis 配置、主动失效和策略选择

数据库查询是后端应用最常见的性能瓶颈。TypeORM 内置了查询缓存,支持内存缓存和 Redis 缓存两种存储后端,能在不改动业务代码的情况下大幅降低数据库负载。这篇讲清楚怎么配、怎么用、以及缓存策略的选择。

两种缓存存储:数据库表 vs Redis

默认方案:数据库表缓存

不配置任何东西,TypeORM 默认用数据库的一张表存缓存:

typescript
const dataSource = new DataSource({ type: 'mysql', host: 'localhost', username: 'root', password: 'password', database: 'myapp', cache: true, // 开启缓存,默认 1000ms 过期 })

TypeORM 会自动创建 query-result-cache 表,把查询 SQL 和结果序列化后存进去。下次同样的查询直接从这张表取,不执行 SQL。

问题:缓存本身也存数据库里——等于用数据库查数据库,只是从业务表换到了缓存表。单实例够用,分布式部署时每个实例有自己的缓存表,互相看不到。

推荐方案:Redis 缓存

typescript
const dataSource = new DataSource({ type: 'mysql', cache: { type: 'redis', options: { host: 'localhost', port: 6379, password: 'redis-password', db: 0, }, duration: 30000, // 默认缓存 30 秒 }, })

Redis 的优势:

  • :内存读取,微秒级延迟
  • 共享:多个应用实例访问同一个 Redis,缓存一致
  • 可控:Redis 的内存管理、过期策略、持久化都很成熟

Redis 集群场景用 ioredis:

typescript
cache: { type: 'ioredis/cluster', options: { startupNodes: [ { host: '10.0.0.1', port: 7000 }, { host: '10.0.0.2', port: 7000 }, { host: '10.0.0.3', port: 7000 }, ], }, }

查询级缓存:精确控制哪些查询缓存

全局开缓存后,不是所有查询都会缓存——需要显式指定。

Repository 方式

typescript
// 缓存 30 秒 const users = await userRepository.find({ cache: 30000, }) // 给缓存一个 ID,方便后续清除 const users = await userRepository.find({ cache: { id: 'users_list', milliseconds: 30000, }, }) // findAndCount 也支持 const [users, count] = await userRepository.findAndCount({ cache: { id: 'users_paginated', milliseconds: 30000, }, })

QueryBuilder 方式

typescript
const posts = await dataSource .createQueryBuilder(Post, 'post') .where('post.isPublished = :published', { published: true }) .cache('published_posts', 60000) // 缓存 ID + 过期时间 .getMany()

缓存 ID 的作用

缓存 ID 是手动控制缓存的关键——通过 ID 可以精确清除某类查询的缓存:

typescript
// 清除指定 ID 的缓存 await dataSource.queryResultCache.remove(['users_list']) // 数据变更后清除相关缓存 async createUser(dto: CreateUserDto) { const user = await this.userRepo.save(dto) // 用户列表缓存失效 await this.dataSource.queryResultCache.remove(['users_list', 'users_paginated']) return user }

原则:所有需要缓存的查询都应该指定 ID,否则你无法在数据变更时精确失效缓存——只能等过期。

缓存策略选择

按数据变化频率决定缓存时长

数据特征缓存时长例子
几乎不变5-30 分钟省份列表、配置项
偶尔变化30-60 秒文章列表、商品分类
频繁变化5-15 秒实时排行榜、库存
实时性要求高不缓存支付状态、账户余额

主动失效 vs 被动过期

  • 被动过期:设一个 duration,到期自动清除。简单,但数据变更后到过期前这段时间,用户看到的可能是旧数据
  • 主动失效:数据变更时手动 remove() 缓存 ID。更精确,但代码更复杂

生产环境推荐两者结合:设一个较长的 duration 做兜底,数据变更时主动失效。这样即使忘了失效,缓存最多存在 duration 时间也不会永远不过期。

typescript
// 查询时设较长缓存 const users = await this.userRepo.find({ cache: { id: 'users_list', milliseconds: 300000 }, // 5 分钟兜底 }) // 变更时主动失效 async updateUser(id: number, dto: UpdateUserDto) { await this.userRepo.update(id, dto) await this.dataSource.queryResultCache.remove(['users_list']) }

缓存与事务

TypeORM 的查询缓存在事务内不会自动失效。这可能导致一个问题:

typescript
// 事务外查询 → 走缓存 const user = await userRepo.findOne({ where: { id: 1 }, cache: 30000 }) // 事务内更新 await dataSource.transaction(async (manager) => { await manager.update(User, 1, { name: 'new name' }) // 此时缓存里还是旧数据! }) // 事务提交后再查 → 可能还是缓存旧数据

解决方案:事务提交后手动清除缓存:

typescript
await dataSource.transaction(async (manager) => { await manager.update(User, 1, { name: 'new name' }) }) await dataSource.queryResultCache.remove(['user_1'])

缓存清理

清除指定缓存

typescript
// 按 ID 清除 await dataSource.queryResultCache.remove(['users_list', 'posts_list']) // 清除所有缓存 await dataSource.queryResultCache.clear()

命令行清除

bash
npx typeorm cache:clear

自动清理

Redis 的 TTL 机制会自动清理过期缓存,不需要手动管理。数据库表缓存 TypeORM 也会定期清理过期记录。

ignoreErrors:缓存降级

缓存不可用时不应该让业务请求也失败:

typescript
cache: { type: 'redis', options: { host: 'localhost', port: 6379 }, ignoreErrors: true, // Redis 挂了不报错,直接查数据库 }

ignoreErrors: true 是生产环境必须加的——Redis 重启或网络抖动时,查询会降级到直接访问数据库,而不是直接 500。

完整配置示例

typescript
// data-source.ts import { DataSource } from 'typeorm' export const dataSource = new DataSource({ type: 'mysql', host: process.env.DB_HOST, port: 3306, username: process.env.DB_USER, password: process.env.DB_PASS, database: 'myapp', cache: { type: 'redis', options: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), password: process.env.REDIS_PASSWORD, db: 0, }, duration: 30000, // 默认 30 秒 ignoreErrors: true, // 缓存故障降级 tableName: 'query_result_cache', // 数据库表缓存的表名(如果用数据库缓存) }, })

缓存策略速查

场景策略
配置数据、字典表长缓存(5 分钟+)+ 变更时手动失效
列表查询中缓存(30-60 秒)+ 分页参数加入缓存 ID
详情页短缓存(15-30 秒)+ 更新时失效
实时数据不缓存,或 5 秒极短缓存
多实例部署必须用 Redis,数据库表缓存不共享
缓存故障容忍ignoreErrors: true
事务内更新事务提交后手动清除缓存
标签:TypeORM