6月5日 21:43
TypeORM 查询缓存实战:Redis 配置、主动失效和策略选择
数据库查询是后端应用最常见的性能瓶颈。TypeORM 内置了查询缓存,支持内存缓存和 Redis 缓存两种存储后端,能在不改动业务代码的情况下大幅降低数据库负载。这篇讲清楚怎么配、怎么用、以及缓存策略的选择。
两种缓存存储:数据库表 vs Redis
默认方案:数据库表缓存
不配置任何东西,TypeORM 默认用数据库的一张表存缓存:
typescriptconst dataSource = new DataSource({ type: 'mysql', host: 'localhost', username: 'root', password: 'password', database: 'myapp', cache: true, // 开启缓存,默认 1000ms 过期 })
TypeORM 会自动创建 query-result-cache 表,把查询 SQL 和结果序列化后存进去。下次同样的查询直接从这张表取,不执行 SQL。
问题:缓存本身也存数据库里——等于用数据库查数据库,只是从业务表换到了缓存表。单实例够用,分布式部署时每个实例有自己的缓存表,互相看不到。
推荐方案:Redis 缓存
typescriptconst 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:
typescriptcache: { 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 方式
typescriptconst 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' }) // 此时缓存里还是旧数据! }) // 事务提交后再查 → 可能还是缓存旧数据
解决方案:事务提交后手动清除缓存:
typescriptawait 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()
命令行清除
bashnpx typeorm cache:clear
自动清理
Redis 的 TTL 机制会自动清理过期缓存,不需要手动管理。数据库表缓存 TypeORM 也会定期清理过期记录。
ignoreErrors:缓存降级
缓存不可用时不应该让业务请求也失败:
typescriptcache: { 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 |
| 事务内更新 | 事务提交后手动清除缓存 |