執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

Redis

使用 Bun 原生 Redis 客戶端,基於 Promise 的 API

Bun 原生 Redis 客戶端提供基於 Promise 的 API,內建連線管理、完全型別化的響應以及 TLS 支援。

redis.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 客戶端,首先需要建立一個連線:

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 客戶端會自動在後臺管理連線:

redis.ts
// 未執行命令前不會連線
const client = new RedisClient();

// 第一次命令觸發連線建立
await client.set("key", "value");

// 連線保持開啟,供後續命令使用
await client.get("key");

// 使用結束後顯式關閉連線
client.close();

你也可以手動控制連線生命週期:

redis.ts
const client = new RedisClient();

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

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

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

基本操作

字串操作

redis.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); // 一小時後過期

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

數值操作

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

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

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

雜湊操作

redis.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);

集合操作

redis.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");

釋出/訂閱(Pub/Sub)

Bun 為 Redis Pub/Sub 協議提供了原生繫結,該功能在 Bun 1.2.23 中加入。

基本用法

在 publisher.ts 中建立釋出者:

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

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

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

writer.close();

在另一個檔案 subscriber.ts 中建立訂閱者:

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

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

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

在一個終端執行訂閱者:

terminal
bun run subscriber.ts

另一個終端執行釋出者:

terminal
bun run publisher.ts

釋出訊息

使用 publish() 方法釋出訊息:

redis.ts
await client.publish(channelName, message);

訂閱頻道

使用 .subscribe() 方法訂閱頻道:

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

使用 .unsubscribe() 取消訂閱:

redis.ts
await client.unsubscribe(); // 取消所有頻道訂閱
await client.unsubscribe(channel); // 取消指定頻道訂閱
await client.unsubscribe(channel, listener); // 取消指定監聽器訂閱

高階用法

命令執行與流水線

客戶端預設自動進行流水線處理,通過批次傳送多個命令並在響應到達時依序處理,提升效能:

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

要停用自動流水線處理,請將 enableAutoPipelining 選項設定為 false:

redis.ts
const client = new RedisClient("redis://localhost:6379", {
  enableAutoPipelining: false, // [!code ++]
});

原生命令執行

使用 send 方法執行任意 Redis 命令,包括沒有專用方法的命令。第一個引數是命令名稱,第二個引數是字串引數陣列。

redis.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"]);

連線事件

可以註冊連線事件處理函式:

redis.ts
const client = new RedisClient();

// 成功連線 Redis 伺服器時呼叫
client.onconnect = () => {
  console.log("Connected to Redis server");
};

// 與 Redis 伺服器斷開連線時呼叫
client.onclose = error => {
  console.error("Disconnected from Redis server:", error);
};

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

連線狀態及監控

redis.ts
// 檢查連線狀態
console.log(client.connected); // 布林值,表示連線是否建立

// 檢視緩衝區資料大小(位元組)
console.log(client.bufferedAmount);

型別轉換

客戶端會自動將 Redis 響應轉換為 JavaScript 值:

  • Integer 響應會作為 JavaScript 數字傳回
  • Bulk 字串會作為 JavaScript 字串傳回
  • Simple 字串會作為 JavaScript 字串傳回
  • Null Bulk 字串和 Null 陣列會作為 null 傳回
  • 陣列響應會作為 JavaScript 陣列傳回
  • Big number 響應(RESP3)會作為 BigInt 傳回。非整數文本的負載會作為字串傳回。getBuffer 會將負載作為 Buffer 傳回
  • 錯誤響應會丟擲帶有相應錯誤程式碼的 JavaScript 錯誤
  • 布林響應(RESP3)會作為 JavaScript 布林值傳回
  • Map 響應(RESP3)會作為 JavaScript 物件傳回
  • Set 響應(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
  • PSUBSCRIBE
  • UNSUBSCRIBE
  • UNPSUBSCRIBE

連線選項

建立客戶端時,可以傳入選項來設定連線:

redis.ts
const client = new RedisClient("redis://localhost:6379", {
  // 連線超時(毫秒),預設 10000
  connectionTimeout: 5000,

  // Idle timeout in milliseconds (default: 0 = no timeout). Bun counts it
  // from the last data the server sent. Sending does not reset it. When it
  // fires, Bun closes the connection and runs onclose. Bun does not reconnect
  // on its own, even with autoReconnect: true. Call connect() to reconnect.
  idleTimeout: 30000,

  // 斷線後是否自動重連,預設 true
  autoReconnect: true,

  // 最大重連嘗試次數(預設:20)
  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 次(預設值:20)
  4. 斷開連線期間,客戶端會:
    • 如果 enableOfflineQueue 為 true(預設值),則將命令加入佇列
    • 如果 enableOfflineQueue 為 false,則立即拒絕命令

支援的 URL 格式

Redis 客戶端支援多種連線 URL 格式:

redis.ts
// 標準 Redis URL
new RedisClient("redis://localhost:6379");
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("rediss://localhost:6379");
new RedisClient("redis+tls://localhost:6379");
new RedisClient("redis+tls://localhost:6379");

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

// Unix 套接字上的 TLS
new RedisClient("redis+tls+unix:///path/to/socket");
new RedisClient("redis+tls+unix:///path/to/socket");

錯誤處理

Redis 客戶端會丟擲帶型別的錯誤,用於不同場景的異常:

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 - 收到來自伺服器的無效響應
  • ERR_REDIS_SERVER_ERROR - 伺服器傳送了錯誤回覆,可能是拒絕某條命令,或者在訂閱者模式下關閉連線

範例用例

快取

redis.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;
}

限流

redis.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),
  };
}

會話儲存

redis.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 客戶端使用 Rust 實現,並使用 Redis 序列化協議(RESP3)。它會使用指數退避演算法自動重連。它還會對命令進行流水線處理,因此可以在不等待先前命令響應的情況下發送多個命令。

限制及未來規劃

我們計劃在未來版本中解決的限制:

  • 事務(MULTI/EXEC)需要使用原始命令

不支援的功能:

  • Redis Sentinel
  • Redis Cluster