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 | 定时快照 | 丢失最后一次快照后的数据 |
| 仅 AOF | appendfsync everysec | 最多丢失 1 秒数据 |
| 混合持久化 | AOF + RDB | 最多丢失 1 秒数据 |
推荐配置:
conf
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yesQ2: 如何处理 Redis 缓存穿透、击穿、雪崩?
code
┌─────────────────────────────────────────────────────────────┐
│ 缓存问题及解决方案 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 缓存穿透:查询不存在的数据,请求直达数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ 1. 布隆过滤器(Bloom Filter)过滤不存在数据 │ │
│ │ 2. 缓存空值(设置较短过期时间) │ │
│ │ 3. 参数校验,拦截非法请求 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 缓存击穿:热点 key 过期,大量请求直达数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ 1. 热点数据永不过期 │ │
│ │ 2. 分布式锁,只允许一个请求更新缓存 │ │
│ │ 3. 逻辑过期(数据中记录过期时间) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 缓存雪崩:大量 key 同时过期,请求直达数据库 │ │
│ │ │ │
│ │ 解决方案: │ │
│ │ 1. 过期时间加随机值,避免同时过期 │ │
│ │ 2. 高可用架构(主从、哨兵、集群) │ │
│ │ 3. 熔断降级,保护数据库 │ │
│ │ 4. 多级缓存(本地缓存 + Redis) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Q3: Redis 单线程为什么这么快?
- 纯内存操作:无磁盘 I/O 阻塞
- 单线程避免竞争:无需加锁,无上下文切换
- I/O 多路复用:epoll/kqueue 高效处理并发连接
- 非阻塞 I/O:事件驱动模型
Q4: 如何选择 Redis 部署方案?
| 数据量 | 并发量 | 推荐方案 |
|---|---|---|
| < 10GB | 低 | 单机 + RDB/AOF |
| < 50GB | 中 | 主从复制 |
| > 50GB | 高 | Redis 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)相关文档
- Redis 基础入门 - 系统架构、核心特性、数据类型操作
- Redis 进阶特性 - 过期策略、事务、发布订阅、持久化、高可用架构
- Node.js 操作 Redis - ioredis 客户端使用、实际应用示例