Bun

Redis 客户端

Bun 提供用于操作 Redis 数据库的原生绑定,具有现代的、基于 Promise 的 API。该接口设计简洁高效,内置连接管理、完全类型化的响应和 TLS 支持。**Bun v1.2.9 新增功能**

import { redis } from "bun";

// Set a key
await redis.set("greeting", "Hello from Bun!");

// Get a key
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"

// Increment a counter
await redis.set("counter", 0);
await redis.incr("counter");

// Check if a key exists
const exists = await redis.exists("greeting");

// Delete a key
await redis.del("greeting");

入门

要使用 Redis 客户端,您首先需要创建一个连接

import { redis, RedisClient } from "bun";

// Using the default client (reads connection info from environment)
// process.env.REDIS_URL is used by default
await redis.set("hello", "world");
const result = await redis.get("hello");

// Creating a custom client
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

默认情况下,客户端从以下环境变量(按优先级顺序)读取连接信息

  • REDIS_URL
  • 如果未设置,则默认为 ` "redis://:6379"`

连接生命周期

Redis 客户端在后台自动处理连接

// No connection is made until a command is executed
const client = new RedisClient();

// First command initiates the connection
await client.set("key", "value");

// Connection remains open for subsequent commands
await client.get("key");

// Explicitly close the connection when done
client.close();

您还可以手动控制连接生命周期

const client = new RedisClient();

// Explicitly connect
await client.connect();

// Run commands
await client.set("key", "value");

// Disconnect when done
client.close();

基本操作

字符串操作

// Set a key
await redis.set("user:1:name", "Alice");

// Get a key
const name = await redis.get("user:1:name");

// Get a key as Uint8Array
const buffer = await redis.getBuffer("user:1:name");

// Delete a key
await redis.del("user:1:name");

// Check if a key exists
const exists = await redis.exists("user:1:name");

// Set expiration (in seconds)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // expires in 1 hour

// Get time to live (in seconds)
const ttl = await redis.ttl("session:123");

数值操作

// Set initial value
await redis.set("counter", "0");

// Increment by 1
await redis.incr("counter");

// Decrement by 1
await redis.decr("counter");

哈希操作

// Set multiple fields in a hash
await redis.hmset("user:123", [
  "name",
  "Alice",
  "email",
  "alice@example.com",
  "active",
  "true",
]);

// Get multiple fields from a hash
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// Get single field from hash (returns value directly, null if missing)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"

// Increment a numeric field in a hash
await redis.hincrby("user:123", "visits", 1);

// Increment a float field in a hash
await redis.hincrbyfloat("user:123", "score", 1.5);

集合操作

// Add member to set
await redis.sadd("tags", "javascript");

// Remove member from set
await redis.srem("tags", "javascript");

// Check if member exists in set
const isMember = await redis.sismember("tags", "javascript");

// Get all members of a set
const allTags = await redis.smembers("tags");

// Get a random member
const randomTag = await redis.srandmember("tags");

// Pop (remove and return) a random member
const poppedTag = await redis.spop("tags");

发布/订阅

Bun 为 Redis 发布/订阅 协议提供原生绑定。**Bun 1.2.23 新增功能**

🚧 — Redis 发布/订阅功能处于实验阶段。虽然我们预计它将是 稳定的,但我们目前正在积极寻求反馈和改进领域。

基本用法

要开始发布消息,您可以在以下文件中设置发布者 publisher.ts:

publisher.ts
import { RedisClient } from "bun";

const writer = new RedisClient("redis://:6739");
await writer.connect();

writer.publish("general", "Hello everyone!");

writer.close();

在另一个文件中,在 `subscriber.ts` 中创建订阅者

subscriber.ts
import { RedisClient } from "bun";

const listener = new RedisClient("redis://:6739");
await listener.connect();

await listener.subscribe("general", (message, channel) => {
  console.log(`Received: ${message}`);
});

在一个 shell 中,运行您的订阅者

bun run subscriber.ts

在另一个 shell 中,运行您的发布者

bun run publisher.ts

注意: 订阅模式会接管 `RedisClient` 连接。一个 带有订阅的客户端只能调用 `RedisClient.prototype.subscribe()`。换句话说, 需要向 Redis 发送消息的应用程序需要一个单独的 连接,可以通过 `.duplicate()` 获取

import { RedisClient } from "bun";

const redis = new RedisClient("redis://:6379");
await redis.connect();
const subscriber = await redis.duplicate();

await subscriber.subscribe("foo", () => {});
await redis.set("bar", "baz");

发布

消息发布通过 `publish()` 方法完成

await client.publish(channelName, message);

订阅

Bun `RedisClient` 允许您通过 ` .subscribe()` 方法订阅频道

await client.subscribe(channel, (message, channel) => {});

您可以通过 `.unsubscribe()` 方法取消订阅

await client.unsubscribe(); // Unsubscribe from all channels.
await client.unsubscribe(channel); // Unsubscribe a particular channel.
await client.unsubscribe(channel, listener); // Unsubscribe a particular listener.

高级用法

命令执行和管道

客户端自动进行命令管道化,通过批量发送多个命令并在响应到达时处理它们来提高性能。

// Commands are automatically pipelined by default
const [infoResult, listResult] = await Promise.all([
  redis.get("user:1:name"),
  redis.get("user:2:email"),
]);

要禁用自动管道化,您可以将 `enableAutoPipelining` 选项设置为 `false`

const client = new RedisClient("redis://:6379", {
  enableAutoPipelining: false,
});

原始命令

当您需要使用没有便捷方法的命令时,可以使用 `send` 方法

// Run any Redis command
const info = await redis.send("INFO", []);

// LPUSH to a list
await redis.send("LPUSH", ["mylist", "value1", "value2"]);

// Get list range
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

`send` 方法允许您使用任何 Redis 命令,即使是客户端中没有专门方法的命令。第一个参数是命令名称,第二个参数是字符串参数数组。

连接事件

您可以注册连接事件的处理程序

const client = new RedisClient();

// Called when successfully connected to Redis server
client.onconnect = () => {
  console.log("Connected to Redis server");
};

// Called when disconnected from Redis server
client.onclose = error => {
  console.error("Disconnected from Redis server:", error);
};

// Manually connect/disconnect
await client.connect();
client.close();

连接状态和监控

// Check if connected
console.log(client.connected); // boolean indicating connection status

// Check amount of data buffered (in bytes)
console.log(client.bufferedAmount);

类型转换

Redis 客户端处理 Redis 响应的自动类型转换

  • 整数响应作为 JavaScript 数字返回
  • 批量字符串作为 JavaScript 字符串返回
  • 简单字符串作为 JavaScript 字符串返回
  • 空批量字符串作为 `null` 返回
  • 数组响应作为 JavaScript 数组返回
  • 错误响应会抛出带有相应错误代码的 JavaScript 错误
  • 布尔响应 (RESP3) 作为 JavaScript 布尔值返回
  • 映射响应 (RESP3) 作为 JavaScript 对象返回
  • 集合响应 (RESP3) 作为 JavaScript 数组返回

特定命令的特殊处理

  • `EXISTS` 返回布尔值而不是数字(1 变为 true,0 变为 false)
  • `SISMEMBER` 返回布尔值(1 变为 true,0 变为 false)

以下命令禁用自动管道化

  • AUTH
  • INFO
  • QUIT
  • EXEC
  • MULTI
  • WATCH
  • SCRIPT
  • SELECT
  • CLUSTER
  • DISCARD
  • UNWATCH
  • PIPELINE
  • SUBSCRIBE
  • UNSUBSCRIBE
  • UNPSUBSCRIBE

连接选项

创建客户端时,您可以传递各种选项来配置连接

const client = new RedisClient("redis://:6379", {
  // Connection timeout in milliseconds (default: 10000)
  connectionTimeout: 5000,

  // Idle timeout in milliseconds (default: 0 = no timeout)
  idleTimeout: 30000,

  // Whether to automatically reconnect on disconnection (default: true)
  autoReconnect: true,

  // Maximum number of reconnection attempts (default: 10)
  maxRetries: 10,

  // Whether to queue commands when disconnected (default: true)
  enableOfflineQueue: true,

  // Whether to automatically pipeline commands (default: true)
  enableAutoPipelining: true,

  // TLS options (default: false)
  tls: true,
  // Alternatively, provide custom TLS config:
  // tls: {
  //   rejectUnauthorized: true,
  //   ca: "path/to/ca.pem",
  //   cert: "path/to/cert.pem",
  //   key: "path/to/key.pem",
  // }
});

重新连接行为

当连接丢失时,客户端会自动尝试以指数退避的方式重新连接

  1. 客户端从一个小的延迟 (50ms) 开始,并在每次尝试时将其加倍
  2. 重新连接延迟上限为 2000ms(2 秒)
  3. 客户端尝试重新连接最多 `maxRetries` 次(默认值:10)
  4. 断开连接期间执行的命令会
    • 如果 `enableOfflineQueue` 为 true(默认),则排队
    • 如果 `enableOfflineQueue` 为 false,则立即拒绝

支持的 URL 格式

Redis 客户端支持各种 URL 格式

// Standard Redis URL
new RedisClient("redis://:6379");
new RedisClient("redis://:6379");

// With authentication
new RedisClient("redis://username:password@localhost:6379");

// With database number
new RedisClient("redis://:6379/0");

// TLS connections
new RedisClient("rediss://:6379");
new RedisClient("rediss://:6379");
new RedisClient("redis+tls://:6379");
new RedisClient("redis+tls://:6379");

// Unix socket connections
new RedisClient("redis+unix:///path/to/socket");
new RedisClient("redis+unix:///path/to/socket");

// TLS over Unix socket
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");

错误处理

Redis 客户端针对不同的场景抛出类型化错误

try {
  await redis.get("non-existent-key");
} catch (error) {
  if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
    console.error("Connection to Redis server was closed");
  } else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
    console.error("Authentication failed");
  } else {
    console.error("Unexpected error:", error);
  }
}

常见错误代码

  • `ERR_REDIS_CONNECTION_CLOSED` - 与服务器的连接已关闭
  • `ERR_REDIS_AUTHENTICATION_FAILED` - 无法通过服务器身份验证
  • `ERR_REDIS_INVALID_RESPONSE` - 从服务器收到无效响应

示例用例

缓存

async function getUserWithCache(userId) {
  const cacheKey = `user:${userId}`;

  // Try to get from cache first
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }

  // Not in cache, fetch from database
  const user = await database.getUser(userId);

  // Store in cache for 1 hour
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);

  return user;
}

速率限制

async function rateLimit(ip, limit = 100, windowSecs = 3600) {
  const key = `ratelimit:${ip}`;

  // Increment counter
  const count = await redis.incr(key);

  // Set expiry if this is the first request in window
  if (count === 1) {
    await redis.expire(key, windowSecs);
  }

  // Check if limit exceeded
  return {
    limited: count > limit,
    remaining: Math.max(0, limit - count),
  };
}

会话存储

async function createSession(userId, data) {
  const sessionId = crypto.randomUUID();
  const key = `session:${sessionId}`;

  // Store session with expiration
  await redis.hmset(key, [
    "userId",
    userId.toString(),
    "created",
    Date.now().toString(),
    "data",
    JSON.stringify(data),
  ]);
  await redis.expire(key, 86400); // 24 hours

  return sessionId;
}

async function getSession(sessionId) {
  const key = `session:${sessionId}`;

  // Get session data
  const exists = await redis.exists(key);
  if (!exists) return null;

  const [userId, created, data] = await redis.hmget(key, [
    "userId",
    "created",
    "data",
  ]);

  return {
    userId: Number(userId),
    created: Number(created),
    data: JSON.parse(data),
  };
}

实现说明

Bun 的 Redis 客户端使用 Zig 实现,并使用 Redis 序列化协议 (RESP3)。它有效地管理连接,并提供带指数退避的自动重连功能。

客户端支持命令管道化,这意味着可以发送多个命令而无需等待前一个命令的回复。这在连续发送多个命令时显著提高了性能。

RESP3 协议支持

Bun 的 Redis 客户端默认使用较新的 RESP3 协议,与 RESP2 相比,它提供了更多的数据类型和功能

  • 通过类型化错误提供更好的错误处理
  • 原生布尔响应
  • Map/字典响应(键值对象)
  • Set 响应
  • 双精度(浮点)值
  • 对大整数值支持 BigNumber

当连接到不支持 RESP3 的旧版本 Redis 服务器时,客户端会自动回退到兼容模式。

局限性和未来计划

我们计划在未来版本中解决的 Redis 客户端当前局限性

不支持的功能

  • Redis Sentinel
  • Redis Cluster