Redis
本指南介绍 AdonisJS 应用中的 Redis 集成。你将学习如何:
- 安装并配置该包
- 执行 Redis 命令
- 管理多个连接
- 使用 Pub/Sub 消息传递
- 处理连接错误
- 配置集群和哨兵(sentinels)
概述
@adonisjs/redis 包是 ioredis(一个 Node.js Redis 客户端)之上的轻量封装,在 Pub/Sub 和多个 Redis 连接的自动管理方面提供了更好的开发体验。
你可以使用 Redis 进行缓存、会话存储、任务队列、限流和实时消息传递。该包提供了一个干净的 API 来执行 Redis 命令、管理多个命名连接,以及订阅 Pub/Sub 频道,而无需手动管理订阅者连接。
安装
使用以下命令安装并配置该包:
node ace add @adonisjs/redis
查看 add 命令执行的步骤
-
使用检测到的包管理器安装
@adonisjs/redis包。 -
在
adonisrc.ts文件中注册以下服务提供者。{ providers: [ // ...其他 providers () => import('@adonisjs/redis/redis_provider') ] } -
创建带有 Redis 服务器连接配置的
config/redis.ts文件。 -
定义以下环境变量及其验证规则。
REDIS_HOST=127.0.0.1 REDIS_PORT=6379 REDIS_PASSWORD=
配置
Redis 包的配置存储在 config/redis.ts 文件中。
另请参阅: 配置文件模板
import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: 'main',
connections: {
main: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD', ''),
db: 0,
keyPrefix: '',
},
},
})
export default redisConfig
connection
connection 属性定义默认使用哪个连接。当你运行 Redis 命令而没有显式选择连接时,它们会针对默认连接执行。
connections
connections 属性是多个命名连接的集合。你可以在此对象中定义一个或多个连接,并使用 redis.connection() 方法在它们之间切换。
每个命名连接的配置与 ioredis 接受的配置 完全相同。
通过 Unix socket 连接
你可以配置 Redis 使用 Unix socket 进行本地连接。使用 path 属性指定 socket 文件的位置。
import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: 'main',
connections: {
main: {
/**
* Unix socket 文件的路径。
* 使用 socket 连接时移除 host 和 port。
*/
path: env.get('REDIS_SOCKET_PATH'),
db: 0,
keyPrefix: '',
},
},
})
export default redisConfig
配置集群
当你在连接配置中定义一个集群节点数组时,@adonisjs/redis 包会创建一个
集群连接
。
集群 将数据分布到多个 Redis 节点上,以实现水平扩展和高可用性。当你需要扩展到单台服务器的内存容量之外,或希望对数据进行跨节点自动分片时,使用集群。
import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: 'main',
connections: {
main: {
// highlight-start
clusters: [
{ host: '127.0.0.1', port: 6380 },
{ host: '127.0.0.1', port: 6381 },
],
clusterOptions: {
scaleReads: 'slave',
slotsRefreshTimeout: 10 * 1000,
},
// highlight-end
},
},
})
export default redisConfig
clusters
一个集群节点地址数组。每个节点应指定 host 和 port。连接到初始节点后,该包会自动发现所有集群节点。
clusterOptions
用于控制行为的集群特定选项。
常用选项:
scaleReads:如何分配读操作('master'、'slave'或'all')slotsRefreshTimeout:刷新集群槽(slot)信息的频率(毫秒)
有关选项的完整列表,请参阅 ioredis 集群文档 。
配置哨兵
哨兵(Sentinels) 通过自动故障转移提供高可用性。哨兵节点监控你的主服务器和副本服务器,并在主服务器失败时自动将一个副本提升为主服务器。
你可以通过在连接配置中定义一个哨兵节点数组,将 Redis 连接配置为使用哨兵。
另请参阅: IORedis 哨兵配置文档
import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: 'main',
connections: {
main: {
// highlight-start
sentinels: [
{ host: 'localhost', port: 26379 },
{ host: 'localhost', port: 26380 },
],
name: 'mymaster',
// highlight-end
password: env.get('REDIS_PASSWORD', ''),
db: 0,
},
},
})
export default redisConfig
sentinels
一个哨兵节点地址数组。哨兵会自动检测当前哪个服务器是主服务器,并相应地重定向连接。
name
在你的哨兵中配置的主服务器组(master group)的名称。这必须与哨兵配置匹配。
用法
你可以使用该包导出的 redis 服务来执行 Redis 命令。redis 服务是一个使用你的 config/redis.ts 文件中的设置配置的单例实例。
命令 API 与 ioredis 完全相同。请查阅 ioredis 文档以查看可用方法的完整列表。
import redis from '@adonisjs/redis/services/main'
await redis.set('username', 'virk')
const username = await redis.get('username')
在连接之间切换
使用 redis 服务执行的命令是针对配置文件中定义的默认连接调用的。你可以通过先获取特定连接的实例,在该连接上执行命令。
.connection() 方法会为进程的生命周期创建并缓存一个连接实例。后续调用会返回同一个缓存的实例。
import redis from '@adonisjs/redis/services/main'
/**
* 获取连接实例
*/
const redisMain = redis.connection('main')
await redisMain.set('username', 'virk')
const username = await redisMain.get('username')
退出连接
连接是长生命周期的,每次调用 .connection() 方法时你都会得到同一个实例。你可以使用 quit 方法优雅地退出连接,或使用 disconnect 方法立即强制关闭。
import redis from '@adonisjs/redis/services/main'
/**
* 优雅地退出 main 连接
*/
await redis.quit('main')
/**
* 立即强制退出 main 连接
*/
await redis.disconnect('main')
你也可以直接使用连接实例退出。
import redis from '@adonisjs/redis/services/main'
const redisMain = redis.connection('main')
/**
* 使用连接实例退出
*/
await redisMain.quit()
/**
* 使用连接实例强制退出
*/
await redisMain.disconnect()
错误处理
Redis 连接可能在应用生命周期的任何时刻失败。如果没有适当的错误处理,连接失败会以未处理的 promise 拒绝使你的应用崩溃。
默认情况下,AdonisJS 使用应用日志记录 Redis 连接错误,并在永久关闭之前重试连接最多 10 次。重试策略使用指数退避(exponential backoff)来平衡从短暂中断中恢复与不在长时间故障上浪费资源。
重试策略在你的配置中为每个连接定义。
另请参阅: IORedis 自动重连文档
{
main: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD', ''),
// highlight-start
/**
* 每次连接尝试失败时调用。
* 返回 null 以停止重试。
* 返回一个数字(毫秒)以在该延迟后重试。
*/
retryStrategy(times) {
// 10 次尝试后停止
if (times > 10) {
return null
}
// 指数退避:50ms、100ms、150ms 等
return times * 50
},
// highlight-end
},
}
如果没有适当的错误处理,Redis 连接失败会使你的应用崩溃。默认的重试策略在放弃之前会尝试重连 10 次。对于生产部署,请根据你的可用性要求自定义重试策略,并监控 Redis 连接的健康状况。
为生产环境自定义:
retryStrategy(times) {
// 生产环境重试更多次
if (times > 20) {
return null
}
// 指数退避,最大 5 秒
return Math.min(times * 50, 5000)
}
你可以使用 .doNotLogErrors() 方法禁用默认的错误报告器。这会移除 Redis 连接上的 error 事件监听器。
import redis from '@adonisjs/redis/services/main'
/**
* 禁用默认错误报告器
*/
redis.doNotLogErrors()
redis.on('connection', (connection) => {
/**
* 始终定义一个 error 监听器以防止崩溃。
* 如果没有它,未处理的错误会使你的应用崩溃。
*/
connection.on('error', (error) => {
console.log(error)
})
})
Pub/Sub
Pub/Sub(发布/订阅)是一种消息传递模式,发布者向频道发送消息而无需知道谁在接收,订阅者监听频道而无需知道谁在发送。这种解耦使 Pub/Sub 非常适合通知、实时更新和聊天系统等实时特性。
Redis 的 Pub/Sub 需要多个连接。订阅者连接除了订阅和取消订阅之外,无法执行常规的 Redis 操作。当你首次调用 subscribe 方法时,该包会自动为你创建一个订阅者连接。
订阅频道
subscribe 方法同时处理订阅频道和监听消息。这与 ioredis API 不同,在后者中你需要使用单独的方法。
import redis from '@adonisjs/redis/services/main'
redis.subscribe('user:add', function (message) {
console.log(message)
})
你可以使用 options 参数处理订阅生命周期事件。
redis.subscribe(
'user:add',
(message) => {
console.log(message)
},
{
/**
* 订阅失败时调用
*/
onError(error) {
console.log(error)
},
/**
* 订阅建立时调用。
* count 是活跃订阅的总数。
*/
onSubscription(count) {
console.log(count)
},
}
)
与 IORedis 的 API 差异
直接使用 ioredis 时,你需要使用两个不同的 API 来订阅频道和监听消息。AdonisJS 的封装将这两者合并为一个便捷的方法。
redis.on('message', (channel, message) => {
console.log(message)
})
redis.subscribe('user:add', (error, count) => {
if (error) {
console.log(error)
}
})
redis.subscribe(
'user:add',
(message) => {
console.log(message)
},
{
onError(error) {
console.log(error)
},
onSubscription(count) {
console.log(count)
},
}
)
发布消息
你可以使用 publish 方法发布消息。该方法接受频道名称作为第一个参数,消息数据作为第二个参数。
redis.publish(
'user:add',
JSON.stringify({
id: 1,
username: 'virk',
})
)
确保在发布消息之前先订阅。在订阅建立之前发布的消息会丢失,因为没有订阅者来接收它们。
订阅模式
你可以使用带通配符的 psubscribe 方法订阅频道模式。回调同时接收频道名称和消息。
redis.psubscribe('user:*', (channel, message) => {
console.log(channel)
console.log(message)
})
redis.publish(
'user:add',
JSON.stringify({
id: 1,
username: 'virk',
})
)
取消订阅
你可以使用 unsubscribe 和 punsubscribe 方法取消订阅频道或模式。
await redis.unsubscribe('user:add')
await redis.punsubscribe('user:*add*')
使用 Lua 脚本
Lua 脚本 允许你在 Redis 服务器上以原子方式执行复杂操作。当你需要多个 Redis 命令要么一起成功要么一起失败时,这很有用,可避免竞态条件。
你可以将 Lua 脚本作为命令注册到 Redis 服务上。这些命令会自动应用到所有连接。
另请参阅: IORedis Lua 脚本文档
import redis from '@adonisjs/redis/services/main'
redis.defineCommand('release', {
numberOfKeys: 2,
lua: `
redis.call('zrem', KEYS[2], ARGV[1])
redis.call('zadd', KEYS[1], ARGV[2], ARGV[1])
return true
`,
})
定义命令后,你可以使用 runCommand 方法执行它。先传递键,然后传递参数。
redis.runCommand(
'release', // 命令名称
'jobs:completed', // 键 1
'jobs:running', // 键 2
'11023', // 参数 1
100 // 参数 2
)
你可以在特定连接上执行相同的命令。
redis.connection('jobs').runCommand(
'release',
'jobs:completed',
'jobs:running',
'11023',
100
)
你也可以使用 connection 事件为特定连接实例定义命令。
redis.on('connection', (connection) => {
if (connection.connectionName === 'jobs') {
connection.defineCommand('release', {
numberOfKeys: 2,
lua: `
redis.call('zrem', KEYS[2], ARGV[1])
redis.call('zadd', KEYS[1], ARGV[2], ARGV[1])
return true
`,
})
}
})
转换参数和回复
你可以使用 redis.Command 属性自定义参数发送到 Redis 的方式以及回复的解析方式。当你想使用 JavaScript 对象、Map 或自定义数据类型时,这很有用。
该 API 与 IORedis transformers API 完全相同。
参数转换器
参数转换器在将数据发送到 Redis 之前对其进行修改。
import redis from '@adonisjs/redis/services/main'
redis.Command.setArgumentTransformer('hmset', (args) => {
if (args.length === 2) {
/**
* 如果第二个参数是 Map,则转换为数组
*/
if (args[1] instanceof Map) {
return [args[0], ...utils.convertMapToArray(args[1])]
}
/**
* 如果第二个参数是对象,则转换为数组
*/
if (typeof args[1] === 'object' && args[1] !== null) {
return [args[0], ...utils.convertObjectToArray(args[1])]
}
}
return args
})
回复转换器
回复转换器修改从 Redis 接收到的数据。
import redis from '@adonisjs/redis/services/main'
redis.Command.setReplyTransformer('hgetall', (result) => {
if (Array.isArray(result)) {
const obj = {}
/**
* Redis 返回 [key1, value1, key2, value2]。
* 转换为对象格式。
*/
for (let i = 0; i < result.length; i += 2) {
obj[result[i]] = result[i + 1]
}
return obj
}
return result
})
测试
在测试使用 Redis 的代码时,你可以配置一个单独的连接用于测试,以将测试数据与开发或生产数据隔离。
首先,在你的 Redis 配置中定义一个测试连接。
import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: env.get('REDIS_CONNECTION', 'main'),
connections: {
main: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD', ''),
db: 0,
},
test: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD', ''),
db: 1, // 为测试使用不同的数据库
},
},
})
export default redisConfig
然后,在 .env.test 文件中运行测试时,将 REDIS_CONNECTION 环境变量设置为 test。
REDIS_CONNECTION=test
在每个测试后清理测试数据,以确保测试的隔离性。
import { test } from '@japa/runner'
import redis from '@adonisjs/redis/services/main'
test.group('Posts', (group) => {
group.each.teardown(async () => {
/**
* 清除测试数据库中的所有键
*/
await redis.flushdb()
})
test('caches post data', async ({ client }) => {
// 你的测试代码
})
})
事件
Redis 连接实例会发出以下事件。你可以使用 redis.on('connection') 方法监听这些事件。
import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('connect', () => {
console.log('Connected')
})
})
连接生命周期事件
connect / subscriber:connect
在建立连接时发出。subscriber:connect 事件在建立订阅者连接时发出。
redis.on('connection', (connection) => {
connection.on('connect', () => {})
connection.on('subscriber:connect', () => {})
})
wait
当由于配置中启用了 lazyConnect 选项,连接处于等待模式时发出。连接在执行第一条命令后退出等待模式。
redis.on('connection', (connection) => {
connection.on('wait', () => {})
})
ready / subscriber:ready
在 connect 事件之后,Redis 准备好接受命令时发出。如果你在配置中启用了 enableReadyCheck 标志,此事件会等待 Redis 服务器报告就绪。
redis.on('connection', (connection) => {
connection.on('ready', () => {})
connection.on('subscriber:ready', () => {})
})
error / subscriber:error
在无法连接到 Redis 服务器或发生连接错误时发出。有关正确的错误处理,请参阅 错误处理 。
close / subscriber:close
在连接关闭时发出。根据重试策略,IORedis 可能会在发出 close 事件后尝试重新建立连接。
redis.on('connection', (connection) => {
connection.on('close', () => {})
connection.on('subscriber:close', () => {})
})
reconnecting / subscriber:reconnecting
在连接关闭后尝试重新连接时发出。
redis.on('connection', (connection) => {
connection.on('reconnecting', ({ waitTime }) => {
console.log(waitTime)
})
connection.on('subscriber:reconnecting', ({ waitTime }) => {
console.log(waitTime)
})
})
end / subscriber:end
在连接已被永久关闭且不会再进行任何重新连接尝试时发出。
redis.on('connection', (connection) => {
connection.on('end', () => {})
connection.on('subscriber:end', () => {})
})
集群事件
node:added
在将新节点添加到集群时发出。仅适用于集群连接。
redis.on('connection', (connection) => {
connection.on('node:added', () => {})
})
node:removed
在从集群中移除节点时发出。仅适用于集群连接。
redis.on('connection', (connection) => {
connection.on('node:removed', () => {})
})
node:error
在无法连接到集群节点时发出。仅适用于集群连接。
redis.on('connection', (connection) => {
connection.on('node:error', ({ error, address }) => {
console.log(error, address)
})
})
订阅事件
subscription:ready / psubscription:ready
在频道或模式上建立订阅时发出。
redis.on('connection', (connection) => {
connection.on('subscription:ready', ({ count }) => {
console.log(count)
})
connection.on('psubscription:ready', ({ count }) => {
console.log(count)
})
})
subscription:error / psubscription:error
在无法订阅频道或模式时发出。
redis.on('connection', (connection) => {
connection.on('subscription:error', () => {})
connection.on('psubscription:error', () => {})
})