Redis
你可以使用 @adonisjs/redis 软件包在 AdonisJS 应用中使用 Redis。该软件包是对 ioredis 的一层轻量封装,在 Pub/Sub(发布/订阅)方面提供了更好的开发体验,并能自动管理多个 redis 连接。
安装
使用以下命令安装并配置该软件包:
node ace add @adonisjs/redis
-
使用检测到的包管理器安装
@adonisjs/redis软件包。 -
在
adonisrc.ts文件中注册以下服务提供者。{providers: [// ...其他 providers() => import('@adonisjs/redis/redis_provider')]} -
创建
config/redis.ts文件。该文件包含你的 redis 服务器的连接配置。 -
定义以下环境变量及其校验规则。
REDIS_HOST=127.0.0.1REDIS_PORT=6379REDIS_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 连接配置为使用哨兵。例如:
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 文件中为每个连接进行定义。
{
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',
})
)
取消订阅
你可以使用 unsubscribe 和 punsubscribe 方法取消订阅频道或模式。
await redis.unsubscribe('user:add')
await redis.punsubscribe('user:*add*')
使用 Lua 脚本
你可以将 Lua 脚本注册为 redis 服务的命令,它们会被应用到所有连接上。
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', () => {})
})