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?

几个场景:

  1. 已有 Redis 基建。团队已经在用 Redis 做 session、队列、限流,新加的 Worker 服务需要跟现有系统共享缓存
  2. 复杂数据结构。Redis 的 Sorted Set、Hash、Pub/Sub、Stream 等数据结构 KV 没有
  3. 强一致读写。KV 是最终一致——写完立刻读可能读不到。Redis 是单线程模型,写完立刻读一定能读到
  4. Atomic 操作。Redis 的 INCRSETNX、Lua 脚本在分布式环境下是原子的——KV 做不到

但先说结论:Worker 项目里 80% 的场景用 KV 就够了。Redis 是补充,不是替代。

2. Worker 环境的 Redis 挑战

传统 Node.js 用 ioredisnode-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 RedisCloudflare KV
读延迟10-30ms5-10ms
写延迟10-30ms5-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 项目独有的缓存类型。