{T}

Redis 最佳实践与常见问题

本文档涵盖 Redis 性能优化策略、最佳实践、安全配置和常见问题解答。

性能优化

内存优化

code
┌─────────────────────────────────────────────────────────────┐
│                    内存优化策略                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 选择合适的数据结构                                       │
│     • 小对象:String                                        │
│     • 多字段对象:Hash                                      │
│     • 计数器:String                                        │
│     • 唯一集合:Set                                         │
│                                                             │
│  2. 控制键名长度                                             │
│     user:profile:1001  vs  u:p:1001                         │
│     (节省内存,但需权衡可读性)                               │
│                                                             │
│  3. 合理设置过期时间                                         │
│     避免内存中堆积无用的 key                                 │
│                                                             │
│  4. 使用 Hash 优化                                          │
│     当字段数 < 512 且值 < 64 字节时,Hash 使用 ziplist      │
│     编码,内存效率更高                                       │
│                                                             │
│  5. 避免 Big Key                                            │
│     • String > 10KB                                        │
│     • 集合元素 > 5000 个                                    │
│     拆分为多个小 key                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

网络优化

javascript
// 1. 使用 Pipeline 批量操作
const pipeline = redis.pipeline()
for (let i = 0; i < 1000; i++) {
  pipeline.set(`key:${i}`, `value:${i}`)
}
await pipeline.exec()

// 2. 使用 MGET/MSET 替代多次 GET/SET
await redis.mset({ key1: "v1", key2: "v2", key3: "v3" })
const values = await redis.mget("key1", "key2", "key3")

// 3. 使用 Lua 脚本减少网络往返
await redis.eval(luaScript, 1, "key", "arg1", "arg2")

// 4. 本地缓存热点数据
const localCache = new Map()
async function getWithLocalCache(key) {
  if (localCache.has(key)) {
    return localCache.get(key)
  }
  const value = await redis.get(key)
  localCache.set(key, value)
  setTimeout(() => localCache.delete(key), 60000)  // 1 分钟后失效
  return value
}

命令优化

javascript
// ❌ 避免在生产环境使用 KEYS
const keys = await redis.keys("user:*")  // 阻塞 Redis

// ✅ 使用 SCAN 安全遍历
async function scanKeys(pattern, count = 100) {
  const keys = []
  let cursor = "0"
  do {
    const [nextCursor, matched] = await redis.scan(cursor, "MATCH", pattern, "COUNT", count)
    cursor = nextCursor
    keys.push(...matched)
  } while (cursor !== "0")
  return keys
}

// ❌ 避免对大集合使用全量操作
const all = await redis.smembers("big_set")  // 可能返回大量数据

// ✅ 使用 SSCAN 分批获取
async function* iterSet(key, count = 100) {
  let cursor = "0"
  do {
    const [nextCursor, members] = await redis.sscan(key, cursor, "COUNT", count)
    cursor = nextCursor
    yield members
  } while (cursor !== "0")
}

最佳实践

键名设计规范

code
┌─────────────────────────────────────────────────────────────┐
│                    键名命名规范                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  格式:业务:对象:ID[:属性]                                   │
│                                                             │
│  示例:                                                     │
│  • user:1001                 用户对象                       │
│  • user:1001:profile         用户资料                       │
│  • order:2024:01:15          按日期分片                     │
│  • cache:api:/users:1001     API 缓存                       │
│  • lock:order:123            分布式锁                       │
│  • rate:limit:192.168.1.1    限流器                         │
│                                                             │
│  注意:                                                     │
│  • 使用冒号分隔,便于管理和查询                             │
│  • 长度适中,避免过长浪费内存                               │
│  • 使用小写,保持一致性                                     │
│  • 避免特殊字符                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

安全配置

conf
# redis.conf 安全配置

# 1. 设置强密码
requirepass "your-strong-password-here"

# 2. 绑定受信任 IP
bind 127.0.0.1 192.168.1.100

# 3. 保护模式
protected-mode yes

# 4. 禁用危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG ""
rename-command DEBUG ""

# 5. 重命名敏感命令
rename-command SHUTDOWN "SHUTDOWN_$(openssl rand -hex 16)"

监控指标

code
┌─────────────────────────────────────────────────────────────┐
│                    关键监控指标                              │
├──────────────────────┬──────────────────────────────────────┤
│        指标          │                说明                  │
├──────────────────────┼──────────────────────────────────────┤
│ used_memory          │ 已使用内存                           │
│ used_memory_peak     │ 内存使用峰值                         │
│ used_memory_ratio    │ 内存使用率                           │
│ connected_clients    │ 当前连接数                           │
│ blocked_clients      │ 阻塞的客户端数                       │
│ instantaneous_ops    │ 每秒操作数                           │
│ total_commands       │ 累计命令数                           │
│ expired_keys         │ 过期 key 数量                        │
│ evicted_keys         │ 被淘汰的 key 数量                    │
│ keyspace_hits        │ 缓存命中次数                         │
│ keyspace_misses      │ 缓存未命中次数                       │
│ latest_fork_usec     │ 最近 fork 耗时                       │
│ rdb_last_save_time   │ 最后保存时间                         │
│ aof_last_write_status│ AOF 最后写入状态                     │
└──────────────────────┴──────────────────────────────────────┘
javascript
// 监控脚本示例
async function monitorRedis(redis) {
  const info = await redis.info()
  const memory = await redis.info("memory")
  const stats = await redis.info("stats")

  // 解析关键指标
  const metrics = {
    usedMemory: parseInfo(memory, "used_memory"),
    usedMemoryPeak: parseInfo(memory, "used_memory_peak"),
    connectedClients: parseInfo(info, "connected_clients"),
    instantaneousOpsPerSec: parseInfo(stats, "instantaneous_ops_per_sec"),
    keyspaceHits: parseInfo(stats, "keyspace_hits"),
    keyspaceMisses: parseInfo(stats, "keyspace_misses")
  }

  // 计算缓存命中率
  const hitRate = metrics.keyspaceHits / (metrics.keyspaceHits + metrics.keyspaceMisses)

  console.log({
    ...metrics,
    hitRate: (hitRate * 100).toFixed(2) + "%"
  })
}

function parseInfo(info, key) {
  const match = info.match(new RegExp(`${key}:(.+)`))
  return match ? match[1].trim() : null
}

常见问题解答

Q1: Redis 如何保证数据不丢失?

答: 通过持久化机制:

方案配置数据丢失风险
仅 RDB定时快照丢失最后一次快照后的数据
仅 AOFappendfsync everysec最多丢失 1 秒数据
混合持久化AOF + RDB最多丢失 1 秒数据

推荐配置:

conf
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes

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

code
┌─────────────────────────────────────────────────────────────┐
│                    缓存问题及解决方案                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 缓存穿透:查询不存在的数据,请求直达数据库            │   │
│  │                                                      │   │
│  │ 解决方案:                                            │   │
│  │ 1. 布隆过滤器(Bloom Filter)过滤不存在数据          │   │
│  │ 2. 缓存空值(设置较短过期时间)                       │   │
│  │ 3. 参数校验,拦截非法请求                            │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 缓存击穿:热点 key 过期,大量请求直达数据库           │   │
│  │                                                      │   │
│  │ 解决方案:                                            │   │
│  │ 1. 热点数据永不过期                                   │   │
│  │ 2. 分布式锁,只允许一个请求更新缓存                   │   │
│  │ 3. 逻辑过期(数据中记录过期时间)                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 缓存雪崩:大量 key 同时过期,请求直达数据库           │   │
│  │                                                      │   │
│  │ 解决方案:                                            │   │
│  │ 1. 过期时间加随机值,避免同时过期                     │   │
│  │ 2. 高可用架构(主从、哨兵、集群)                     │   │
│  │ 3. 熔断降级,保护数据库                              │   │
│  │ 4. 多级缓存(本地缓存 + Redis)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Q3: Redis 单线程为什么这么快?

  1. 纯内存操作:无磁盘 I/O 阻塞
  2. 单线程避免竞争:无需加锁,无上下文切换
  3. I/O 多路复用:epoll/kqueue 高效处理并发连接
  4. 非阻塞 I/O:事件驱动模型

Q4: 如何选择 Redis 部署方案?

数据量并发量推荐方案
< 10GB单机 + RDB/AOF
< 50GB主从复制
> 50GBRedis Cluster
需要高可用-哨兵 或 Cluster

Q5: Big Key 有什么危害?如何处理?

危害:

  • 内存占用大
  • 操作耗时,阻塞其他请求
  • 主从同步延迟
  • 集群数据倾斜

处理方案:

javascript
// 1. 拆分大 Key
// 原来
await redis.set("user:1001:history", bigJson)  // 大对象

// 拆分后
await redis.hset("user:1001:history", "page1", page1Data)
await redis.hset("user:1001:history", "page2", page2Data)

// 2. 使用 SCAN 逐步删除
async function deleteBigKey(key) {
  const type = await redis.type(key)

  if (type === "string") {
    await redis.del(key)
  } else if (type === "hash") {
    let cursor = "0"
    do {
      const [nextCursor, fields] = await redis.hscan(key, cursor, "COUNT", 100)
      cursor = nextCursor
      if (fields.length > 0) {
        await redis.hdel(key, ...fields.filter((_, i) => i % 2 === 0))
      }
    } while (cursor !== "0")
  }
  // 其他类型类似处理
}

Q6: Redis 集群支持哪些操作?

支持: 单 key 操作、事务(同一槽位)、Lua 脚本(同一槽位)

不支持: 跨槽位的多 key 操作、跨节点事务

解决方案: 使用 Hash Tag 确保相关 key 在同一槽位

javascript
// Hash Tag: 使用 {} 指定分片依据
// 以下 key 会被分配到同一槽位
await redis.set("user:{1001}:profile", data)
await redis.set("user:{1001}:settings", settings)
await redis.set("user:{1001}:history", history)

Q7: 如何实现分布式锁?

推荐方案:Redisson 或 RedLock 算法

javascript
class DistributedLock {
  constructor(redis) {
    this.redis = redis
  }

  async lock(key, ttl = 10000) {
    const value = `${Date.now()}-${Math.random()}`
    const result = await this.redis.set(key, value, "PX", ttl, "NX")
    return result === "OK" ? value : null
  }

  async unlock(key, value) {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `
    return await this.redis.eval(script, 1, key, value)
  }
}

Q8: Redis 如何实现消息队列?

方案优点缺点适用场景
List + BLPOP简单无 ACK 机制简单任务队列
Pub/Sub实时推送无持久化实时通知
Stream持久化、ACK、消费者组配置复杂可靠消息队列

Stream 示例:

javascript
// 生产者
await redis.xadd("mystream", "*", { task: "data" })

// 消费者
await redis.xgroup("CREATE", "mystream", "mygroup", "$", "MKSTREAM")
const messages = await redis.xreadgroup(
  "GROUP", "mygroup", "consumer1",
  "COUNT", 10,
  "STREAMS", "mystream", ">"
)

// 确认消息
await redis.xack("mystream", "mygroup", messageId)

相关文档

参考资料