Skip to content

NOTE

Bun 的 Redis 客戶端支持 Redis 服務器 7.2 及以上版本。

Bun 提供了用於處理 Redis 數據庫的原生綁定,具有現代化、基於 Promise 的 API。該接口設計簡單且高性能,具有內置連接管理、完全類型化的響應和 TLS 支持。

ts
import { redis } from "bun";

// 設置鍵
await redis.set("greeting", "Hello from Bun!");

// 獲取鍵
const greeting = await redis.get("greeting");
console.log(greeting); // "Hello from Bun!"

// 遞增計數器
await redis.set("counter", 0);
await redis.incr("counter");

// 檢查鍵是否存在
const exists = await redis.exists("greeting");

// 刪除鍵
await redis.del("greeting");

入門

要使用 Redis 客戶端,你首先需要創建連接:

ts
import { redis, RedisClient } from "bun";

// 使用默認客戶端(從環境讀取連接信息)
// 默認使用 process.env.REDIS_URL
await redis.set("hello", "world");
const result = await redis.get("hello");

// 創建自定義客戶端
const client = new RedisClient("redis://username:password@localhost:6379");
await client.set("counter", "0");
await client.incr("counter");

默認情況下,客戶端按以下優先級順序從環境變量讀取連接信息:

  • REDIS_URL
  • VALKEY_URL
  • 如果未設置,默認為 "redis://localhost:6379"

連接生命周期

Redis 客戶端自動在後台處理連接:

ts
// 在執行命令之前不會建立連接
const client = new RedisClient();

// 第一個命令初始化連接
await client.set("key", "value");

// 連接保持打開以進行後續命令
await client.get("key");

// 完成後顯式關閉連接
client.close();

你也可以手動控制連接生命周期:

ts
const client = new RedisClient();

// 顯式連接
await client.connect();

// 運行命令
await client.set("key", "value");

// 完成後斷開連接
client.close();

基本操作

字符串操作

ts
// 設置鍵
await redis.set("user:1:name", "Alice");

// 獲取鍵
const name = await redis.get("user:1:name");

// 獲取鍵為 Uint8Array
const buffer = await redis.getBuffer("user:1:name");

// 刪除鍵
await redis.del("user:1:name");

// 檢查鍵是否存在
const exists = await redis.exists("user:1:name");

// 設置過期時間(秒)
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // 1 小時後過期

// 獲取剩余生存時間(秒)
const ttl = await redis.ttl("session:123");

數值操作

ts
// 設置初始值
await redis.set("counter", "0");

// 遞增 1
await redis.incr("counter");

// 遞減 1
await redis.decr("counter");

哈希操作

ts
// 在哈希中設置多個字段
await redis.hmset("user:123", ["name", "Alice", "email", "alice@example.com", "active", "true"]);

// 從哈希中獲取多個字段
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// 從哈希中獲取單個字段(直接返回值,如果缺失則返回 null)
const userName = await redis.hget("user:123", "name");
console.log(userName); // "Alice"

// 遞增哈希中的數值字段
await redis.hincrby("user:123", "visits", 1);

// 遞增哈希中的浮點字段
await redis.hincrbyfloat("user:123", "score", 1.5);

集合操作

ts
// 添加成員到集合
await redis.sadd("tags", "javascript");

// 從集合中移除成員
await redis.srem("tags", "javascript");

// 檢查成員是否存在於集合中
const isMember = await redis.sismember("tags", "javascript");

// 獲取集合的所有成員
const allTags = await redis.smembers("tags");

// 獲取隨機成員
const randomTag = await redis.srandmember("tags");

// 彈出(移除並返回)隨機成員
const poppedTag = await redis.spop("tags");

發布/訂閱

Bun 提供 Redis 發布/訂閱 協議的原生綁定。Bun 1.2.23 新增

基本用法

要開始發布消息,你可以在 publisher.ts 中設置發布者:

typescript
import { RedisClient } from "bun";

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

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

writer.close();

在另一個文件中,在 subscriber.ts 中創建訂閱者:

typescript
import { RedisClient } from "bun";

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

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

在一個 shell 中,運行你的訂閱者:

bash
bun run subscriber.ts

在另一個 shell 中,運行你的發布者:

bash
bun run publisher.ts

NOTE

訂閱模式接管 `RedisClient` 連接。具有訂閱的客戶端只能調用 `RedisClient.prototype.subscribe()`。換句話說,需要向 Redis 發送消息的應用程序需要一個單獨的連接,可通過 `.duplicate()` 獲取:
ts
import { RedisClient } from "bun";

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

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

發布

發布消息通過 publish() 方法完成:

typescript
await client.publish(channelName, message);

訂閱

Bun RedisClient 允許你通過 .subscribe() 方法訂閱頻道:

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

你可以通過 .unsubscribe() 方法取消訂閱:

typescript
await client.unsubscribe(); // 取消訂閱所有頻道。
await client.unsubscribe(channel); // 取消訂閱特定頻道。
await client.unsubscribe(channel, listener); // 取消訂閱特定監聽器。

高級用法

命令執行和流水線

客戶端自動流水線命令,通過批量發送多個命令並在響應到達時處理它們來提高性能。

ts
// 命令默認自動流水線
const [infoResult, listResult] = await Promise.all([redis.get("user:1:name"), redis.get("user:2:email")]);

要禁用自動流水線,你可以將 enableAutoPipelining 選項設置為 false

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

原始命令

當你需要使用沒有便捷方法的命令時,你可以使用 send 方法:

ts
// 運行任何 Redis 命令
const info = await redis.send("INFO", []);

// LPUSH 到列表
await redis.send("LPUSH", ["mylist", "value1", "value2"]);

// 獲取列表范圍
const list = await redis.send("LRANGE", ["mylist", "0", "-1"]);

send 方法允許你使用任何 Redis 命令,即使是客戶端中沒有專用方法的命令。第一個參數是命令名稱,第二個參數是字符串參數數組。

連接事件

你可以為連接事件注冊處理程序:

ts
const client = new RedisClient();

// 成功連接到 Redis 服務器時調用
client.onconnect = () => {
  console.log("已連接到 Redis 服務器");
};

// 從 Redis 服務器斷開連接時調用
client.onclose = error => {
  console.error("已從 Redis 服務器斷開連接:", error);
};

// 手動連接/斷開連接
await client.connect();
client.close();

連接狀態和監控

ts
// 檢查是否已連接
console.log(client.connected); // 布爾值指示連接狀態

// 檢查緩沖的數據量(字節)
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
  • PUNSUBSCRIBE

連接選項

創建客戶端時,你可以傳遞各種選項來配置連接:

ts
const client = new RedisClient("redis://localhost:6379", {
  // 連接超時(毫秒)(默認:10000)
  connectionTimeout: 5000,

  // 空閒超時(毫秒)(默認:0 = 無超時)
  idleTimeout: 30000,

  // 斷開連接時是否自動重連(默認:true)
  autoReconnect: true,

  // 最大重連嘗試次數(默認:10)
  maxRetries: 10,

  // 斷開連接時是否排隊命令(默認:true)
  enableOfflineQueue: true,

  // 是否自動流水線命令(默認:true)
  enableAutoPipelining: true,

  // TLS 選項(默認:false)
  tls: true,
  // 或者,提供自定義 TLS 配置:
  // 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 格式:

ts
// 標准 Redis URL
new RedisClient("redis://localhost:6379");

// 帶身份驗證
new RedisClient("redis://username:password@localhost:6379");

// 帶數據庫編號
new RedisClient("redis://localhost:6379/0");

// TLS 連接
new RedisClient("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");

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

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

錯誤處理

Redis 客戶端為不同場景拋出類型化錯誤:

ts
try {
  await redis.get("non-existent-key");
} catch (error) {
  if (error.code === "ERR_REDIS_CONNECTION_CLOSED") {
    console.error("與 Redis 服務器的連接已關閉");
  } else if (error.code === "ERR_REDIS_AUTHENTICATION_FAILED") {
    console.error("身份驗證失敗");
  } else {
    console.error("意外錯誤:", error);
  }
}

常見錯誤碼:

  • ERR_REDIS_CONNECTION_CLOSED - 與服務器的連接已關閉
  • ERR_REDIS_AUTHENTICATION_FAILED - 與服務器身份驗證失敗
  • ERR_REDIS_INVALID_RESPONSE - 從服務器收到無效響應

示例用例

緩存

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

  // 首先嘗試從緩存獲取
  const cachedUser = await redis.get(cacheKey);
  if (cachedUser) {
    return JSON.parse(cachedUser);
  }

  // 緩存中沒有,從數據庫獲取
  const user = await database.getUser(userId);

  // 存儲到緩存 1 小時
  await redis.set(cacheKey, JSON.stringify(user));
  await redis.expire(cacheKey, 3600);

  return user;
}

速率限制

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

  // 遞增計數器
  const count = await redis.incr(key);

  // 如果是窗口中的第一個請求則設置過期時間
  if (count === 1) {
    await redis.expire(key, windowSecs);
  }

  // 檢查是否超出限制
  return {
    limited: count > limit,
    remaining: Math.max(0, limit - count),
  };
}

會話存儲

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

  // 存儲會話及過期時間
  await redis.hmset(key, ["userId", userId.toString(), "created", Date.now().toString(), "data", JSON.stringify(data)]);
  await redis.expire(key, 86400); // 24 小時

  return sessionId;
}

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

  // 獲取會話數據
  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)。它高效管理連接並提供帶指數退避的自動重連。

客戶端支持命令流水線,這意味著可以發送多個命令而無需等待先前命令的回復。這在連續發送多個命令時顯著提高性能。

限制和未來計劃

我們計劃在將來版本中解決的 Redis 客戶端當前限制:

  • 事務 (MULTI/EXEC) 目前必須通過原始命令完成

不支持的功能:

  • Redis Sentinel
  • Redis Cluster

Bun學習網由www.bunjs.com.cn整理維護