Skip to content

Redis 缓存

Redis 核心概念、数据类型、集群与应用场景

Redis 概述

特点

  • 内存存储,性能极高
  • 支持多种数据结构
  • 持久化(RDB/AOF)
  • 高可用(主从、哨兵、集群)

应用场景

  • 缓存层
  • 分布式锁
  • 消息队列
  • 计数器
  • 会话存储
  • 排行榜

数据类型

String(字符串)

javascript
// 基本操作
await redis.set('key', 'value')
await redis.get('key')                    // 'value'
await redis.del('key')                    // 1

// 过期时间
await redis.setex('key', 3600, 'value')   // 1小时过期
await redis.expire('key', 3600)           // 设置过期
await redis.ttl('key')                    // 查看剩余秒数

// 计数器
await redis.incr('counter')               // +1
await redis.incrby('counter', 10)         // +10
await redis.decr('counter')               // -1
await redis.getdel('key')                 // 获取并删除

// 批量操作
await redis.mset({ k1: 'v1', k2: 'v2' })
await redis.mget(['k1', 'k2'])            // ['v1', 'v2']

Hash(哈希)

javascript
// 基本操作
await redis.hset('user:1', 'name', 'Alice')
await redis.hget('user:1', 'name')        // 'Alice'
await redis.hgetall('user:1')             // { name: 'Alice', age: '25' }
await redis.hdel('user:1', 'age')         // 删除字段

// 批量操作
await redis.hmset('user:1', { name: 'Alice', age: '25' })
await redis.hmget('user:1', ['name', 'age'])  // ['Alice', '25']

// 计数器
await redis.hincrby('user:1', 'visits', 1)
await redis.hincrbyfloat('user:1', 'score', 1.5)

List(列表)

javascript
// 头部操作
await redis.lpush('list', 'a', 'b')       // [b, a]
await redis.lpop('list')                  // 'b'
await redis.lpushx('list', 'c')           // 列表存在才推送

// 尾部操作
await redis.rpush('list', 'd', 'e')       // [a, d, e]
await redis.rpop('list')                  // 'e'

// 查询
await redis.lrange('list', 0, -1)         // 获取所有
await redis.lindex('list', 0)             // 获取指定索引
await redis.llen('list')                  // 长度

// 阻塞操作(消息队列)
await redis.blpop('queue', 10)            // 阻塞弹出,10秒超时

Set(集合)

javascript
// 基本操作
await redis.sadd('tags', 'js', 'ts', 'react')  // 3
await redis.smembers('tags')                   // ['js', 'ts', 'react']
await redis.sismember('tags', 'js')            // 1 (true)
await redis.srem('tags', 'js')                 // 删除

// 集合操作
await redis.sadd('set1', 'a', 'b', 'c')
await redis.sadd('set2', 'b', 'c', 'd')

await redis.sinter('set1', 'set2')             // 交集 ['b', 'c']
await redis.sunion('set1', 'set2')             // 并集 ['a', 'b', 'c', 'd']
await redis.sdiff('set1', 'set2')              // 差集 ['a']

// 随机操作
await redis.srandmember('set1', 2)             // 随机2个
await redis.spop('set1')                       // 随机弹出

Sorted Set(有序集合)

javascript
// 基本操作
await redis.zadd('leaderboard', 100, 'Alice')
await redis.zadd('leaderboard', [
  [200, 'Bob'],
  [150, 'Charlie']
])

await redis.zrange('leaderboard', 0, -1)       // 按分数升序
await redis.zrevrange('leaderboard', 0, 2)     // 按分数降序前3

await redis.zscore('leaderboard', 'Alice')     // 100
await redis.zrank('leaderboard', 'Alice')      // 0 (排名)

// 分数操作
await redis.zincrby('leaderboard', 50, 'Alice')
await redis.zadd('leaderboard', 100, 'Alice')

// 范围查询
await redis.zrangebyscore('leaderboard', 0, 150)     // 分数0-150
await redis.zrevrangebyscore('leaderboard', 200, 100) // 分数100-200

// 数量
await redis.zcard('leaderboard')              // 元素数量
await redis.zcount('leaderboard', 0, 200)     // 指定分数范围数量

// 有序集合应用:排行榜
await redis.zadd('game:scores', [
  [100, 'player1'],
  [200, 'player2'],
  [150, 'player3']
])
// 获取前三名
await redis.zrevrange('game:scores', 0, 2, 'WITHSCORES')

高级功能

发布订阅

javascript
// 发布者
await redis.publish('channel:news', 'Hello subscribers!')

// 订阅者
const subscriber = redis.duplicate()
await subscriber.subscribe('channel:news')
subscriber.on('message', (channel, message) => {
  console.log(`Received: ${message} from ${channel}`)
})

// 模式订阅
await subscriber.psubscribe('channel:*')

分布式锁

javascript
// 获取锁
const lock = await redis.set('lock:mykey', 'unique_value', 'NX', 'EX', 10)
// NX - 只有key不存在时设置
// EX - 设置过期时间(秒)

// 释放锁(Lua脚本确保原子性)
const releaseLock = async (redis, lockKey, lockValue) => {
  const script = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `
  return redis.eval(script, 1, lockKey, lockValue)
}

// 使用
const lock = await redis.set('lock:order', 'request-id', 'NX', 'EX', 30)
if (lock === 'OK') {
  try {
    // 执行业务逻辑
    await processOrder()
  } finally {
    await releaseLock(redis, 'lock:order', 'request-id')
  }
}

Lua 脚本

javascript
// 原子操作
const script = `
  local current = redis.call("get", KEYS[1])
  if current == false or tonumber(current) < tonumber(ARGV[1]) then
    redis.call("set", KEYS[1], ARGV[1])
    return 1
  end
  return 0
`

// 限流脚本
const rateLimitScript = `
  local key = KEYS[1]
  local limit = tonumber(ARGV[1])
  local window = tonumber(ARGV[2])
  local now = tonumber(ARGV[3])

  redis.call('zadd', key, now, now)
  redis.call('zremrangebyscore', key, 0, now - window)
  local count = redis.call('zcard', key)

  if count > limit then
    return 0
  end

  redis.call('pexpire', key, window)
  return 1
`

await redis.eval(rateLimitScript, 1, 'ratelimit:api', 100, 60000, Date.now())

管道(Pipeline)

javascript
// 批量操作,减少网络往返
const pipeline = redis.pipeline()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.get('key1')
pipeline.incr('counter')
const results = await pipeline.exec()
// [ [null, 'OK'], [null, 'OK'], [null, 'value1'], [null, 1] ]

Node.js 集成

ioredis

javascript
import Redis from 'ioredis'

const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'your-password',
  db: 0,
  maxRetriesPerRequest: 3,
  retryDelayOnFailover: 100,
  enableReadyCheck: true,
  lazyConnect: true
})

// 连接
await redis.connect()

// 错误处理
redis.on('error', err => console.error('Redis Error:', err))
redis.on('connect', () => console.log('Redis connected'))

// 断线重连
redis.on('close', () => {
  console.log('Redis disconnected, reconnecting...')
})

// 集群模式
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 }
])

封装缓存服务

javascript
import Redis from 'ioredis'

class CacheService {
  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: process.env.REDIS_PORT || 6379,
      password: process.env.REDIS_PASSWORD
    })
  }

  // 基础操作
  async get(key) {
    const value = await this.redis.get(key)
    return value ? JSON.parse(value) : null
  }

  async set(key, value, ttlSeconds = 3600) {
    await this.redis.setex(key, ttlSeconds, JSON.stringify(value))
  }

  async del(key) {
    await this.redis.del(key)
  }

  async exists(key) {
    return (await this.redis.exists(key)) === 1
  }

  // 缓存模式
  async getOrSet(key, fetchFn, ttl = 3600) {
    const cached = await this.get(key)
    if (cached !== null) return cached

    const value = await fetchFn()
    await this.set(key, value, ttl)
    return value
  }

  // 分布式锁
  async acquireLock(lockName, ttlMs = 30000) {
    const lockId = `${Date.now()}-${Math.random()}`
    const result = await this.redis.set(
      `lock:${lockName}`,
      lockId,
      'PX',
      ttlMs,
      'NX'
    )
    return result === 'OK' ? lockId : null
  }

  async releaseLock(lockName, lockId) {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      end
      return 0
    `
    return this.redis.eval(script, 1, `lock:${lockName}`, lockId)
  }

  // 限流
  async rateLimit(key, maxRequests, windowSeconds) {
    const now = Date.now()
    const windowMs = windowSeconds * 1000

    const script = `
      local key = KEYS[1]
      local limit = tonumber(ARGV[1])
      local window = tonumber(ARGV[2])
      local now = tonumber(ARGV[3])

      redis.call('zadd', key, now, now)
      redis.call('zremrangebyscore', key, 0, now - window)
      local count = redis.call('zcard', key)

      redis.call('pexpire', key, window)
      return count
    `

    const count = await this.redis.eval(
      script,
      1,
      `ratelimit:${key}`,
      maxRequests,
      windowMs,
      now
    )

    return {
      allowed: count <= maxRequests,
      count,
      remaining: Math.max(0, maxRequests - count)
    }
  }
}

export const cache = new CacheService()

面试高频题

Q1: Redis 有哪些数据类型?适用场景?

类型场景
String缓存、计数器、Session
Hash对象存储、用户信息
List消息队列、最新列表
Set标签、共同好友、去重
Sorted Set排行榜、权重队列

Q2: Redis 和 Memcached 的区别?

特性RedisMemcached
数据结构多种只支持字符串
持久化支持不支持
复制支持不支持
集群原生支持客户端分片
性能略低略高
内存管理更灵活固定

Q3: Redis 如何实现分布式锁?

答案:

  1. 使用 SET key value NX EX 原子操作获取锁
  2. 锁需要设置过期时间防止死锁
  3. 释放锁时需要验证 value(防止误删其他客户端的锁)
  4. 使用 Lua 脚本保证释放锁的原子性

Q4: Redis 缓存穿透、击穿、雪崩如何处理?

问题原因解决方案
缓存穿透查询不存在的数据布隆过滤器、缓存空值
缓存击穿热点key过期,大量请求分布式锁、永不过期
缓存雪崩大量key同时过期随机过期时间、多级缓存

Q5: Redis 的持久化方式?

RDB:

  • 定时生成数据快照
  • 文件小,恢复快
  • 可能丢失最近数据

AOF:

  • 记录所有写操作
  • 数据完整,可配置同步频率
  • 文件较大,恢复慢

推荐: RDB + AOF 混合使用

前端面试知识库