Redis

Redis

你可以使用 @adonisjs/redis 软件包在 AdonisJS 应用中使用 Redis。该软件包是对 ioredis 的一层轻量封装,在 Pub/Sub(发布/订阅)方面提供了更好的开发体验,并能自动管理多个 redis 连接。

安装

使用以下命令安装并配置该软件包:

node ace add @adonisjs/redis
  1. 使用检测到的包管理器安装 @adonisjs/redis 软件包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
    providers: [
    // ...其他 providers
    () => import('@adonisjs/redis/redis_provider')
    ]
    }
  3. 创建 config/redis.ts 文件。该文件包含你的 redis 服务器的连接配置。

  4. 定义以下环境变量及其校验规则。

    REDIS_HOST=127.0.0.1
    REDIS_PORT=6379
    REDIS_PASSWORD=

配置

Redis 软件包的配置存储在 config/redis.ts 文件中。

另见:配置文件桩(stub)

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 接受的配置 完全一致。

通过 Socket 连接

你可以配置 Redis 使用 Unix socket 进行连接。在你的 Redis 配置对象中使用 path 属性,并提供该 socket 的文件系统路径。

import env from '#start/env'
import { defineConfig } from '@adonisjs/redis'
const redisConfig = defineConfig({
connection: 'main',
connections: {
main: {
path: env.get('REDIS_SOCKET_PATH'),
db: 0,
keyPrefix: '',
},
},
})
export default redisConfig

配置集群

如果你在连接配置中定义了一个主机数组,@adonisjs/redis 软件包将创建一个集群连接。例如:

const redisConfig = defineConfig({
connections: {
main: {
clusters: [
{ host: '127.0.0.1', port: 6380 },
{ host: '127.0.0.1', port: 6381 },
],
clusterOptions: {
scaleReads: 'slave',
slotsRefreshTimeout: 10 * 1000,
},
},
},
})

配置哨兵(sentinel)

你可以通过在连接配置中定义一个哨兵节点数组,将 redis 连接配置为使用哨兵。例如:

另见:IORedis 关于哨兵配置的文档

const redisConfig = defineConfig({
connections: {
main: {
sentinels: [
{ host: 'localhost', port: 26379 },
{ host: 'localhost', port: 26380 },
],
name: 'mymaster',
},
},
})

使用

你可以使用该软件包导出的 redis 服务来运行 redis 命令。该 redis 服务是一个使用你在 config/redis.ts 文件中定义的配置进行配置的单例对象。

查阅 ioredis 文档以查看可用的方法列表。因为我们是对 IORedis 的一层封装,所以命令 API 与之一致。

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'
await redis.quit('main') // 退出 main 连接
await redis.disconnect('main') // 强制退出 main 连接
import redis from '@adonisjs/redis/services/main'
const redisMain = redis.connection('main')
redisMain.quit() // 使用连接实例退出
redisMain.disconnect() // 使用连接实例强制退出

错误处理

Redis 连接可能在应用生命周期的任何时刻失败。因此,捕获错误并制定重试策略至关重要。

默认情况下,AdonisJS 会使用应用日志记录器记录 redis 连接错误,并在永久关闭连接之前重试十次。重试策略在 config/redis.ts 文件中为每个连接进行定义。

另见:IORedis 关于自动重连的文档

config/redis.ts
{
main: {
host: env.get('REDIS_HOST'),
port: env.get('REDIS_PORT'),
password: env.get('REDIS_PASSWORD', ''),
retryStrategy(times) {
return times > 10 ? null : times * 50
},
},
}

你可以使用 .doNotLogErrors 方法禁用默认的错误报告器。这样做会从 redis 连接上移除 error 事件监听器。

import redis from '@adonisjs/redis/services/main'
/**
* 禁用默认错误报告器
*/
redis.doNotLogErrors()
redis.on('connection', (connection) => {
/**
* 务必始终定义一个错误监听器。
* 否则应用将会崩溃
*/
connection.on('error', (error) => {
console.log(error)
})
})

发布/订阅(Pub/Sub)

Redis 需要多个连接才能向频道(channel)发布消息以及订阅频道。订阅者(subscriber)连接除了订阅新的频道/模式以及取消订阅之外,无法执行其他操作。

使用 @adonisjs/redis 软件包时,你无需手动创建订阅者连接;我们会替你处理。当你首次调用 subscribe 方法时,我们会自动创建一个新的订阅者连接。

import redis from '@adonisjs/redis/services/main'
redis.subscribe('user:add', function (message) {
console.log(message)
})

IORedis 与 AdonisJS 之间的 API 差异

使用 ioredis 时,你必须使用两个不同的 API 来订阅频道并监听新消息。而使用 AdonisJS 封装时,subscribe 方法会一并处理这两件事。

使用 IORedis

redis.on('message', (channel, messages) => {
console.log(message)
})
redis.subscribe('user:add', (error, count) => {
if (error) {
console.log(error)
}
})

使用 AdonisJS

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 方法订阅模式。与 subscribe 方法类似,它会创建(如果尚不存在)一个订阅者连接。

redis.psubscribe('user:*', (channel, message) => {
console.log(channel)
console.log(message)
})
redis.publish(
'user:add',
JSON.stringify({
id: 1,
username: 'virk',
})
)

取消订阅

你可以使用 unsubscribepunsubscribe 方法取消订阅频道或模式。

await redis.unsubscribe('user:add')
await redis.punsubscribe('user:*add*')

使用 Lua 脚本

你可以将 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 方法执行它。首先定义所有键(key),然后是参数(argument)。

redis.runCommand(
'release', // 命令名称
'jobs:completed', // key 1
'jobs:running', // key 2
'11023', // argv 1
100 // argv 2
)

同样的命令也可以在特定连接上执行。

redis.connection('jobs').runCommand(
'release', // 命令名称
'jobs:completed', // key 1
'jobs:running', // key 2
'11023', // argv 1
100 // argv 2
)

最后,你也可以使用某个特定的连接实例来定义命令。例如:

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 属性定义参数转换器和回复转换器。该 API 与 IORedis API 一致。

参数转换器
import redis from '@adonisjs/redis/services/main'
redis.Command.setArgumentTransformer('hmset', (args) => {
if (args.length === 2) {
if (args[1] instanceof Map) {
// utils 是 ioredis 的内部模块
return [args[0], ...utils.convertMapToArray(args[1])]
}
if (typeof args[1] === 'object' && args[1] !== null) {
return [args[0], ...utils.convertObjectToArray(args[1])]
}
}
return args
})
回复转换器
import redis from '@adonisjs/redis/services/main'
redis.Command.setReplyTransformer('hgetall', (result) => {
if (Array.isArray(result)) {
const obj = {}
for (let i = 0; i < result.length; i += 2) {
obj[result[i]] = result[i + 1]
}
return obj
}
return result
})

事件

以下是 Redis 连接实例发出的事件列表。

connect / subscriber:connect

当建立连接时发出该事件。subscriber:connect 事件在建立订阅者连接时发出。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('connect', () => {})
connection.on('subscriber:connect', () => {})
})

wait

当连接处于 wait 模式(因为配置中设置了 lazyConnect 选项)时发出该事件。执行第一条命令后,连接将从 wait 状态移出。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('wait', () => {})
})

ready / subscriber:ready

除非你在配置中启用了 enableReadyCheck 标志,否则该事件会在 connect 事件之后立即发出。如果启用了该标志,我们会等待 Redis 服务器报告其已准备好接受命令。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('ready', () => {})
connection.on('subscriber:ready', () => {})
})

error / subscriber:error

当无法连接到 redis 服务器时发出该事件。请参阅错误处理了解 AdonisJS 如何处理连接错误。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('error', () => {})
connection.on('subscriber:error', () => {})
})

close / subscriber:close

当连接关闭时发出该事件。根据重试策略的不同,IORedis 在发出 close 事件后可能会尝试重新建立连接。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('close', () => {})
connection.on('subscriber:close', () => {})
})

reconnecting / subscriber:reconnecting

当在 close 事件之后尝试重新连接到 redis 服务器时发出该事件。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('reconnecting', ({ waitTime }) => {
console.log(waitTime)
})
connection.on('subscriber:reconnecting', ({ waitTime }) => {
console.log(waitTime)
})
})

end / subscriber:end

当连接已关闭,且不会再尝试重新连接时发出该事件。它应该标志着连接生命周期的结束。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('end', () => {})
connection.on('subscriber:end', () => {})
})

node:added

当连接到一个新的集群节点时发出该事件(仅适用于集群实例)。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('node:added', () => {})
})

node:removed

当某个集群节点被移除时发出该事件(仅适用于集群实例)。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('node:removed', () => {})
})

node:error

当无法连接到某个集群节点时发出该事件(仅适用于集群实例)。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('node:error', ({ error, address }) => {
console.log(error, address)
})
})

subscription:ready / psubscription:ready

当在某个频道或模式上建立的订阅生效时发出该事件。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('subscription:ready', ({ count }) => {
console.log(count)
})
connection.on('psubscription:ready', ({ count }) => {
console.log(count)
})
})

subscription:error / psubscription:error

当无法订阅某个频道或模式时发出该事件。

import redis from '@adonisjs/redis/services/main'
redis.on('connection', (connection) => {
connection.on('subscription:error', () => {})
connection.on('psubscription:error', () => {})
})