Skip to content

Redis 实战

Redis 基础

什么是 Redis

Redis(Remote Dictionary Server)是一个开源的、内存中的数据结构存储系统,可用作数据库、缓存和消息代理。它支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。

Redis 为什么快

┌───────────────────────────────────────────────────┐
│                  Redis 性能优势                  │
├───────────────────┬───────────────────────────────┤
│ 1. 内存存储       │ 数据全部在内存,读写速度快    │
│ 2. 单线程模型     │ 避免线程切换和锁竞争         │
│ 3. I/O 多路复用   │ 高效处理并发连接             │
│ 4. 精简数据结构   │ 底层实现优化,占用空间小      │
│ 5. 网络模型       │ 非阻塞 I/O,响应迅速         │
└───────────────────┴───────────────────────────────┘

Redis 6.0 多线程

Redis 6.0 引入了多线程,但只用于处理网络 I/O,核心命令执行仍然是单线程的,这样既保持了单线程的简单性,又提高了并发处理能力。


五种基本数据结构

String(字符串)

用途:缓存、计数器、分布式锁

常用命令

  • SET key value:设置键值对
  • GET key:获取值
  • INCR key:自增
  • DECR key:自减
  • EXPIRE key seconds:设置过期时间

示例

javascript
// 计数器
await redisClient.incr('page_view');

// 缓存用户信息
await redisClient.set('user:1000', JSON.stringify({ id: 1000, name: 'John' }));
await redisClient.expire('user:1000', 3600);

List(列表)

用途:消息队列、最新列表、栈/队列

常用命令

  • LPUSH key value:从左侧插入
  • RPUSH key value:从右侧插入
  • LPOP key:从左侧弹出
  • RPOP key:从右侧弹出
  • LLEN key:获取列表长度
  • LRANGE key start stop:获取指定范围的元素

示例

javascript
// 消息队列
await redisClient.lpush('messages', 'hello');
const message = await redisClient.rpop('messages');

// 最新文章列表
await redisClient.lpush('latest_articles', 'article:1001');
await redisClient.ltrim('latest_articles', 0, 9); // 只保留最新 10 篇

Hash(哈希)

用途:对象存储、用户信息

常用命令

  • HSET key field value:设置字段值
  • HGET key field:获取字段值
  • HGETALL key:获取所有字段值
  • HDEL key field:删除字段
  • HMSET key field1 value1 field2 value2:设置多个字段值

示例

javascript
// 存储用户信息
await redisClient.hset('user:1000', {
  name: 'John',
  email: 'john@example.com',
  age: 30
});

const user = await redisClient.hgetall('user:1000');

Set(集合)

用途:去重、交集并集差集

常用命令

  • SADD key member:添加成员
  • SREM key member:移除成员
  • SMEMBERS key:获取所有成员
  • SISMEMBER key member:判断成员是否存在
  • SINTER key1 key2:交集
  • SUNION key1 key2:并集
  • SDIFF key1 key2:差集

示例

javascript
// 标签管理
await redisClient.sadd('article:1001:tags', 'nodejs', 'redis', 'javascript');

// 用户关注
await redisClient.sadd('user:1000:following', 'user:1001', 'user:1002');
await redisClient.sadd('user:1001:followers', 'user:1000');

// 共同关注
const mutualFollows = await redisClient.sinter('user:1000:following', 'user:1001:following');

Sorted Set(有序集合)

用途:排行榜、延迟队列、范围查询

常用命令

  • ZADD key score member:添加成员及分数
  • ZREM key member:移除成员
  • ZRANGE key start stop:按分数升序获取成员
  • ZREVRANGE key start stop:按分数降序获取成员
  • ZSCORE key member:获取成员分数
  • ZINCRBY key increment member:增加成员分数

示例

javascript
// 排行榜
await redisClient.zadd('leaderboard', 100, 'user:1000');
await redisClient.zadd('leaderboard', 200, 'user:1001');

// 获取前 10 名
const top10 = await redisClient.zrevrange('leaderboard', 0, 9, 'WITHSCORES');

// 延迟队列
const score = Date.now() + 60000; // 1 分钟后
await redisClient.zadd('delayed_jobs', score, 'job:1001');

// 处理到期任务
const now = Date.now();
const jobs = await redisClient.zrangebyscore('delayed_jobs', 0, now);

高级数据结构

HyperLogLog

用途:基数统计(如独立用户数)

特点:占用空间小,误差率约 0.81%

常用命令

  • PFADD key element:添加元素
  • PFCOUNT key:获取基数
  • PFMERGE destkey sourcekey1 sourcekey2:合并多个 HyperLogLog

示例

javascript
// 统计独立访客
await redisClient.pfadd('unique_visitors', 'user:1000', 'user:1001', 'user:1002');
const count = await redisClient.pfcount('unique_visitors');
console.log(`独立访客数:${count}`);

Bitmap

用途:布尔值存储(如用户签到、在线状态)

常用命令

  • SETBIT key offset value:设置位值
  • GETBIT key offset:获取位值
  • BITCOUNT key:统计 1 的个数
  • BITOP operation destkey key1 key2:位运算

示例

javascript
// 用户签到
const userId = 1000;
const dayOfYear = 100; // 一年中的第 100 天
await redisClient.setbit(`user:${userId}:checkins`, dayOfYear, 1);

// 统计签到天数
const checkinDays = await redisClient.bitcount(`user:${userId}:checkins`);

Geo

用途:地理位置(如附近的人、商家)

常用命令

  • GEOADD key longitude latitude member:添加位置
  • GEODIST key member1 member2 unit:计算距离
  • GEORADIUS key longitude latitude radius unit:获取指定范围内的成员

示例

javascript
// 添加商家位置
await redisClient.geoadd('restaurants', 116.404, 39.915, 'restaurant:1001');
await redisClient.geoadd('restaurants', 116.414, 39.925, 'restaurant:1002');

// 查找附近的商家(5 公里内)
const nearbyRestaurants = await redisClient.georadius('restaurants', 116.404, 39.915, 5, 'km');

Stream

用途:消息队列(支持消费组)

常用命令

  • XADD key * field value:添加消息
  • XREAD COUNT count BLOCK milliseconds STREAMS key id:读取消息
  • XGROUP CREATE key groupname id:创建消费组
  • XREADGROUP GROUP groupname consumername COUNT count BLOCK milliseconds STREAMS key >:消费组读取消息

示例

javascript
// 生产消息
await redisClient.xadd('orders', '*', 'orderId', '1001', 'userId', '2001');

// 消费消息
const messages = await redisClient.xread('COUNT', 10, 'BLOCK', 0, 'STREAMS', 'orders', '0');

缓存策略

缓存模式

模式描述优点缺点
Cache-Aside应用先查缓存,未命中则查数据库并更新缓存简单,适用范围广存在缓存不一致风险
Read-Through缓存负责查询数据库并更新自己逻辑清晰,一致性好实现复杂
Write-Through应用写缓存,缓存负责写数据库一致性好写性能受数据库影响
Write-Back应用写缓存,缓存异步写数据库写性能好有数据丢失风险

缓存问题与解决方案

1. 缓存穿透

问题:请求不存在的数据,导致每次都查询数据库

解决方案

  • 布隆过滤器:过滤不存在的键
  • 空值缓存:缓存不存在的数据,设置较短的过期时间

2. 缓存击穿

问题:热点数据过期,导致大量请求同时查询数据库

解决方案

  • 互斥锁:只允许一个线程更新缓存
  • 逻辑过期:缓存不过期,通过后台线程更新
  • 永不过期:热点数据永不自动过期

3. 缓存雪崩

问题:大量缓存同时过期,导致数据库压力骤增

解决方案

  • 随机过期时间:为缓存设置随机的过期时间
  • 多级缓存:使用本地缓存 + 分布式缓存
  • 限流降级:对数据库访问进行限流

分布式锁

基本实现

javascript
// 获取锁
async function acquireLock(key, value, expireTime) {
  return await redisClient.set(key, value, 'NX', 'EX', expireTime);
}

// 释放锁(使用 Lua 脚本保证原子性)
async function releaseLock(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 redisClient.eval(script, 1, key, value);
}

Redlock 算法

Redlock 是 Redis 官方推荐的分布式锁算法,适用于多个 Redis 实例的场景,提高了锁的可靠性。

步骤

  1. 获取当前时间
  2. 依次向 N 个 Redis 实例获取锁
  3. 计算获取锁的时间
  4. 如果获取锁的时间小于过期时间,且成功获取了多数实例的锁,则锁获取成功
  5. 否则,释放所有实例的锁

Redis 持久化

RDB(Redis Database)

特点

  • 定期生成快照,将数据持久化到磁盘
  • 恢复速度快
  • 适合备份和灾难恢复

配置

ini
save 900 1      # 900 秒内有 1 个键变化
save 300 10     # 300 秒内有 10 个键变化
save 60 10000   # 60 秒内有 10000 个键变化

AOF(Append Only File)

特点

  • 记录所有写操作,追加到文件
  • 数据更安全,丢失数据少
  • 恢复速度较慢

配置

ini
aof yes                   # 开启 AOF
aof-use-rdb-preamble yes  # 混合持久化
appendfsync everysec      # 每秒同步一次

混合持久化

Redis 4.0 引入了混合持久化,结合了 RDB 和 AOF 的优点:

  • 使用 RDB 作为基础,保证恢复速度
  • 使用 AOF 记录增量操作,保证数据安全性

Redis 集群

主从复制

作用

  • 数据备份
  • 负载均衡(读操作)
  • 高可用性

配置

ini
replicaof <masterip> <masterport>  # 从节点配置

哨兵模式(Sentinel)

作用

  • 监控 Redis 实例
  • 自动故障转移
  • 配置中心

配置

ini
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

Redis Cluster

作用

  • 数据分片(Hash Slot)
  • 高可用性
  • 水平扩展

特点

  • 16384 个 Hash Slot
  • 每个节点负责一部分 Slot
  • 支持在线添加/删除节点
  • 自动故障转移

Node.js 操作 Redis

客户端选择

客户端特点适用场景
redis官方客户端,功能基础简单场景
ioredis功能丰富,支持集群生产环境
node-redis新版官方客户端,支持 Promise新项目

ioredis 示例

javascript
const Redis = require('ioredis');

// 单机连接
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'your-password',
  db: 0
});

// 集群连接
const redisCluster = new Redis.Cluster([
  { host: '127.0.0.1', port: 7000 },
  { host: '127.0.0.1', port: 7001 },
  { host: '127.0.0.1', port: 7002 }
]);

// 基本操作
async function redisExample() {
  // String
  await redis.set('name', 'John');
  const name = await redis.get('name');
  console.log(name);

  // Hash
  await redis.hset('user:1', 'name', 'John', 'age', 30);
  const user = await redis.hgetall('user:1');
  console.log(user);

  // List
  await redis.lpush('messages', 'hello', 'world');
  const messages = await redis.lrange('messages', 0, -1);
  console.log(messages);

  // Set
  await redis.sadd('tags', 'nodejs', 'redis');
  const tags = await redis.smembers('tags');
  console.log(tags);

  // Sorted Set
  await redis.zadd('leaderboard', 100, 'user:1', 200, 'user:2');
  const leaderboard = await redis.zrevrange('leaderboard', 0, -1, 'WITHSCORES');
  console.log(leaderboard);
}

redisExample().catch(console.error);

Pipeline 批量操作

javascript
// 使用 Pipeline 减少网络往返
const pipeline = redis.pipeline();
pipeline.set('key1', 'value1');
pipeline.set('key2', 'value2');
pipeline.get('key1');
const results = await pipeline.exec();
console.log(results);

Pub/Sub 发布订阅

javascript
// 订阅者
const subscriber = new Redis();
subscriber.subscribe('news', (err, count) => {
  console.log(`订阅了 ${count} 个频道`);
});

subscriber.on('message', (channel, message) => {
  console.log(`频道 ${channel} 收到消息: ${message}`);
});

// 发布者
const publisher = new Redis();
publisher.publish('news', 'Hello Redis Pub/Sub!');

实战场景

1. 会话存储

javascript
const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
  store: new RedisStore({ client: redis }),
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000
  }
}));

2. 缓存热点数据

javascript
async function getUser(id) {
  const cacheKey = `user:${id}`;
  
  // 先查缓存
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }
  
  // 缓存未命中,查数据库
  const user = await db.users.findByPk(id);
  
  // 更新缓存
  if (user) {
    await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
  }
  
  return user;
}

3. 限流

javascript
async function rateLimit(key, limit, windowMs) {
  const current = Date.now();
  const windowStart = current - windowMs;
  
  // 移除窗口外的记录
  await redis.zremrangebyscore(key, 0, windowStart);
  
  // 获取当前窗口内的请求数
  const count = await redis.zcard(key);
  
  if (count >= limit) {
    return false; // 超过限制
  }
  
  // 添加当前请求
  await redis.zadd(key, current, current);
  
  // 设置过期时间
  await redis.expire(key, Math.ceil(windowMs / 1000));
  
  return true; // 未超过限制
}

// 使用
app.use(async (req, res, next) => {
  const ip = req.ip;
  const key = `rate_limit:${ip}`;
  
  const allowed = await rateLimit(key, 60, 60000); // 1 分钟 60 次请求
  
  if (!allowed) {
    return res.status(429).json({ message: 'Too Many Requests' });
  }
  
  next();
});

4. 分布式锁

javascript
async function withLock(key, expireTime, callback) {
  const value = Date.now().toString();
  
  try {
    // 获取锁
    const acquired = await redis.set(key, value, 'NX', 'EX', expireTime);
    
    if (!acquired) {
      throw new Error('Could not acquire lock');
    }
    
    // 执行回调
    return await callback();
  } finally {
    // 释放锁
    const script = `
      if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
      else
        return 0
      end
    `;
    await redis.eval(script, 1, key, value);
  }
}

// 使用
await withLock('order:123', 10, async () => {
  // 处理订单,保证并发安全
  await processOrder(123);
});

面试高频问题

1. Redis 为什么这么快?

回答思路

  • 内存存储:数据全部在内存,读写速度快
  • 单线程模型:避免线程切换和锁竞争
  • I/O 多路复用:高效处理并发连接
  • 精简数据结构:底层实现优化,占用空间小
  • 网络模型:非阻塞 I/O,响应迅速

2. Redis 有哪些数据结构?各有什么用途?

回答思路

  • String:缓存、计数器、分布式锁
  • List:消息队列、最新列表、栈/队列
  • Hash:对象存储、用户信息
  • Set:去重、交集并集差集
  • Sorted Set:排行榜、延迟队列、范围查询
  • HyperLogLog:基数统计
  • Bitmap:布尔值存储
  • Geo:地理位置
  • Stream:消息队列(支持消费组)

3. Redis 持久化有哪些方式?区别是什么?

回答思路

  • RDB:定期生成快照,恢复速度快,适合备份
  • AOF:记录所有写操作,数据更安全,恢复速度慢
  • 混合持久化:结合 RDB 和 AOF 的优点,Redis 4.0+ 支持

4. 如何解决 Redis 缓存穿透、缓存击穿、缓存雪崩?

回答思路

  • 缓存穿透:布隆过滤器、空值缓存
  • 缓存击穿:互斥锁、逻辑过期、永不过期
  • 缓存雪崩:随机过期时间、多级缓存、限流降级

5. 如何实现 Redis 分布式锁?

回答思路

  • 基本实现:SET key value NX EX expireTime
  • 释放锁:使用 Lua 脚本保证原子性
  • Redlock 算法:多实例场景,提高可靠性
  • 注意事项:锁过期时间、死锁、误释放

6. Redis 集群有哪些模式?

回答思路

  • 主从复制:数据备份、负载均衡
  • 哨兵模式:监控、自动故障转移
  • Redis Cluster:数据分片、水平扩展

7. Redis 内存淘汰策略有哪些?

回答思路

  • volatile-lru:从过期键中删除最近最少使用的
  • allkeys-lru:从所有键中删除最近最少使用的
  • volatile-ttl:从过期键中删除剩余 TTL 最小的
  • volatile-random:从过期键中随机删除
  • allkeys-random:从所有键中随机删除
  • noeviction:不删除键,返回错误

8. Redis 与 Memcached 有什么区别?

回答思路

  • 数据结构:Redis 支持多种数据结构,Memcached 只支持字符串
  • 持久化:Redis 支持 RDB 和 AOF,Memcached 不支持
  • 集群:Redis 有官方集群方案,Memcached 需要客户端实现
  • 内存管理:Redis 使用自己的内存分配器,Memcached 使用 slab 分配
  • 性能:单线程 vs 多线程,各有优势

延伸阅读

官方文档

书籍

  • 《Redis 设计与实现》by 黄健宏
  • 《Redis 实战》by Josiah L. Carlson
  • 《Redis 深度历险》by 钱文品

在线资源

工具

用心学习,用代码说话 💻