ioredis 是 Node.js 生态中功能完善、使用广泛的 Redis 客户端,支持:

  • Redis 字符串、哈希、列表、集合、有序集合

  • Pipeline 和事务

  • 发布/订阅

  • Redis Cluster

  • Sentinel

  • Lua 脚本

  • 自动重连

  • Promise 和 async/await

本文将介绍 ioredis 的安装、连接配置以及常见数据操作,并重点演示:

  1. 查询所有 Key 对应的数据

  2. 一个 Key 存储多个数据

  3. 哈希、列表、集合等常用结构的实际使用


一、安装 ioredis

使用 npm:

npm install ioredis

使用 pnpm:

pnpm add ioredis

使用 yarn:

yarn add ioredis

二、创建 Redis 连接

1. 最简单的连接方式

假设 Redis 运行在本机 6379 端口:

const Redis = require('ioredis');

const redis = new Redis();

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

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

使用 async/await:

const Redis = require('ioredis');

const redis = new Redis();

async function main() {
  await redis.set('name', '张三');

  const name = await redis.get('name');
  console.log(name); // 张三
}

main().catch(console.error);

2. 指定 Redis 配置

const redis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  password: 'your-password',
  db: 0,
});

如果 Redis 使用 URL 连接:

const redis = new Redis('redis://:your-password@127.0.0.1:6379/0');

连接格式:

redis://[:password@]host:port/database

3. 使用环境变量

推荐在项目中通过环境变量配置 Redis。

.env:

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

代码:

require('dotenv').config();

const Redis = require('ioredis');

const redis = new Redis({
  host: process.env.REDIS_HOST,
  port: Number(process.env.REDIS_PORT || 6379),
  password: process.env.REDIS_PASSWORD || undefined,
  db: Number(process.env.REDIS_DB || 0),
});

三、字符串 String 操作

Redis 的 String 类型既可以存字符串,也可以存数字或 JSON 字符串。

1. 保存和读取字符串

await redis.set('username', '张三');

const username = await redis.get('username');

console.log(username); // 张三

2. 判断 Key 是否存在

const exists = await redis.exists('username');

console.log(exists); // 1 表示存在,0 表示不存在

3. 删除 Key

await redis.del('username');

4. 设置过期时间

await redis.set('verification_code', '123456', 'EX', 300);

上面的代码表示:

  • Key:verification_code

  • Value:123456

  • 过期时间:300 秒

也可以分开设置:

await redis.set('token', 'abc123');
await redis.expire('token', 3600);

5. 查询剩余过期时间

const ttl = await redis.ttl('token');

console.log(ttl);

返回值:

  • -1:Key 存在,但没有设置过期时间

  • -2:Key 不存在

  • 大于等于 0:剩余秒数

6. 保存 JSON 数据

Redis 本身不会直接保存 JavaScript 对象,需要转换为 JSON 字符串:

const user = {
  id: 1,
  name: '张三',
  age: 18,
};

await redis.set('user:1', JSON.stringify(user));

const userData = await redis.get('user:1');
const result = JSON.parse(userData);

console.log(result);

建议统一封装:

async function setJSON(key, value, expireSeconds) {
  const data = JSON.stringify(value);

  if (expireSeconds) {
    return redis.set(key, data, 'EX', expireSeconds);
  }

  return redis.set(key, data);
}

async function getJSON(key) {
  const data = await redis.get(key);

  if (!data) {
    return null;
  }

  return JSON.parse(data);
}

使用:

await setJSON('user:1', {
  id: 1,
  name: '张三',
});

const user = await getJSON('user:1');
console.log(user);

四、一个 Key 存储多个数据

“一个 Key 对应多个数据”有多种实现方式,应该根据数据特点选择 Redis 数据结构。

常用方式包括:

  1. Hash:一个 Key 保存多个字段

  2. List:一个 Key 保存有序数据列表

  3. Set:一个 Key 保存不重复的数据集合

  4. Sorted Set:一个 Key 保存带分数的数据集合

  5. JSON 字符串:一个 Key 保存完整对象


五、使用 Hash 保存多个字段

Hash 适合保存对象,例如用户信息、商品信息、配置信息等。

1. 保存多个字段

await redis.hset('user:1', {
  id: '1',
  name: '张三',
  age: '18',
  city: '北京',
});

也可以使用参数形式:

await redis.hset(
  'user:2',
  'id',
  '2',
  'name',
  '李四',
  'age',
  '20'
);

2. 查询全部字段

const user = await redis.hgetall('user:1');

console.log(user);

结果:

{
  id: '1',
  name: '张三',
  age: '18',
  city: '北京'
}

注意:Redis 中所有值默认都是字符串。

3. 查询某个字段

const name = await redis.hget('user:1', 'name');

console.log(name); // 张三

4. 查询多个字段

const fields = await redis.hmget(
  'user:1',
  'name',
  'age'
);

console.log(fields); // [ '张三', '18' ]

5. 修改字段

await redis.hset('user:1', 'age', '19');

6. 删除字段

await redis.hdel('user:1', 'city');

7. 判断字段是否存在

const exists = await redis.hexists('user:1', 'name');

console.log(exists); // 1 或 0

8. Hash 计数

await redis.hset('article:1', 'title', 'Redis 教程');
await redis.hincrby('article:1', 'viewCount', 1);
await redis.hincrby('article:1', 'viewCount', 5);

const article = await redis.hgetall('article:1');

console.log(article);

结果:

{
  title: 'Redis 教程',
  viewCount: '6'
}

六、使用 List 保存多个数据

List 是有序列表,适合实现:

  • 消息队列

  • 任务队列

  • 最新数据列表

  • 操作记录

  • 历史记录

1. 从右侧添加数据

await redis.rpush('tasks', 'task-1');
await redis.rpush('tasks', 'task-2');
await redis.rpush('tasks', 'task-3');

结果逻辑上为:

task-1, task-2, task-3

也可以一次添加多个:

await redis.rpush(
  'tasks',
  'task-4',
  'task-5',
  'task-6'
);

2. 从左侧添加数据

await redis.lpush('tasks', 'task-0');

结果:

task-0, task-1, task-2, task-3

3. 查询列表中的所有数据

const tasks = await redis.lrange('tasks', 0, -1);

console.log(tasks);

lrange 参数说明:

redis.lrange(key, start, stop)

其中:

  • 0:第一个元素

  • -1:最后一个元素

  • 0, -1:查询全部元素

4. 查询前几个数据

const tasks = await redis.lrange('tasks', 0, 2);

console.log(tasks);

表示查询下标 0 到 2 的元素。

5. 查询列表长度

const length = await redis.llen('tasks');

console.log(length);

6. 删除并获取数据

从左侧取出:

const task = await redis.lpop('tasks');

console.log(task);

从右侧取出:

const task = await redis.rpop('tasks');

console.log(task);

7. 使用 List 实现简单队列

生产任务:

await redis.rpush('job_queue', JSON.stringify({
  id: 1,
  type: 'email',
  to: 'test@example.com',
}));

消费任务:

const jobData = await redis.lpop('job_queue');

if (jobData) {
  const job = JSON.parse(jobData);
  console.log('处理任务:', job);
}

注意:lpop 在没有数据时会立即返回 null。如果需要阻塞等待,可以使用 BLPOP:

const result = await redis.blpop('job_queue', 10);

console.log(result);

返回结果类似:

['job_queue', '{"id":1,"type":"email"}']

七、使用 Set 保存不重复数据

Set 是无序且不重复的集合,适合保存:

  • 标签

  • 用户角色

  • 用户关注列表

  • 在线用户

  • 去重数据

1. 添加数据

await redis.sadd('user:1:tags', 'JavaScript');
await redis.sadd('user:1:tags', 'Node.js');
await redis.sadd('user:1:tags', 'Redis');

重复添加不会产生重复数据:

await redis.sadd('user:1:tags', 'Redis');

2. 查询全部数据

const tags = await redis.smembers('user:1:tags');

console.log(tags);

3. 判断是否包含某个值

const isMember = await redis.sismember(
  'user:1:tags',
  'Redis'
);

console.log(isMember); // 1 或 0

4. 删除集合元素

await redis.srem('user:1:tags', 'Redis');

5. 集合数量

const count = await redis.scard('user:1:tags');

console.log(count);

6. 集合交集

假设:

await redis.sadd('user:1:friends', 'user:2', 'user:3');
await redis.sadd('user:2:friends', 'user:3', 'user:4');

查询共同好友:

const result = await redis.sinter(
  'user:1:friends',
  'user:2:friends'
);

console.log(result); // [ 'user:3' ]

八、使用 Sorted Set 保存带分数的数据

Sorted Set,也叫 ZSet,集合中的每个成员都有一个分数,Redis 会根据分数排序。

适合实现:

  • 排行榜

  • 积分排名

  • 延迟任务

  • 热度排序

1. 添加排行榜数据

await redis.zadd(
  'scoreboard',
  100,
  'user:1',
  80,
  'user:2',
  120,
  'user:3'
);

2. 查询全部数据

按分数从低到高:

const result = await redis.zrange(
  'scoreboard',
  0,
  -1,
  'WITHSCORES'
);

console.log(result);

结果类似:

[
  'user:2', '80',
  'user:1', '100',
  'user:3', '120'
]

3. 按分数从高到低查询

const result = await redis.zrevrange(
  'scoreboard',
  0,
  -1,
  'WITHSCORES'
);

console.log(result);

4. 增加分数

await redis.zincrby(
  'scoreboard',
  10,
  'user:1'
);

5. 查询排名

从低到高排名:

const rank = await redis.zrank(
  'scoreboard',
  'user:1'
);

从高到低排名:

const rank = await redis.zrevrank(
  'scoreboard',
  'user:1'
);

九、查询 Redis 中所有 Key 对应的数据

这是实际开发中非常常见的需求,但需要注意:Redis 中不同类型的 Key,查询方式不同。

  • String 使用 GET

  • Hash 使用 HGETALL

  • List 使用 LRANGE

  • Set 使用 SMEMBERS

  • Sorted Set 使用 ZRANGE

1. 不推荐直接使用 KEYS

可以使用:

const keys = await redis.keys('*');

但是 KEYS * 会一次性扫描整个 Redis 数据库。当 Key 数量很多时,可能阻塞 Redis,影响线上服务。

因此,生产环境推荐使用 SCAN。


2. 使用 SCAN 查询所有 Key

async function getAllKeys(redis) {
  const keys = [];
  let cursor = '0';

  do {
    const result = await redis.scan(
      cursor,
      'MATCH',
      '*',
      'COUNT',
      100
    );

    cursor = result[0];
    keys.push(...result[1]);
  } while (cursor !== '0');

  return keys;
}

使用:

const keys = await getAllKeys(redis);

console.log(keys);

3. 查询所有 Key 的类型和值

async function getAllKeyValues(redis) {
  const result = [];
  let cursor = '0';

  do {
    const scanResult = await redis.scan(
      cursor,
      'MATCH',
      '*',
      'COUNT',
      100
    );

    cursor = scanResult[0];
    const keys = scanResult[1];

    for (const key of keys) {
      const type = await redis.type(key);
      let value;

      switch (type) {
        case 'string':
          value = await redis.get(key);
          break;

        case 'hash':
          value = await redis.hgetall(key);
          break;

        case 'list':
          value = await redis.lrange(key, 0, -1);
          break;

        case 'set':
          value = await redis.smembers(key);
          break;

        case 'zset':
          value = await redis.zrange(
            key,
            0,
            -1,
            'WITHSCORES'
          );
          break;

        default:
          value = null;
      }

      result.push({
        key,
        type,
        value,
      });
    }
  } while (cursor !== '0');

  return result;
}

使用:

const allData = await getAllKeyValues(redis);

console.dir(allData, { depth: null });

结果示例:

[
  {
    key: 'username',
    type: 'string',
    value: '张三'
  },
  {
    key: 'user:1',
    type: 'hash',
    value: {
      id: '1',
      name: '张三',
      age: '18'
    }
  },
  {
    key: 'tasks',
    type: 'list',
    value: ['task-1', 'task-2']
  }
]

4. 查询指定前缀的 Key

实际项目中建议使用 Key 前缀,例如:

user:1
user:2
user:3
article:1
article:2

查询所有用户相关 Key:

async function getKeysByPattern(redis, pattern) {
  const keys = [];
  let cursor = '0';

  do {
    const result = await redis.scan(
      cursor,
      'MATCH',
      pattern,
      'COUNT',
      100
    );

    cursor = result[0];
    keys.push(...result[1]);
  } while (cursor !== '0');

  return keys;
}

使用:

const userKeys = await getKeysByPattern(
  redis,
  'user:*'
);

console.log(userKeys);

十、Pipeline 批量操作

如果需要连续执行多个 Redis 命令,可以使用 Pipeline,减少网络往返次数。

const result = await redis
  .pipeline()
  .set('name', '张三')
  .set('age', '18')
  .get('name')
  .get('age')
  .exec();

console.log(result);

返回值格式:

[
  [null, 'OK'],
  [null, 'OK'],
  [null, '张三'],
  [null, '18']
]

也可以批量写入:

const pipeline = redis.pipeline();

for (let i = 1; i <= 1000; i++) {
  pipeline.set(`user:${i}`, JSON.stringify({
    id: i,
    name: `用户${i}`,
  }));
}

await pipeline.exec();

十一、事务操作 MULTI / EXEC

Redis 事务可以将多个命令作为一个整体执行。

const result = await redis
  .multi()
  .set('balance:user:1', 100)
  .incrby('balance:user:1', 50)
  .get('balance:user:1')
  .exec();

console.log(result);

需要注意:

  • Redis 事务不等同于数据库事务

  • 命令会按顺序执行

  • Redis 事务通常不会因为某一条命令失败而自动回滚


十二、发布与订阅

Redis 支持发布/订阅模式。

1. 发布端

const Redis = require('ioredis');

const publisher = new Redis();

async function publishMessage() {
  await publisher.publish(
    'news',
    JSON.stringify({
      title: 'Redis 消息',
      content: '这是一条消息',
    })
  );
}

publishMessage();

2. 订阅端

订阅连接通常应单独创建:

const Redis = require('ioredis');

const subscriber = new Redis();

subscriber.subscribe('news', (err, count) => {
  if (err) {
    console.error(err);
    return;
  }

  console.log(`成功订阅 ${count} 个频道`);
});

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

注意:一个连接进入订阅模式后,不能再正常执行普通 Redis 命令,因此发布和订阅通常使用不同的 Redis 实例。


十三、Lua 脚本

Lua 脚本可以将多个 Redis 操作放在服务端原子执行。

例如,实现库存扣减:

const script = `
  local stock = tonumber(redis.call('GET', KEYS[1]))

  if not stock or stock <= 0 then
    return 0
  end

  redis.call('DECR', KEYS[1])
  return 1
`;

const result = await redis.eval(
  script,
  1,
  'product:1:stock'
);

if (result === 1) {
  console.log('扣库存成功');
} else {
  console.log('库存不足');
}

参数说明:

redis.eval(script, keyCount, ...keysAndArguments)

十四、连接池与多个 Redis 实例

有时项目会分别使用不同 Redis 连接:

const Redis = require('ioredis');

const cacheRedis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  db: 0,
});

const queueRedis = new Redis({
  host: '127.0.0.1',
  port: 6379,
  db: 1,
});

如果使用订阅,也应单独创建连接:

const subscriberRedis = new Redis({
  host: '127.0.0.1',
  port: 6379,
});

十五、优雅关闭 Redis 连接

应用退出时,可以关闭 Redis 连接:

process.on('SIGTERM', async () => {
  await redis.quit();
  process.exit(0);
});

也可以使用:

redis.disconnect();

区别是:

  • quit():等待已经发送的命令执行完成后关闭

  • disconnect():立即断开连接

通常推荐使用 quit()。


十六、实用封装示例

下面是一个简单的 Redis 工具类:

const Redis = require('ioredis');

class RedisService {
  constructor(options = {}) {
    this.redis = new Redis(options);
  }

  async set(key, value, expireSeconds) {
    const data =
      typeof value === 'string'
        ? value
        : JSON.stringify(value);

    if (expireSeconds) {
      return this.redis.set(
        key,
        data,
        'EX',
        expireSeconds
      );
    }

    return this.redis.set(key, data);
  }

  async get(key) {
    const value = await this.redis.get(key);

    if (value === null) {
      return null;
    }

    try {
      return JSON.parse(value);
    } catch {
      return value;
    }
  }

  async delete(key) {
    return this.redis.del(key);
  }

  async has(key) {
    return (await this.redis.exists(key)) === 1;
  }

  async close() {
    await this.redis.quit();
  }
}

module.exports = RedisService;

使用:

const RedisService = require('./RedisService');

const cache = new RedisService({
  host: '127.0.0.1',
  port: 6379,
});

async function main() {
  await cache.set(
    'user:1',
    {
      id: 1,
      name: '张三',
    },
    3600
  );

  const user = await cache.get('user:1');

  console.log(user);

  await cache.close();
}

main().catch(console.error);

十七、Key 设计建议

建议使用统一的 Key 命名规则:

业务名称:数据类型:唯一标识

例如:

user:info:1
user:token:1
article:detail:100
article:views:100
order:status:20240001

常见设计:

user:1
user:1:tags
user:1:friends
article:1
article:1:comments

良好的 Key 设计便于:

  • 查询和管理

  • 按前缀扫描

  • 删除一组相关数据

  • 降低 Key 冲突风险

  • 提高代码可读性


十八、常见注意事项

1. 不要在生产环境频繁使用 KEYS

不推荐:

await redis.keys('*');

推荐:

await redis.scan('0', 'MATCH', '*', 'COUNT', 100);

2. Redis 返回的数据通常是字符串

例如:

const age = await redis.get('age');

console.log(typeof age); // string

如果需要数字:

const age = Number(await redis.get('age'));

3. 对象需要 JSON 序列化

await redis.set('user', JSON.stringify(user));

读取时:

const user = JSON.parse(await redis.get('user'));

4. 设置合理的过期时间

缓存数据通常应该设置过期时间:

await redis.set(
  'cache:article:1',
  JSON.stringify(article),
  'EX',
  3600
);

5. 大数据量操作要避免一次性读取

例如,下面的操作可能返回大量数据:

await redis.lrange('large-list', 0, -1);

可以分页读取:

const page = 0;
const pageSize = 20;

const items = await redis.lrange(
  'large-list',
  page * pageSize,
  (page + 1) * pageSize - 1
);

十九、常用命令速查

需求

ioredis 方法

保存字符串

set

获取字符串

get

删除 Key

del

判断 Key 存在

exists

设置过期时间

expire

查询剩余时间

ttl

保存 Hash

hset

获取 Hash 全部字段

hgetall

保存 List

lpush、rpush

获取 List

lrange

保存 Set

sadd

获取 Set

smembers

保存 Sorted Set

zadd

获取 Sorted Set

zrange

扫描 Key

scan

批量执行

pipeline

事务

multi、exec

发布消息

publish

订阅消息

subscribe


总结

ioredis 的使用重点是根据业务数据选择合适的 Redis 数据结构:

  • 单个值:使用 String

  • 一个 Key 对应多个字段:使用 Hash

  • 有序数据列表:使用 List

  • 不重复数据:使用 Set

  • 排行榜和分数排序:使用 Sorted Set

  • 查询所有 Key:使用 SCAN,不要在生产环境滥用 KEYS

  • 批量操作:使用 Pipeline

  • 原子操作:使用事务或 Lua 脚本

掌握这些基本操作后,就可以使用 ioredis 实现缓存、Session、排行榜、队列、计数器、消息通知等常见功能。