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 的区别?
| 特性 | Redis | Memcached |
|---|---|---|
| 数据结构 | 多种 | 只支持字符串 |
| 持久化 | 支持 | 不支持 |
| 复制 | 支持 | 不支持 |
| 集群 | 原生支持 | 客户端分片 |
| 性能 | 略低 | 略高 |
| 内存管理 | 更灵活 | 固定 |
Q3: Redis 如何实现分布式锁?
答案:
- 使用
SET key value NX EX原子操作获取锁 - 锁需要设置过期时间防止死锁
- 释放锁时需要验证 value(防止误删其他客户端的锁)
- 使用 Lua 脚本保证释放锁的原子性
Q4: Redis 缓存穿透、击穿、雪崩如何处理?
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、缓存空值 |
| 缓存击穿 | 热点key过期,大量请求 | 分布式锁、永不过期 |
| 缓存雪崩 | 大量key同时过期 | 随机过期时间、多级缓存 |
Q5: Redis 的持久化方式?
RDB:
- 定时生成数据快照
- 文件小,恢复快
- 可能丢失最近数据
AOF:
- 记录所有写操作
- 数据完整,可配置同步频率
- 文件较大,恢复慢
推荐: RDB + AOF 混合使用