当你的网站用户量从 10 个增长到 10 万个,数据库查询逐渐成为瓶颈时,你会怎么办?加更多数据库服务器?优化 SQL 查询?这些办法有用,但还有一个更简单高效的方案——使用 Redis 缓存。
今天我们就来聊聊 Redis,这个能让你的接口性能提升好几个档次的神奇工具。
Redis 简介与应用场景
Redis(Remote Dictionary Server,远程字典服务器)是一个开源的内存数据结构存储系统,可以用作数据库、缓存和消息代理。
为什么选择 Redis?
- 极快的速度:基于内存操作,读写速度可达 10 万次/秒
- 丰富的数据类型:不只是简单的 key-value,还支持列表、集合、有序集合等
- 持久化支持:可以定期保存到磁盘,重启后数据不丢失
- 分布式支持:支持主从复制、哨兵、集群等高可用方案
典型应用场景
| 场景 |
说明 |
| 缓存 |
缓存热点数据,减轻数据库压力 |
| 会话存储 |
存储用户登录状态、购物车等 |
| 排行榜 |
利用有序集合实现实时排名 |
| 计数器 |
文章阅读数、点赞数等 |
| 分布式锁 |
防止并发操作冲突 |
| 消息队列 |
简单的发布订阅、列表队列 |
五大基础数据类型
Redis 支持五种基本数据类型,每种都有其特定的应用场景。
1. String(字符串)
最基本的数据类型,可以存储任何类型的字符串(包括二进制数据)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| SET user:1001 "张三" GET user:1001
SET session:abc123 "user_data" EX 3600
SET lock:order "locked" NX EX 10
MSET user:1001:name "张三" user:1001:age 25 user:1001:city "北京" MGET user:1001:name user:1001:age
SET views:1001 0 INCR views:1001 INCRBY views:1001 10 DECR views:1001
|
2. Hash(哈希)
适合存储对象,一个 key 对应多个字段。
1 2 3 4 5 6 7 8 9 10 11 12 13
| HSET user:1001 name "张三" age 25 city "北京"
HGET user:1001 name HGETALL user:1001
HINCRBY user:1001 age 1 HEXISTS user:1001 email
HDEL user:1001 city
|
3. List(列表)
有序的字符串集合,可以重复。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| LPUSH messages "你好" "世界" RPUSH messages "欢迎使用 Redis"
LRANGE messages 0 -1 LINDEX messages 0 LLEN messages
LPOP messages RPOP messages
BLPOP queue 0
|
4. Set(集合)
无序的唯一字符串集合,适合做去重和交集运算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| SADD tags:1001 "技术" "编程" "Redis"
SMEMBERS tags:1001
SISMEMBER tags:1001 "Redis"
SADD set1 1 2 3 4 SADD set2 3 4 5 6 SINTER set1 set2 SUNION set1 set2 SDIFF set1 set2
SRANDMEMBER tags:1001 SPOP tags:1001
|
5. Sorted Set(有序集合)
类似 Set,但每个元素关联一个分数,按分数排序。适合做排行榜。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ZADD rank:score 100 "张三" 95 "李四" 88 "王五"
ZRANGE rank:score 0 -1 ZRANGE rank:score 0 -1 WITHSCORES ZREVRANGE rank:score 0 2
ZSCORE rank:score "张三" ZRANK rank:score "张三" ZREVRANK rank:score "张三"
ZINCRBY rank:score 5 "张三" ZCOUNT rank:score 90 100
|
持久化机制
Redis 是内存数据库,但提供了两种持久化方式,防止重启后数据丢失。
RDB(快照持久化)
1 2 3 4 5 6 7 8
| save 900 1 save 300 10 save 60 10000
BGSAVE SAVE
|
RDB 特点:
- 文件紧凑,适合备份
- 恢复速度快
- 可能丢失最后一次快照后的数据
AOF(追加文件)
1 2 3 4 5
| appendonly yes appendfsync everysec appendfsync always appendfsync no
|
AOF 特点:
- 数据更安全,丢失数据更少
- 文件较大,恢复较慢
- 可以重写压缩文件
混合持久化(推荐)
1 2
| aof-use-rdb-preamble yes
|
结合 RDB 和 AOF 的优点:AOF 文件开头用 RDB 格式,后面是 AOF 格式。
分布式锁实现
在分布式系统中,经常需要防止多个进程同时操作同一个资源。Redis 可以用来实现分布式锁。
1 2 3 4 5 6 7 8 9 10 11
|
SET lock:order "unique_identifier" NX EX 10
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
|
Node.js 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| const redis = require("redis"); const client = redis.createClient();
class RedisLock { constructor(key, ttl = 10) { this.key = `lock:${key}`; this.ttl = ttl; this.identifier = `${Date.now()}_${Math.random()}`; }
async acquire() { const result = await client.set( this.key, this.identifier, "NX", "EX", this.ttl, ); return result === "OK"; }
async release() { const script = ` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `; await client.eval(script, 1, this.key, this.identifier); } }
async function processOrder(orderId) { const lock = new RedisLock(`order:${orderId}`);
if (await lock.acquire()) { try { console.log("处理订单:", orderId); } finally { await lock.release(); } } else { console.log("获取锁失败,订单正在处理中"); } }
|
缓存穿透/击穿/雪崩
这是使用缓存时常见的三个问题。
缓存穿透
问题描述:查询一个不存在的数据,缓存和数据库都没有,每次都打到数据库。
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
async function getUser(userId) { const key = `user:${userId}`;
let user = await redis.get(key); if (user === "NULL") return null; if (user) return JSON.parse(user);
user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
if (user) { await redis.set(key, JSON.stringify(user), "EX", 3600); } else { await redis.set(key, "NULL", "EX", 60); }
return user; }
|
缓存击穿
问题描述:某个热点 key 过期瞬间,大量请求直接打到数据库。
解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
async function getHotData(key) { let data = await redis.get(key);
if (data) return JSON.parse(data);
const lock = new RedisLock(`lock:${key}`); if (await lock.acquire()) { try { data = await redis.get(key); if (data) return JSON.parse(data);
data = await db.query("..."); await redis.set(key, JSON.stringify(data), "EX", 3600); return data; } finally { await lock.release(); } }
await sleep(100); return getHotData(key); }
|
缓存雪崩
问题描述:大量 key 同时失效,请求全部打到数据库。
解决方案:
1 2 3 4 5 6
| const expireTime = 3600 + Math.random() * 600; await redis.set(key, data, "EX", expireTime);
|
实战案例:使用 Redis 优化接口性能
假设有一个获取文章详情的接口,每次都查询数据库,响应时间约 200ms。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| const redis = require("redis"); const client = redis.createClient();
async function getArticle(articleId) { const article = await db.query("SELECT * FROM articles WHERE id = ?", [ articleId, ]); return article; }
async function getArticleCached(articleId) { const key = `article:${articleId}`;
const cached = await client.get(key); if (cached) { console.log("从缓存获取"); return JSON.parse(cached); }
console.log("从数据库获取"); const article = await db.query("SELECT * FROM articles WHERE id = ?", [ articleId, ]);
await client.set(key, JSON.stringify(article), "EX", 3600);
return article; }
async function getArticleWithLock(articleId) { const key = `article:${articleId}`; const lockKey = `lock:${articleId}`;
const cached = await client.get(key); if (cached) { return JSON.parse(cached); }
const lockAcquired = await client.set(lockKey, "1", "NX", "EX", 10);
if (lockAcquired) { try { const doubleCheck = await client.get(key); if (doubleCheck) { return JSON.parse(doubleCheck); }
const article = await db.query("SELECT * FROM articles WHERE id = ?", [ articleId, ]);
await client.set(key, JSON.stringify(article), "EX", 3600);
return article; } finally { await client.del(lockKey); } } else { await new Promise((resolve) => setTimeout(resolve, 100)); return getArticleWithLock(articleId); } }
|
总结
Redis 是一个强大而灵活的缓存工具,掌握它能带来:
- 性能提升:从数据库查询到内存缓存,性能提升 10-100 倍
- 减轻数据库压力:热点数据缓存,减少数据库负载
- 支持复杂场景:排行榜、计数器、分布式锁等
- 简单易用:丰富的客户端库,学习成本低
关键知识点回顾:
- 数据类型:String、Hash、List、Set、Sorted Set
- 持久化:RDB 快照、AOF 日志、混合模式
- 常见问题:缓存穿透、击穿、雪崩及解决方案
- 最佳实践:合理设置过期时间、使用互斥锁、多级缓存
接下来你可以探索:
- Redis 主从复制和哨兵
- Redis 集群搭建
- Redis 事务和管道
- Redis 发布订阅
记住,缓存虽好,但也不要滥用。不是所有数据都需要缓存,合理使用才是王道!