18.04-Redis缓存
要点
- Cloudflare Worker 环境没有原生 Redis——需要连接外部服务(Upstash、Redis Cloud 等)
- Redis 常用缓存模式有四种:Cache-Aside、Read-Through、Write-Through、Write-Behind
- Upstash 提供 REST API,可以直接
fetch调用,不需要 TCP 长连接——这是 Worker 友好型 Redis - Cloudflare KV 已经能覆盖 80% 的 Redis 缓存场景,引入 Redis 要有明确理由
- 引入 Redis 的典型理由:复杂数据结构(Sorted Set、Pub/Sub)、强一致读写、已有 Redis 基建
- 选型建议:能不用就不用——KV 更便宜、更快、更简单
1. 为什么 Worker 项目要考虑 Redis
前面讲了 Worker 环境的三层缓存:内存、Cache API、KV。KV 已经能覆盖绝大多数业务缓存需求——读延迟低、全球分布、按量付费。
那为什么还要谈 Redis?
几个场景:
- 已有 Redis 基建。团队已经在用 Redis 做 session、队列、限流,新加的 Worker 服务需要跟现有系统共享缓存
- 复杂数据结构。Redis 的 Sorted Set、Hash、Pub/Sub、Stream 等数据结构 KV 没有
- 强一致读写。KV 是最终一致——写完立刻读可能读不到。Redis 是单线程模型,写完立刻读一定能读到
- Atomic 操作。Redis 的
INCR、SETNX、Lua 脚本在分布式环境下是原子的——KV 做不到
但先说结论:Worker 项目里 80% 的场景用 KV 就够了。Redis 是补充,不是替代。
2. Worker 环境的 Redis 挑战
传统 Node.js 用 ioredis 或 node-redis 连接 Redis——依赖 TCP 长连接。但 Worker 有几个限制。
2.1 没有 TCP Socket
Worker 没有 net.Socket——不能直接建立 TCP 连接。传统的 Redis 客户端跑不了。
2.2 没有持久连接
Worker 每次请求可能落到不同 Isolate,无法维持 Redis 连接池。每次请求都重新建连?延迟会很高。
2.3 解决方案:REST API
Upstash 和 Redis Cloud 都提供 REST API——用 HTTP fetch 代替 TCP 连接。Worker 天生擅长 HTTP,所以这条路完全可行。
// Upstash REST 调用示例
const response = await fetch('https://<your-upstash-url>/SET/key/value', {
headers: {
Authorization: 'Bearer <your-token>',
},
})Upstash 专门为 Serverless 环境设计——无状态、按请求计费、冷启动友好。
3. Upstash 接入实战
3.1 注册与配置
Upstash 注册后拿到 endpoint 和 token,加到 wrangler.jsonc:
{
"vars": {
"UPSTASH_REDIS_REST_URL": "https://your-region.upstash.io",
"UPSTASH_REDIS_REST_TOKEN": "your-token"
}
}3.2 用 @upstash/redis 库
Upstash 提供官方库,已经处理好 REST 调用和序列化:
pnpm add @upstash/redis// src/lib/redis.ts
import { Redis } from '@upstash/redis/cloudflare'
type Bindings = {
UPSTASH_REDIS_REST_URL: string
UPSTASH_REDIS_REST_TOKEN: string
}
export function getRedis(env: Bindings) {
return Redis.fromEnv(env)
}用法跟普通 Redis 客户端几乎一样:
// src/routes/cache.ts
import { getRedis } from '../lib/redis'
app.get('/api/data/:key', async (c) => {
const redis = getRedis(c.env)
const key = c.req.param('key')
// 先查 Redis
const cached = await redis.get<string>(key)
if (cached) {
return c.json({ source: 'cache', data: cached })
}
// miss,真跑业务
const data = await loadFromDatabase(key)
// 写入 Redis,TTL 1 小时
await redis.set(key, data, { ex: 3600 })
return c.json({ source: 'db', data })
})@upstash/redis/cloudflare 这个入口专门优化了 Worker 环境——不走 TCP,走 REST。
3.3 性能考量
Upstash REST API 的延迟比 TCP 高:
- TCP 直连 Redis:1-2ms(同地域)
- Upstash REST:10-30ms(包含 HTTP 开销)
但跟 KV(5-10ms)比,没有数量级差距。如果已经在用 Upstash 做其他服务,这个延迟可以接受。
如果延迟敏感,可以用 Upstash 的 Pipeline 批量发送多条命令,减少 round trip。
4. Redis 缓存模式
Redis 缓存有四种经典模式,选哪个看业务需求。
4.1 Cache-Aside(旁路缓存)
最常用的模式。应用自己负责读写缓存:
读:查缓存 → miss → 查 DB → 写缓存 → 返回
写:更新 DB → 删缓存(不更新)
// 读
async function getUser(id: string, redis: Redis, db: Database) {
const cached = await redis.get(`user:${id}`)
if (cached) return JSON.parse(cached)
const user = await db.getUser(id)
await redis.set(`user:${id}`, JSON.stringify(user), { ex: 3600 })
return user
}
// 写
async function updateUser(id: string, data: User, redis: Redis, db: Database) {
await db.updateUser(id, data)
await redis.del(`user:${id}`) // 删,不更新
}为什么删而不更新?因为「更新缓存」需要跟 DB 保持事务一致性,很复杂。「删缓存」更简单——下次读的时候自然会重建。
4.2 Read-Through(读穿透)
缓存层代理读操作。应用只跟缓存打交道:
class ReadThroughCache {
constructor(private redis: Redis, private loader: (key: string) => Promise<any>) {}
async get(key: string) {
const cached = await this.redis.get(key)
if (cached) return JSON.parse(cached)
const value = await this.loader(key)
await this.redis.set(key, JSON.stringify(value), { ex: 3600 })
return value
}
}
// 使用
const cache = new ReadThroughCache(redis, (key) => db.getUser(key))
const user = await cache.get('user:123')跟 Cache-Aside 比,逻辑封装在缓存层里,应用代码更干净。
4.3 Write-Through(写穿透)
缓存层代理写操作。写缓存的同时也写 DB:
class WriteThroughCache extends ReadThroughCache {
async set(key: string, value: any) {
await this.db.save(key, value)
await this.redis.set(key, JSON.stringify(value), { ex: 3600 })
}
}保证缓存和 DB 一致,但每次写都要两次 IO,延迟高。
4.4 Write-Behind / Write-Back(异步写)
写操作只写缓存,异步批量同步到 DB:
class WriteBehindCache {
private buffer: Map<string, any> = new Map()
async set(key: string, value: any) {
await this.redis.set(key, JSON.stringify(value), { ex: 3600 })
this.buffer.set(key, value)
// 满了就批量写 DB
if (this.buffer.size >= 100) {
await this.flush()
}
}
async flush() {
const entries = Array.from(this.buffer.entries())
await this.db.batchSave(entries)
this.buffer.clear()
}
}写延迟最低,但有丢数据风险——缓存挂了、Worker 崩了,buffer 里的数据就丢了。只适合能容忍少量丢失的场景(日志、计数)。
5. Redis vs KV:怎么选
| 维度 | Upstash Redis | Cloudflare KV |
|---|---|---|
| 读延迟 | 10-30ms | 5-10ms |
| 写延迟 | 10-30ms | 5-10ms |
| 一致性 | 强一致 | 最终一致 |
| 数据结构 | String、Hash、Set、Sorted Set、Stream、Pub/Sub | 只有 String |
| 原子操作 | INCR、SETNX、Lua 脚本 | 无 |
| 全球分布 | 单区域(要选区域) | 全球自动分布 |
| 成本 | 按请求计费(较贵) | 按读/写计费(便宜) |
| 连接方式 | REST API | 原生绑定 |
| 适用 | 复杂数据结构、强一致、已有基建 | 简单 KV、全球分布、低成本 |
选择建议:
- 新系统、无 Redis 基建:用 KV,不要引入 Redis
- 已有 Upstash 或 Redis Cloud:Worker 里直接用 REST 客户端接入
- 需要 Sorted Set、Pub/Sub、Stream:必须 Redis
- 需要强一致读写:Redis 或 D1(KV 是最终一致)
- 全球分布需求:KV(Redis 单区域)
6. 实战:用 Redis 做分布式限流
Redis 的 INCR 和原子操作是做限流的最佳选择(后面几篇会详细讲)。这里先看下跟 KV 的对比:
// Redis 限流(原子操作)
async function rateLimitRedis(redis: Redis, userId: string) {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, 60) // 第一次设置 1 分钟过期
}
return count <= 100 // 每分钟 100 次
}
// KV 限流(需要事务或 CAS)
async function rateLimitKV(kv: KVNamespace, userId: string) {
const key = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`
const current = parseInt(await kv.get(key) || '0')
if (current >= 100) return false
await kv.put(key, String(current + 1)) // 不是原子的!并发会超
return true
}KV 的 get + put 不是原子的——高并发下会超出限制。Redis 的 INCR 是原子的,不会有这个问题。
这就是「必须用 Redis」的典型场景之一。
7. 缓存雪崩、穿透、击穿
这三个问题 Redis 和 KV 都会碰到,但 Redis 的应对工具更多。
7.1 缓存雪崩
大量缓存同时过期,请求全部打到 DB。
防御:给 TTL 加随机抖动,避免同时过期。
const ttl = 3600 + Math.floor(Math.random() * 600) // 3600~4200 秒
await redis.set(key, value, { ex: ttl })7.2 缓存穿透
查询不存在的数据,每次都 miss。
防御:布隆过滤器拦截不存在的 key,或者缓存空值。
const cached = await redis.get(key)
if (cached === 'NULL') return null // 空值缓存
const data = await db.get(key)
if (!data) {
await redis.set(key, 'NULL', { ex: 60 }) // 缓存空值,短 TTL
return null
}7.3 缓存击穿
热点 key 过期瞬间,大量请求同时 miss。
防御:互斥锁(Redis 的 SETNX)——只让一个请求重建缓存,其他等待。
async function getWithLock(redis: Redis, key: string, loader: () => Promise<any>) {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
// 抢锁
const lockKey = `lock:${key}`
const locked = await redis.set(lockKey, '1', { ex: 10, nx: true })
if (locked) {
// 抢到锁的去加载
const data = await loader()
await redis.set(key, JSON.stringify(data), { ex: 3600 })
await redis.del(lockKey)
return data
} else {
// 没抢到的等一下再读
await new Promise((r) => setTimeout(r, 100))
return redis.get(key).then((v) => JSON.parse(v as string))
}
}KV 没有 SETNX,实现互斥锁很困难。这是 Redis 的优势。
8. 小结
Redis 在 Worker 项目里的定位:
- 不是必需。Cloudflare KV 覆盖 80% 缓存需求,更便宜、更快、更简单
- 补充角色。复杂数据结构(Sorted Set、Pub/Sub)、强一致读写、原子操作、已有 Redis 基建时引入
- 接入方式。用 Upstash 的 REST API——不需要 TCP 长连接,Worker 友好
- 典型场景。分布式限流(INCR 原子操作)、互斥锁(SETNX)、排行榜(Sorted Set)、消息队列(Stream)
缓存的四个模式:Cache-Aside、Read-Through、Write-Through、Write-Behind。绝大多数场景用 Cache-Aside 就够了——简单、清晰、容易调试。
缓存的三个经典问题:雪崩(随机 TTL)、穿透(缓存空值)、击穿(互斥锁)。Redis 应对工具更多,KV 也能凑合。
下一篇讲语义缓存——用向量相似度代替精确匹配,让「字面不同但意思相同」的查询也能命中缓存。这是 AI 项目独有的缓存类型。