ioredis 是 Node.js 生态中功能完善、使用广泛的 Redis 客户端,支持:
Redis 字符串、哈希、列表、集合、有序集合
Pipeline 和事务
发布/订阅
Redis Cluster
Sentinel
Lua 脚本
自动重连
Promise 和 async/await
本文将介绍 ioredis 的安装、连接配置以及常见数据操作,并重点演示:
查询所有 Key 对应的数据
一个 Key 存储多个数据
哈希、列表、集合等常用结构的实际使用
一、安装 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_codeValue:
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 数据结构。
常用方式包括:
Hash:一个 Key 保存多个字段
List:一个 Key 保存有序数据列表
Set:一个 Key 保存不重复的数据集合
Sorted Set:一个 Key 保存带分数的数据集合
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 使用
GETHash 使用
HGETALLList 使用
LRANGESet 使用
SMEMBERSSorted 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 方法 |
|---|---|
保存字符串 |
|
获取字符串 |
|
删除 Key |
|
判断 Key 存在 |
|
设置过期时间 |
|
查询剩余时间 |
|
保存 Hash |
|
获取 Hash 全部字段 |
|
保存 List |
|
获取 List |
|
保存 Set |
|
获取 Set |
|
保存 Sorted Set |
|
获取 Sorted Set |
|
扫描 Key |
|
批量执行 |
|
事务 |
|
发布消息 |
|
订阅消息 |
|
总结
ioredis 的使用重点是根据业务数据选择合适的 Redis 数据结构:
单个值:使用 String
一个 Key 对应多个字段:使用 Hash
有序数据列表:使用 List
不重复数据:使用 Set
排行榜和分数排序:使用 Sorted Set
查询所有 Key:使用
SCAN,不要在生产环境滥用KEYS批量操作:使用 Pipeline
原子操作:使用事务或 Lua 脚本
掌握这些基本操作后,就可以使用 ioredis 实现缓存、Session、排行榜、队列、计数器、消息通知等常见功能。