Node.js 使用 Redis

Node.js与Redis天然契合,本课学习Node.js操作Redis。

Redis Node.js客户端

主流Node.js Redis客户端:

💡 推荐: 新项目使用ioredis或node-redis v4+。

ioredis

安装

BASH
npm install ioredis

基本连接

JAVASCRIPT
const Redis = require('ioredis');

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

// 测试连接
redis.ping().then(result => {
  console.log(result); // 'PONG'
});

字符串操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

// SET和GET
async function stringDemo() {
  // SET
  await redis.set('name', 'Alice');
  
  // GET
  const name = await redis.get('name');
  console.log(name); // 'Alice'
  
  // SET带过期时间
  await redis.set('session', 'data', 'EX', 3600); // 1小时后过期
  
  // MSET和MGET
  await redis.mset('key1', 'value1', 'key2', 'value2');
  const values = await redis.mget('key1', 'key2');
  console.log(values); // ['value1', 'value2']
  
  // INCR
  await redis.set('counter', 0);
  const count = await redis.incr('counter');
  console.log(count); // 1
  
  // DEL
  await redis.del('name');
}

stringDemo();

哈希操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function hashDemo() {
  // HSET和HGET
  await redis.hset('user:1', 'name', 'Alice');
  await redis.hset('user:1', 'age', 25);
  const name = await redis.hget('user:1', 'name');
  console.log(name); // 'Alice'
  
  // HMSET
  await redis.hmset('user:2', {
    name: 'Bob',
    age: 30,
    city: 'Beijing'
  });
  
  // HGETALL
  const user = await redis.hgetall('user:2');
  console.log(user); // { name: 'Bob', age: '30', city: 'Beijing' }
  
  // HKEYS和HVALS
  const keys = await redis.hkeys('user:2');
  const values = await redis.hvals('user:2');
  
  // HDEL
  await redis.hdel('user:2', 'city');
  
  // HINCRBY
  await redis.hincrby('user:2', 'age', 1);
}

hashDemo();

列表操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function listDemo() {
  // LPUSH和RPUSH
  await redis.lpush('mylist', 'value1', 'value2');
  await redis.rpush('mylist', 'value3');
  
  // LRANGE
  const list = await redis.lrange('mylist', 0, -1);
  console.log(list); // ['value2', 'value1', 'value3']
  
  // LPOP和RPOP
  const left = await redis.lpop('mylist');
  const right = await redis.rpop('mylist');
  
  // LLEN
  const length = await redis.llen('mylist');
}

listDemo();

集合操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function setDemo() {
  // SADD
  await redis.sadd('myset', 'a', 'b', 'c');
  
  // SMEMBERS
  const members = await redis.smembers('myset');
  console.log(members); // Set { 'a', 'b', 'c' }
  
  // SISMEMBER
  const exists = await redis.sismember('myset', 'a');
  console.log(exists); // 1 (true)
  
  // SREM
  await redis.srem('myset', 'a');
  
  // SINTER、SUNION、SDIFF
  await redis.sadd('set1', 'a', 'b', 'c');
  await redis.sadd('set2', 'b', 'c', 'd');
  const inter = await redis.sinter('set1', 'set2');
  console.log(inter); // Set { 'b', 'c' }
}

setDemo();

有序集合操作

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function zsetDemo() {
  // ZADD
  await redis.zadd('leaderboard', 100, 'player1', 200, 'player2', 150, 'player3');
  
  // ZRANGE
  const top = await redis.zrange('leaderboard', 0, -1, 'WITHSCORES');
  console.log(top); // ['player1', '100', 'player3', '150', 'player2', '200']
  
  // ZREVRANGE(从高到低)
  const topDesc = await redis.zrevrange('leaderboard', 0, 2, 'WITHSCORES');
  
  // ZSCORE
  const score = await redis.zscore('leaderboard', 'player1');
  console.log(score); // '100'
  
  // ZINCRBY
  await redis.zincrby('leaderboard', 50, 'player1');
  
  // ZREM
  await redis.zrem('leaderboard', 'player1');
}

zsetDemo();

发布订阅

JAVASCRIPT
const Redis = require('ioredis');

// 订阅者
const subscriber = new Redis();
subscriber.subscribe('news', 'sports');

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

// 发布者
const publisher = new Redis();
setInterval(() => {
  publisher.publish('news', `新闻消息 ${Date.now()}`);
}, 1000);

管道

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function pipelineDemo() {
  const pipeline = redis.pipeline();
  
  pipeline.set('key1', 'value1');
  pipeline.set('key2', 'value2');
  pipeline.set('key3', 'value3');
  pipeline.get('key1');
  
  const results = await pipeline.exec();
  console.log(results);
  // [ [ null, 'OK' ], [ null, 'OK' ], [ null, 'OK' ], [ null, 'value1' ] ]
}

pipelineDemo();

事务

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function transactionDemo() {
  const multi = redis.multi();
  
  multi.set('key1', 'value1');
  multi.set('key2', 'value2');
  multi.incr('counter');
  
  const results = await multi.exec();
  console.log(results);
}

transactionDemo();

连接池

ioredis内置连接池,无需额外配置:

JAVASCRIPT
const Redis = require('ioredis');

// 单个连接(自动连接池)
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  maxRetriesPerRequest: 3,
  enableReadyCheck: true
});

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

node-redis(官方客户端)

安装

BASH
npm install redis

基本使用

JAVASCRIPT
const { createClient } = require('redis');

async function main() {
  // 创建客户端
  const client = createClient({
    url: 'redis://localhost:6379'
  });
  
  // 连接
  await client.connect();
  
  // SET和GET
  await client.set('key', 'value');
  const value = await client.get('key');
  console.log(value); // 'value'
  
  // 哈希操作
  await client.hSet('user:1', 'name', 'Alice');
  const name = await client.hGet('user:1', 'name');
  
  // 列表操作
  await client.lPush('mylist', 'value1');
  const list = await client.lRange('mylist', 0, -1);
  
  // 集合操作
  await client.sAdd('myset', 'a', 'b', 'c');
  const members = await client.sMembers('myset');
  
  // 关闭连接
  await client.quit();
}

main();

发布订阅

JAVASCRIPT
const { createClient } = require('redis');

async function pubsub() {
  const subscriber = createClient();
  await subscriber.connect();
  
  // 订阅
  await subscriber.subscribe('news', (message) => {
    console.log(`收到消息:${message}`);
  });
  
  const publisher = createClient();
  await publisher.connect();
  
  // 发布
  await publisher.publish('news', 'Hello World');
}

pubsub();

实战案例

案例1:缓存装饰器

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

function cache(expire = 3600) {
  return function(target, propertyKey, descriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function(...args) {
      const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
      
      // 尝试从缓存获取
      const cached = await redis.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }
      
      // 执行原方法
      const result = await originalMethod.apply(this, args);
      
      // 存入缓存
      await redis.set(cacheKey, JSON.stringify(result), 'EX', expire);
      
      return result;
    };
    
    return descriptor;
  };
}

class UserService {
  @cache(300)
  async getUser(userId) {
    console.log(`查询数据库:userId=${userId}`);
    return { id: userId, name: `User${userId}` };
  }
}

案例2:分布式锁

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();
const { v4: uuidv4 } = require('uuid');

class DistributedLock {
  constructor(key, expire = 10) {
    this.key = `lock:${key}`;
    this.expire = expire;
    this.identifier = uuidv4();
  }
  
  async acquire() {
    const result = await redis.set(
      this.key,
      this.identifier,
      'NX',
      'EX',
      this.expire
    );
    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
    `;
    const result = await redis.eval(script, 1, this.key, this.identifier);
    return result === 1;
  }
}

// 使用锁
async function withLock(key, callback) {
  const lock = new DistributedLock(key);
  
  try {
    while (!(await lock.acquire())) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    
    return await callback();
  } finally {
    await lock.release();
  }
}

// 示例
await withLock('resource', async () => {
  console.log('执行业务逻辑');
});

案例3:限流器

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

async function rateLimit(key, limit = 10, period = 60) {
  const current = Math.floor(Date.now() / 1000);
  const windowStart = current - period;
  
  const results = await redis
    .multi()
    .zremrangebyscore(key, 0, windowStart)
    .zcard(key)
    .zadd(key, current, `${current}`)
    .expire(key, period)
    .exec();
  
  const count = results[1][1];
  return count < limit;
}

// 使用限流
async function apiHandler(userId) {
  const allowed = await rateLimit(`api:${userId}`, 10, 60);
  
  if (allowed) {
    console.log('请求允许');
    return { success: true };
  } else {
    console.log('请求拒绝');
    return { success: false, error: 'Rate limit exceeded' };
  }
}

案例4:消息队列

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

class MessageQueue {
  constructor(name) {
    this.name = `queue:${name}`;
  }
  
  async push(message) {
    await redis.rpush(this.name, JSON.stringify(message));
  }
  
  async pop(timeout = 0) {
    const result = await redis.blpop(this.name, timeout);
    if (result) {
      return JSON.parse(result[1]);
    }
    return null;
  }
  
  async size() {
    return await redis.llen(this.name);
  }
}

// 生产者
const queue = new MessageQueue('tasks');
await queue.push({ task: 'send_email', to: 'user@example.com' });

// 消费者
while (true) {
  const task = await queue.pop(5);
  if (task) {
    console.log('处理任务:', task);
  } else {
    break;
  }
}

错误处理

JAVASCRIPT
const Redis = require('ioredis');
const redis = new Redis();

redis.on('error', (error) => {
  console.error('Redis错误:', error);
});

redis.on('connect', () => {
  console.log('Redis连接成功');
});

redis.on('ready', () => {
  console.log('Redis准备就绪');
});

redis.on('close', () => {
  console.log('Redis连接关闭');
});

❓ 常见问题

Q ioredis和node-redis如何选择?
A ioredis功能更强大(集群、哨兵),node-redis是官方客户端。推荐ioredis。
Q ioredis是线程安全的吗?
A 是的。ioredis内置连接池,可以在多个地方共享实例。
Q 如何处理连接断开?
A ioredis会自动重连。可以监听error和connect事件。
Q 异步操作如何处理?
A ioredis所有操作返回Promise,使用async/await或.then()。
Q 如何实现连接池?
A ioredis内置连接池,无需额外配置。

📖 小节

📝 作业

  1. 基本操作: 使用ioredis实现字符串、哈希、列表的基本操作
  2. 管道: 使用管道批量设置1000个键,对比性能
  3. 发布订阅: 实现简单的发布订阅消息系统
  4. 实战: 实现一个简单的分布式锁或限流器

恭喜完成!

你已经完成了Redis教程的全部26课学习!接下来可以:

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏