跳转到内容

Redis 缓存策略与分布式锁实战 2026 | 高并发架构核心组件

Redis 缓存策略与分布式锁实战

Redis 是高并发架构中不可或缺的组件。本文将系统讲解 Redis 缓存策略、缓存三大经典问题的解决方案、分布式锁的实现原理及生产环境最佳实践。


一、Redis 缓存模式

1.1 Cache-Aside(旁路缓存)

最常用的缓存模式,应用程序先查缓存,未命中再查数据库:

typescript
// Cache-Aside 模式实现
class CacheAsideService {
  constructor(
    private redis: RedisClient,
    private db: Database
  ) {}

  async getUser(id: string): Promise<User | null> {
    const cacheKey = `user:${id}`

    // 1. 先查缓存
    const cached = await this.redis.get(cacheKey)
    if (cached) {
      return JSON.parse(cached)
    }

    // 2. 缓存未命中,查数据库
    const user = await this.db.user.findById(id)
    if (!user) {
      return null
    }

    // 3. 写入缓存(设置过期时间)
    await this.redis.setex(cacheKey, 3600, JSON.stringify(user))

    return user
  }

  async updateUser(id: string, data: Partial<User>): Promise<void> {
    // 1. 更新数据库
    await this.db.user.update(id, data)

    // 2. 删除缓存(而非更新缓存)
    await this.redis.del(`user:${id}`)
  }
}

为什么删除缓存而不是更新缓存?

  • 避免并发写入导致数据不一致
  • 部分场景更新缓存计算成本高
  • 懒加载思想,下次读取时再缓存

1.2 Write-Through(直写模式)

写入时同时更新缓存和数据库,由缓存组件协调:

typescript
class WriteThroughService {
  constructor(
    private redis: RedisClient,
    private db: Database
  ) {}

  async writeUser(user: User): Promise<void> {
    // 同时写入缓存和数据库
    const pipeline = this.redis.pipeline()
    pipeline.set(`user:${user.id}`, JSON.stringify(user), 'EX', 3600)
    pipeline.exec()

    await this.db.user.upsert(user)
  }

  async readUser(id: string): Promise<User | null> {
    // 缓存中一定有数据(因为写入时已同步)
    const cached = await this.redis.get(`user:${id}`)
    return cached ? JSON.parse(cached) : null
  }
}

1.3 Write-Behind(异步写模式)

先写缓存,异步批量写入数据库,性能最高但有一致性风险:

typescript
class WriteBehindService {
  private writeQueue: Map<string, any> = new Map()
  private flushTimer: NodeJS.Timeout

  constructor(
    private redis: RedisClient,
    private db: Database
  ) {
    // 每 5 秒批量刷入数据库
    this.flushTimer = setInterval(() => this.flush(), 5000)
  }

  async writeUser(user: User): Promise<void> {
    // 1. 立即写入缓存
    await this.redis.set(`user:${user.id}`, JSON.stringify(user), 'EX', 3600)

    // 2. 加入写入队列
    this.writeQueue.set(user.id, user)
  }

  private async flush(): Promise<void> {
    if (this.writeQueue.size === 0) return

    const batch = Array.from(this.writeQueue.values())
    this.writeQueue.clear()

    try {
      // 批量写入数据库
      await this.db.user.batchUpsert(batch)
      console.log(`Flushed ${batch.length} records to database`)
    } catch (err) {
      // 写入失败,重新加入队列
      batch.forEach(item => this.writeQueue.set(item.id, item))
      console.error('Flush failed:', err)
    }
  }
}

1.4 缓存模式对比

模式一致性性能复杂度适用场景
Cache-Aside最终一致通用场景
Write-Through强一致一致性要求高
Write-Behind弱一致最高日志、计数器

二、缓存三大问题

2.1 缓存穿透

问题:查询不存在的数据,缓存和数据库都没有,每次请求都打到数据库。

用户 → 缓存(未命中) → 数据库(未命中) → 返回 null

恶意攻击:大量请求不存在的 ID → 数据库压力骤增

解决方案 1:缓存空值

typescript
async getUser(id: string): Promise<User | null> {
  const cacheKey = `user:${id}`

  const cached = await this.redis.get(cacheKey)
  if (cached !== null) {
    if (cached === 'NULL') return null  // 空值标记
    return JSON.parse(cached)
  }

  const user = await this.db.user.findById(id)
  if (!user) {
    // 缓存空值,设置较短的过期时间(60秒)
    await this.redis.setex(cacheKey, 60, 'NULL')
    return null
  }

  await this.redis.setex(cacheKey, 3600, JSON.stringify(user))
  return user
}

解决方案 2:布隆过滤器

typescript
import BloomFilter from 'bloom-filter'

class BloomGuard {
  private filter: BloomFilter

  constructor(size: number = 1000000) {
    this.filter = BloomFilter.create(size, 0.01) // 1% 误判率
  }

  // 初始化:加载所有存在的 ID
  async init(db: Database) {
    const ids = await db.user.getAllIds()
    ids.forEach(id => this.filter.add(id))
  }

  mightExist(id: string): boolean {
    return this.filter.contains(id)
  }
}

// 使用
async getUser(id: string): Promise<User | null> {
  // 布隆过滤器前置检查
  if (!this.bloom.mightExist(id)) {
    return null  // 一定不存在
  }

  // 正常查缓存 → 数据库流程
  return this.cacheAside.getUser(id)
}

2.2 缓存击穿

问题:热点数据过期瞬间,大量并发请求同时打到数据库。

热点 key 过期 → 1000 个并发请求 → 同时查数据库 → 数据库崩溃

解决方案 1:互斥锁

typescript
async getHotData(key: string): Promise<any> {
  const cached = await this.redis.get(key)
  if (cached) return JSON.parse(cached)

  // 获取互斥锁
  const lockKey = `lock:${key}`
  const lockAcquired = await this.redis.set(
    lockKey, '1', 'NX', 'EX', 10  // 10秒自动过期
  )

  if (lockAcquired) {
    try {
      // 再次检查缓存(可能已被其他请求填充)
      const cached = await this.redis.get(key)
      if (cached) return JSON.parse(cached)

      // 查询数据库
      const data = await this.db.query(key)
      await this.redis.setex(key, 3600, JSON.stringify(data))
      return data
    } finally {
      await this.redis.del(lockKey)
    }
  } else {
    // 等待 50ms 后重试
    await sleep(50)
    return this.getHotData(key)
  }
}

解决方案 2:逻辑过期

typescript
interface CachedData<T> {
  data: T
  expire: number  // 逻辑过期时间
}

async getWithLogicalExpiry(key: string): Promise<any> {
  const cached = await this.redis.get(key)
  if (!cached) return null

  const parsed: CachedData<any> = JSON.parse(cached)

  // 未逻辑过期,直接返回
  if (Date.now() < parsed.expire) {
    return parsed.data
  }

  // 逻辑过期,尝试异步刷新
  const lockKey = `lock:${key}`
  const lockAcquired = await this.redis.set(lockKey, '1', 'NX', 'EX', 10)

  if (lockAcquired) {
    // 异步刷新缓存
    setImmediate(async () => {
      try {
        const data = await this.db.query(key)
        const newData: CachedData<any> = {
          data,
          expire: Date.now() + 3600 * 1000
        }
        await this.redis.set(key, JSON.stringify(newData))
      } finally {
        await this.redis.del(lockKey)
      }
    })
  }

  // 返回旧数据(不阻塞用户)
  return parsed.data
}

2.3 缓存雪崩

问题:大量缓存同时过期,或 Redis 宕机,所有请求打到数据库。

解决方案 1:随机过期时间

typescript
async cacheData(key: string, data: any, ttl: number): Promise<void> {
  // 基础 TTL + 随机偏移(0~300秒)
  const randomTTL = ttl + Math.floor(Math.random() * 300)
  await this.redis.setex(key, randomTTL, JSON.stringify(data))
}

// 批量缓存时设置不同过期时间
async cacheUsers(users: User[]): Promise<void> {
  const pipeline = this.redis.pipeline()
  users.forEach((user, index) => {
    // 每个用户的过期时间略有不同
    const ttl = 3600 + Math.floor(Math.random() * 600)
    pipeline.setex(`user:${user.id}`, ttl, JSON.stringify(user))
  })
  await pipeline.exec()
}

解决方案 2:多级缓存

typescript
class MultiLevelCache {
  // L1: 本地内存缓存(最快)
  private localCache = new Map<string, { data: any; expire: number }>()

  // L2: Redis 缓存
  constructor(private redis: RedisClient) {}

  async get(key: string): Promise<any> {
    // L1: 检查本地缓存
    const local = this.localCache.get(key)
    if (local && local.expire > Date.now()) {
      return local.data
    }

    // L2: 检查 Redis
    const cached = await this.redis.get(key)
    if (cached) {
      const data = JSON.parse(cached)
      // 回填 L1(本地缓存 60 秒)
      this.localCache.set(key, { data, expire: Date.now() + 60000 })
      return data
    }

    return null
  }

  async set(key: string, data: any, ttl: number): Promise<void> {
    // 写入 L1
    this.localCache.set(key, { data, expire: Date.now() + 60000 })
    // 写入 L2
    await this.redis.setex(key, ttl, JSON.stringify(data))
  }
}

三、分布式锁

3.1 基础实现

typescript
class RedisLock {
  constructor(private redis: RedisClient) {}

  async acquire(key: string, ttl: number = 10): Promise<string | null> {
    // 生成唯一标识(防止误解锁)
    const lockId = crypto.randomUUID()

    // SET key value NX EX ttl
    const acquired = await this.redis.set(
      `lock:${key}`, lockId, 'NX', 'EX', ttl
    )

    return acquired ? lockId : null
  }

  async release(key: string, lockId: string): Promise<boolean> {
    // 使用 Lua 脚本保证原子性
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `

    const result = await this.redis.eval(
      script, 1, `lock:${key}`, lockId
    )

    return result === 1
  }
}

3.2 带自动续期的分布式锁

typescript
class AutoRenewLock {
  private renewTimers: Map<string, NodeJS.Timeout> = new Map()

  constructor(private redis: RedisClient) {}

  async acquire(
    key: string,
    ttl: number = 30,
    timeout: number = 5000
  ): Promise<string | null> {
    const lockId = crypto.randomUUID()
    const startTime = Date.now()

    // 尝试获取锁,带超时
    while (Date.now() - startTime < timeout) {
      const acquired = await this.redis.set(
        `lock:${key}`, lockId, 'NX', 'EX', ttl
      )

      if (acquired) {
        // 启动自动续期
        this.startRenewal(key, lockId, ttl)
        return lockId
      }

      // 等待 100ms 后重试
      await sleep(100)
    }

    return null
  }

  private startRenewal(key: string, lockId: string, ttl: number): void {
    // 每 ttl/3 秒续期一次
    const interval = Math.floor(ttl * 1000 / 3)

    const timer = setInterval(async () => {
      const script = `
        if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("expire", KEYS[1], ARGV[2])
        else
          return 0
        end
      `
      const result = await this.redis.eval(
        script, 1, `lock:${key}`, lockId, String(ttl)
      )

      if (result === 0) {
        // 锁已丢失,停止续期
        this.stopRenewal(key)
      }
    }, interval)

    this.renewTimers.set(key, timer)
  }

  private stopRenewal(key: string): void {
    const timer = this.renewTimers.get(key)
    if (timer) {
      clearInterval(timer)
      this.renewTimers.delete(key)
    }
  }

  async release(key: string, lockId: string): Promise<void> {
    this.stopRenewal(key)

    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `

    await this.redis.eval(script, 1, `lock:${key}`, lockId)
  }
}

3.3 使用示例

typescript
// 库存扣减场景
async deductStock(productId: string, quantity: number): Promise<boolean> {
  const lock = new AutoRenewLock(this.redis)
  const lockKey = `stock:${productId}`
  const lockId = await lock.acquire(lockKey, 30, 5000)

  if (!lockId) {
    throw new Error('获取锁超时,请重试')
  }

  try {
    // 查询库存
    const stock = await this.db.product.getStock(productId)
    if (stock < quantity) {
      return false
    }

    // 扣减库存
    await this.db.product.updateStock(productId, stock - quantity)
    return true
  } finally {
    await lock.release(lockKey, lockId)
  }
}

3.4 Redlock 算法

单点 Redis 锁在主从切换时可能丢失锁。Redlock 通过多个独立 Redis 节点提高可靠性:

typescript
class Redlock {
  private servers: RedisClient[]

  constructor(servers: RedisClient[]) {
    this.servers = servers
  }

  async acquire(key: string, ttl: number = 10): Promise<string | null> {
    const lockId = crypto.randomUUID()
    const quorum = Math.floor(this.servers.length / 2) + 1
    const startTime = Date.now()

    // 向所有节点请求加锁
    const results = await Promise.allSettled(
      this.servers.map(server =>
        server.set(`lock:${key}`, lockId, 'NX', 'EX', ttl)
      )
    )

    // 统计成功数量
    const acquired = results.filter(
      r => r.status === 'fulfilled' && r.value
    ).length

    // 计算耗时
    const elapsed = (Date.now() - startTime) / 1000

    if (acquired >= quorum && elapsed < ttl) {
      return lockId
    }

    // 加锁失败,释放所有已获取的锁
    await this.release(key, lockId)
    return null
  }

  async release(key: string, lockId: string): Promise<void> {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `

    await Promise.allSettled(
      this.servers.map(server =>
        server.eval(script, 1, `lock:${key}`, lockId)
      )
    )
  }
}

四、Redis 数据结构选型

4.1 常用数据结构对比

结构场景时间复杂度示例
String缓存、计数器O(1)SET key value
Hash对象存储O(1)HSET user:1 name "Alice"
List消息队列O(1)~O(N)LPUSH queue msg
Set去重、标签O(1)SADD tags "vue"
ZSet排行榜O(logN)ZADD rank 100 "user1"
Stream消息流O(1)XADD stream * key val
Bitmap签到O(1)SETBIT sign:uid 100 1
HyperLogLogUV 统计O(1)PFADD uv user1

4.2 排行榜实现

typescript
class LeaderboardService {
  constructor(private redis: RedisClient) {}

  // 添加分数
  async addScore(gameId: string, userId: string, score: number): Promise<void> {
    await this.redis.zadd(`leaderboard:${gameId}`, score, userId)
  }

  // 获取 Top N
  async getTopN(gameId: string, n: number = 10): Promise<Array<{ userId: string; score: number }>> {
    const results = await this.redis.zrevrange(
      `leaderboard:${gameId}`, 0, n - 1, 'WITHSCORES'
    )

    const leaderboard: Array<{ userId: string; score: number }> = []
    for (let i = 0; i < results.length; i += 2) {
      leaderboard.push({
        userId: results[i],
        score: parseFloat(results[i + 1])
      })
    }

    return leaderboard
  }

  // 获取用户排名
  async getUserRank(gameId: string, userId: string): Promise<{ rank: number; score: number } | null> {
    const rank = await this.redis.zrevrank(`leaderboard:${gameId}`, userId)
    const score = await this.redis.zscore(`leaderboard:${gameId}`, userId)

    if (rank === null || score === null) return null

    return { rank: rank + 1, score: parseFloat(score) }
  }
}

4.3 限流器实现

typescript
class RateLimiter {
  constructor(private redis: RedisClient) {}

  // 滑动窗口限流
  async isAllowed(
    userId: string,
    action: string,
    limit: number,
    windowSec: number
  ): Promise<boolean> {
    const key = `rate:${action}:${userId}`
    const now = Date.now()
    const windowStart = now - windowSec * 1000

    const pipeline = this.redis.pipeline()

    // 移除窗口外的记录
    pipeline.zremrangebyscore(key, 0, windowStart)
    // 添加当前请求
    pipeline.zadd(key, now, `${now}`)
    // 统计窗口内请求数
    pipeline.zcard(key)
    // 设置过期时间
    pipeline.expire(key, windowSec)

    const results = await pipeline.exec()
    const count = results![2][1] as number

    return count <= limit
  }
}

// 使用:每分钟最多 60 次请求
const allowed = await rateLimiter.isAllowed(userId, 'api_call', 60, 60)
if (!allowed) {
  throw new Error('请求过于频繁')
}

五、最佳实践

5.1 Key 命名规范

业务:对象:ID:字段

# 示例
user:1001:profile
order:2024:1001
cache:api:/users:page1
lock:stock:product-1001
rate:login:user-1001

5.2 Pipeline 批量操作

typescript
// 不好:多次往返
for (const id of userIds) {
  await redis.get(`user:${id}`)
}

// 好:Pipeline 批量
const pipeline = redis.pipeline()
userIds.forEach(id => pipeline.get(`user:${id}`))
const results = await pipeline.exec()

5.3 连接池配置

typescript
import Redis from 'ioredis'

const redis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  retryStrategy: (times) => Math.min(times * 100, 3000),

  // 连接池
  family: 4,
  keepAlive: true,
  connectionTimeout: 5000,
  commandTimeout: 3000,
})

// 集群模式
const cluster = new Redis.Cluster(
  [
    { host: '10.0.0.1', port: 6379 },
    { host: '10.0.0.2', port: 6379 },
    { host: '10.0.0.3', port: 6379 },
  ],
  {
    scaleReads: 'slave',
    maxRedirections: 16,
    retryDelayOnFailover: 200,
    redisOptions: { password: process.env.REDIS_PASSWORD }
  }
)

六、总结

  • ✅ 三种缓存模式(Cache-Aside、Write-Through、Write-Behind)
  • ✅ 缓存穿透(空值缓存、布隆过滤器)
  • ✅ 缓存击穿(互斥锁、逻辑过期)
  • ✅ 缓存雪崩(随机 TTL、多级缓存)
  • ✅ 分布式锁(基础锁、自动续期锁、Redlock 算法)
  • ✅ 数据结构选型与实战(排行榜、限流器)
  • ✅ 最佳实践(Key 命名、Pipeline、连接池)

Redis 是高并发系统的核心组件,掌握缓存策略和分布式锁是构建高可用架构的关键能力。


相关阅读: