官方插件

Redis

直接使用 Redis、定义类型安全的 Lua 脚本,或为 Cache 与 Rates 提供后端。

@pluxel/plugin-redis 目前只供 Pluxel 工作区使用,尚不是公开安装入口。完整边界见 Package 矩阵。

需要队列、Stream、Pub/Sub 或原子 Lua 操作时使用 Redis;只需要缓存或限频时,让业务依赖抽象能力,宿主选择 Redis 后端。

任务业务插件依赖本页入口
调用 Redis 命令或读写 StreamRedis直接使用 Redis
原子组合多个命令RedisTyped Lua scripts
共享缓存Cache作为 Cache backend
多进程共享限频额度Rates作为 Rates backend

直接使用 Redis

import {  } from '@pluxel/plugin-redis'
import { ,  } from '@pluxel/core'

@({ : 'Queue' })
export class  extends  {
	constructor(private readonly : ) {
		super()
	}

	(: string): <number> {
		return this..('queue')..('queue:jobs', )
	}
}

host 安装默认 provider:

以下 host 是 测试宿主,用于验证装配。应用入口按 添加插件 配置清单、配置记录和自动启动。

import { RedisPlugin } from '@pluxel/plugin-redis'

await host.commit((change) => {
	change.start(RedisPlugin, {
		initialConfig: {
			connections: [
				{
					id: 'queue',
					url: 'redis://127.0.0.1:6379',
					database: 0,
					connectTimeoutMs: 10_000,
					commandQueueMaxLength: 10_000,
					disableOfflineQueue: true,
					pingIntervalMs: 0,
				},
			],
		},
	})
	change.start(QueuePlugin)
})

上例需要本机 Redis 监听 127.0.0.1:6379。启动后调用 push(),返回值是写入后的队列长度;连接失败时先检查服务地址与启动错误,consumer 不会在 provider 未就绪时运行。

RedisConnection.client 的公开类型是 node-redis 的 standalone、Cluster 或 Sentinel client union。这个 capability 是 raw server access,不自动添加 caller prefix;key、channel、consumer group 和 stream 的 namespace 都是 consumer 自己定义的业务 contract。

部署配置入口

以下 schema 均从 @pluxel/plugin-redis 根入口导出,可直接交给 envBinding 或 fileBinding,无需 Workbench:

Plugin配置 schema部署字段
RedisPluginRedisConfigconnections,完整 JSON 数组
RedisCacheBackendPluginRedisCacheBackendConfigconnectionId、keyPrefix、scanCount、deleteBatchSize
RedisRatesBackendPluginRedisRatesBackendConfigconnectionId、keyPrefix

例如 envBinding(RedisPlugin, { config: { schema: RedisConfig, mapping: { connections: 'REDIS_CONNECTIONS' } } })。普通配置的优先级与锁定遵循 Host 部署绑定;Redis 中的缓存/限流数据不覆盖部署配置。Workbench PING 只做诊断,不是连接设置入口。凭据不能通过 URL 或普通环境配置绕过下面的限制。

默认 provider 的边界

默认 RedisPlugin 只创建 credential-free standalone 连接:

  • url 只能是没有 username、password、database path、query 或 hash 的 redis:// / rediss:// URL;
  • database 通过独立的非负整数 database 配置;
  • client name 自动使用 pluxel:<Plugin node reference>:<connection-id>;
  • catalog 限制为 1–64 个唯一 ID,启动时并行连接并通过 Map 做 O(1) 选择;
  • 任一 initial connect 超时或失败会回滚全部连接,让 lifecycle 失败并抛 RedisConnectionError;
  • stale injected Redis facade 由 Core generation gate 拒绝;RedisNotRunningError 保护当前 provider 尚未发布或已经撤销的 client holder,captured node-redis client 保留 node-redis 自己的 closed-client 错误;
  • stop、replacement、rollback 和 shutdown 会关闭或销毁连接。

disableOfflineQueue 默认为 true,让断线期间的 command 快速失败;commandQueueMaxLength 为 node-redis command queue 设置上限;pingIntervalMs: 0 表示不启用周期 PING。

认证、Sentinel、Cluster、自定义 TLS 或云平台 binding 不应把 secret 塞进普通 config。host 可以提供另一个 @Plugin(Redis) implementation,通过 Vault 或平台 secret binding 管理 client,并实现相同的 connection(id) / connectionIds() catalog contract。 custom provider 必须自己把连接清理注册到 lifecycle;consumer 和下面两个 backend adapter 不需要因此改变。

Typed Lua scripts

需要原子组合多个 Redis command 时,用 defineRedisScript() 把 keys、arguments 和返回值绑定成一个 definition。 arguments tuple 含必需成员时,调用必须提供该字段;允许空数组的参数契约(例如 readonly [] 或 readonly [] | readonly [string])可省略。类型不会解析 Lua 或代替运行时校验。

import { defineRedisScript } from '@pluxel/plugin-redis'

const Increment = defineRedisScript<readonly [string], readonly [string], number>({
	name: 'counter.increment',
	numberOfKeys: 1,
	source: `return redis.call('INCRBY', KEYS[1], ARGV[1])`,
	decode(reply) {
		const value = Number(reply)
		if (!Number.isSafeInteger(value)) throw new TypeError('Expected an integer')
		return value
	},
})

const increment = this.redis.connection('queue').scripts.use(Increment)
const next = await increment({ keys: ['counter'], arguments: ['2'] })

definition 在声明时被 trim/校验、计算 source SHA-1 并冻结。numberOfKeys 可选;设置后 runner 会在发请求前校验 key 数量。arguments 可省略,等价于空数组。

runner 先调用 EVALSHA,只在收到 NOSCRIPT 时自动回退 EVAL。因此 Redis restart、failover 或 SCRIPT FLUSH 后不需要重新注册。connection.scripts.use(definition) 对同一 owner、connection 和 definition 返回稳定 runner;一次性调用也可使用:

const value = await this.redis.connection('queue').scripts.run(Increment, {
	keys: ['counter'],
	arguments: ['1'],
})

readOnly: true 会切换为 Redis 7+ 的 EVALSHA_RO / EVAL_RO。decode 失败会被包装为 RedisScriptDecodeError,原始异常保留在 cause;Redis command 自身的异常则原样 reject。

多连接 catalog

同一 host 需要 cache、queue、session 等连接时,在 RedisPlugin.connections 中声明稳定 ID,再由 consumer 使用 redis.connection(id) 选择。Catalog 是 provider-owned、配置驱动且有界的领域 collection,不是可变全局 service locator;重复选择 会返回 owner-bound handle,consumer 或 provider 停止后旧 handle 会被撤销。

所有配置连接属于同一个原子 generation。一个连接初始化失败会回滚整个 catalog;需要独立启停、故障隔离或扩缩容的 Redis 服务, 应使用独立进程或 host。

作为 Cache backend

import { CachePlugin } from '@pluxel/plugin-cache'
import { RedisCacheBackendPlugin, RedisPlugin } from '@pluxel/plugin-redis'

await host.commit((change) => {
	change.start(RedisPlugin, {
		initialConfig: {
			connections: [{ id: 'cache', url: 'redis://127.0.0.1:6379' }],
		},
	})
	change.start(RedisCacheBackendPlugin, {
		initialConfig: {
			connectionId: 'cache',
			keyPrefix: 'pluxel:cache:',
			scanCount: 200,
			deleteBatchSize: 200,
		},
	})
	change.start(CachePlugin)
	change.start(AccountsPlugin)
})

业务插件依赖 Cache,而不是 Redis。adapter 的关键行为是:

  • 用 Lua 原子读取 value 与 PTTL;undefined 只表示 miss,null 是合法 value;
  • ttlMs: 0 表示无 expiry,正值使用 Redis PX;
  • 以版本头和 Node v8.serialize() 编码 structured value,支持 BigInt、Date、Buffer、Map/Set;
  • clear(prefix) 使用 cursor SCAN 和有界 UNLINK,并正确转义 glob 元字符;
  • Cluster 会逐个 master 扫描,并逐 key unlink 以避免 CROSSSLOT;
  • single-flight、scope 与 caller owner envelope 仍由 CachePlugin 负责。

keyPrefix 只是 Redis keyspace 隔离,不是 cache scope。它必须是 well-formed Unicode。

作为 Rates backend

import { RatesPlugin } from '@pluxel/plugin-rates'
import { RedisPlugin, RedisRatesBackendPlugin } from '@pluxel/plugin-redis'

await host.commit((change) => {
	change.start(RedisPlugin, {
		initialConfig: {
			connections: [{ id: 'rates', url: 'redis://127.0.0.1:6379' }],
		},
	})
	change.start(RedisRatesBackendPlugin, {
		initialConfig: {
			connectionId: 'rates',
			keyPrefix: 'pluxel:rates:',
		},
	})
	change.start(RatesPlugin)
	change.start(MessagingPlugin)
})

adapter 为四种 Rates algorithm 分别使用静态、单 key Lua script。它读取 Redis TIME,在一次调用内校验完整 resolved policy、owner address、状态格式并更新 TTL。物理 key 使用 canonical request 的 SHA-256 digest,不暴露 raw identity;policy 不参与 key,因此同一 identity 的 policy 冲突可被检测。

Rates keyspace 必须由 adapter 独占,不能由其他 writer 修改。多个实例共享 Redis 后会得到同一个原子判定;这正是它与进程内 memory backend 的主要区别。

运行时失败原则

Workbench-enabled host 会用一张固定 Content 展示 bounded connection rows,并允许按 ID 执行 transient PING。动态 connection 数量不会增加 entry、RPC root 或 socket;Content 不暴露 endpoint、任意 command 或 key browser。Workbench disabled 时不会注册额外 状态 listener。

连接建立失败属于 lifecycle failure,required dependents 会被阻塞。调用期间的 node-redis command error、script error 或 adapter error 属于调用事实,应保留 error/cause,让 consumer 或上层 transport 明确选择重试、降级或失败;不要吞掉 Redis error 后报告成功。

将缺失映射为业务 Result

Redis 保留原生 node-redis API。只有领域确定“没有这个 key”需要调用方分支时,才在 consumer 转成 Result。 下面读取应用自己管理的草稿文本,使用 default connection;空字符串仍是成功值。

import { ,  } from '@pluxel/core'
import { ,  } from '@pluxel/core/better-result'
import {  } from '@pluxel/plugin-redis'

export class  extends ('DraftNotFound')<{ : string }> {}

@()
export class  extends  {
	constructor(private readonly : ) {
		super()
	}

	async (: string): <<string, >> {
		const  = await this..()..(`drafts:${}`)
		if ( === null) return .(new ({  }))
		if (typeof  !== 'string') throw new ('Expected UTF-8 draft text from Redis')
		return .()
	}
}

调用方在 read() 返回 Err 时可展示“草稿不存在”,Ok 时显示 value。连接失败、权限错误、错误 Redis type 和 generation 撤回继续 reject,不能降级为不存在。不要按 message 猜测 Redis 故障。 Cache/Rates adapter 沿用各自的 miss/decision 契约,见缓存示例与限流示例。

可执行示例与回归测试。

最后更新于

本页目录