Bun技术评估 - 06 Redis

概述

本文是笔者的系列博文 《Bun技术评估》 中的第六篇。

本文的来由,是笔者在编写系列文章的过程中,突然发现,bun提供了对redis的原生支持。这也是一个令人比较感兴趣的特性。

Web应用的开发和部署,经过一个比较长时间的发展,都已经有了一个比较成熟的模式。简单而言,就是前后端分离+静态内容服务+API负载均衡+数据库+缓存+消息队列这些相对固定的组成模块。在这个框架中,通常选择Redis来支撑缓存这部分的应用需求,它也是这方面应用模式的事实标准。

因此,bun在内置实现对redis的支持,也是比较符合逻辑和其"All In One"开发体系的技术设定的。

本文讨论的相关技术细节,参考内容主要来自bun官方技术文档的相关章节和valkey的技术文档,相关链接如下:

bun.sh/docs/api/re...

valkey.io/commands

基本应用

和SQL的实现方式类似,bun引入的redis支持的应用,也非常简单直接,例如下面的示例代码:

js 复制代码
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 环境配置信息 .env
REDIS_URL=redis://:password@192.168.9.192:6380

bun redis也可以直接使用.evn环境配置文件。配置好之后,使用时系统会自动连接redis服务器并进行操作,非常方便。当然,bun redis也支持客户端实例化,连接后使用的方式:

js 复制代码
const client = new RedisClient("redis://username:password@localhost:6379");

// 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();

常见指令

这里正好借机复习和熟悉一下Redis的常见操作和指令:

  • 基本键值操作

包括通过键,来设置内容、获取内容、删除内容和检查存在性。

js 复制代码
// 设置内容
await redis.set("user:1:name", "Alice");

// 查询内容
const name = await redis.get("user:1:name");

// 删除内容
await redis.del("user:1:name");

// 检查存在性
const exists = await redis.exists("user:1:name");
  • 设置超时

可以给redis对象设置过期时间,实现内容的自动管理,通常用于缓存和临时性的内容操作。

js 复制代码
// 设置过期时间,单位是秒
await redis.set("session:123", "active");
await redis.expire("session:123", 3600); // expires in 1 hour

// 获取过期时间
const ttl = await redis.ttl("session:123");
  • 数值操作

通常用于计数器,可以设置当前值,增加或者减少值。

js 复制代码
// 数值操作(计数器)
await redis.set("counter", "0");
await redis.incr("counter");
await redis.decr("counter");
  • Hash

redis其实没有对象结构,而是使用一种hash结构来模拟对象的操作和行为。

js 复制代码
// 设置hash对象
await redis.hmset("user:123", [
  "name",
  "Alice",
  "email",
  "alice@example.com",
  "active",
  "true",
]);

// 获取对象内容 键+hash键
const userFields = await redis.hmget("user:123", ["name", "email"]);
console.log(userFields); // ["Alice", "alice@example.com"]

// hash对象中的值
await redis.hincrby("user:123", "visits", 1);

// 支持浮点型
await redis.hincrbyfloat("user:123", "score", 1.5);
  • set集合

包括了常见的set集合操作,典型的应用场景就是标签管理。

js 复制代码
// 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");

原始指令

细心的读者会发现,前面例举的这些redis的操作,都是使用对象方法来实现的。就是说,这些redis的功能,必须要在bun redis中,有对应的方法实现。显然这是不够灵活的。其实,bun redis支持原始的redis指令,只要redis服务支持这个指令,就可以进行操作。相关的指令集合,可以在对应版本的redis操作手册上查询。

下面是一些简单的示例:

js 复制代码
// Run any Redis command
const info = await redis.send("INFO", ["server"]);

// 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指令,后面是一个对象参数,就是这个指令所需要的参数(可能是多个,所以需要一个数组来承载)。

错误处理

错误处理的实现非常简单,就是try..catch方式,但是需要注意,如果要匹配错误类型的话,需要注意查阅技术手册,获得可能的错误编码的名称。

js 复制代码
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);
  }
}

高级特性

bun redis的实现,支持一些高级特性,在实际的业务应用开发中,是比较实用的:

  • reconnect 自动重新连接

bun redis可以在连接中断的时候,自动重新连接。还有一些重连参数,可以让开发者配置控制重新连接的策略。也有参数配置在连接中断时,操作的行为,是等待重新连接,还是直接返回错误信息。

  • auto type convert 自动类型转换

bun redis提供了一些redis响应内容的自动化的数据类型转换(因为redis响应的格式一般都是string)。

  • connection monitor 连接状态和监控

bun redis提供了一些客户端连接状态的监控机制,开发者可以监控连接状态和信息。

  • pipeline 执行流水线

bun redis提供了多个redis命令执行的流水线操作机制,可以提高执行效率(当然这个特性是控制可选的)。如下面的示例代码:

js 复制代码
// Commands are automatically pipelined by default
const [infoResult, listResult] = await Promise.all([
  redis.get("user:1:name"),
  redis.get("user:2:email"),
]);
  • raw command 原始指令

send方法,可以用于执行任何redis服务器支持的指令和操作。前面已经有简单的示例。

应用场景

很多使用Redis的开发者,并没有认真的想过,redis可以做什么。笔者刚好在bun的redis文档中,看到了相关的内容,觉得不错,有些地方的考虑和操作方式都是比较好的,可以分享和学习一下。

  • Caching 缓存

缓存是redis最常见的一个使用场景。因为在很多情况下,从数据库里面查询结果是一个代价比较高的操作,如果能够确定查询的结果不会频繁变化,可以在查询后,将结果存储在redis中。下次如果查询相同的数据,就可以直接从redis中获得结果,而且是格式化好的,不需要数据库操作和转换,这样就能够提供更好的查询性能。

下面的代码,就展示了一个带有缓存的数据库查询操作:

js 复制代码
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;
}

其实,在这里面,如果要有一个更完整的设计的话,应该有一个缓存项目消除的操作,就是在数据修改(比如当前这个用户信息)后,应当执行一个相关缓存的清除。这样可以保证,下次查询,获得的是最新的数据,而且缓存也会更新到新版本。现在设计的机制,就不是很严谨,使用一个时间来控制缓存的过期和更新。

  • Rate Limit 流量控制

下面的示例,是应用redis来实现的一个IP限流函数。可以用于监控和检查在一段时间窗口内某些IP的请求数量。其他的限流算法还可以选择滑动窗口、令牌桶和漏桶等等,它们都可以利用到redis提供的高性能计数器增加方法和超时机制。

js 复制代码
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),
  };
}
  • Session Store 会话存储

它给的示例代码是这样的:

js 复制代码
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),
  };
}

从代码中,我们可以理解到session管理和使用方式。分成两个阶段。用户成功登录后,使用createSession方法,将用户信息放入redis中,同时将生成的sessionid作为会话ID在客户端和服务端共享; 需要的时候,可以快速的基于sessionid,从redis中找到这个id所对应的用户信息。存储实现的方式是hash表,可以存放多个(有序的)集合信息。redis可以为这个hash对象设置过期时间,过期后自动销毁关联的信息。

  • Sharing Data 共享数据

redis很适合应用在一个复杂系统当中,作为某种需要共享的数据存储的机制,来在不同的子系统之间共享数据和状态。因为它天生的支持网络访问和提供高性能的小型数据操作。前面的几种应用场景,都可以扩展到网络应用中,为不同的子系统所使用。

实现规格和限制

同样的,由于技术发展时间的限制,bun redis也有一些需要完善的地方和限制。这里简单例举几条:

  • RESP3, Bun Redis是使用Zig实现的比较新的RESP3协议。

  • 不支持Redis Sentinel和 Redis Cluater

  • 没有专用的pub/sub方法(可以用原始命令api)

  • 现在只能通过原始指令,实现事务

  • 没有专用方法支持stream(原始指令api支持)

Valkey

虽然redis是缓存数据库系统的事实标准。但后来它的许可证模式发生了一些变化,已经不再被认为是传统意义上的"开源"软件。所以市场上出现了一些开源的替代方案。

在本文的实验操作中,笔者实际使用的系统,就已经不是官方的redis服务系统,而是它的一个开源替代系统: Valkey。从技术而言,Valkey可以看成是redis的一个分支版本(7版本之后)。从笔者的实际使用经验来看,对于客户端系统来说,其实没有什么区别,因为它们的功能指令集和数据类型,都是一样的。

js 复制代码
192.168.9.192:6380> INFO
# Server
redis_version:7.2.4
server_name:valkey
valkey_version:7.2.8
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:a4da9b33b1c6872a
redis_mode:standalone
os:Linux 4.4.167 aarch64
arch_bits:64
monotonic_clock:POSIX clock_gettime
multiplexing_api:epoll
atomicvar_api:c11-builtin
gcc_version:9.4.0
process_id:1230944
process_supervised:no
run_id:8856614b73492bb28629a3eec6bd6f6688fd861a
tcp_port:6380
server_time_usec:1749541463089474
uptime_in_seconds:226
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:4710999
executable:/opt/valkey-7.2.8-focal-arm64/bin/valkey-server
config_file:
io_threads_active:0
listener0:name=tcp,bind=*,bind=-::*,port=6380
...

关于valkey本身的安装、配置和使用,其实和redis是非常相似的,这里就不再扩展讨论。如果确有必要,笔者可能在另外的专题中专门探讨。

小结

本文探讨了另一个bun原生提供的,常用的Web应用支持的特性: redis。 包括了基本应用、常见的指令和数据形式、扩展和高级特性,然后讨论了常见的redis应用场景,以及redis的兼容替代技术如valkey等等。

相关推荐
程序员清风11 小时前
贝壳一面:年轻代回收频率太高,如何定位?
java·后端·面试
考虑考虑11 小时前
Java实现字节转bcd编码
java·后端·java ee
AAA修煤气灶刘哥11 小时前
ES 聚合爽到飞起!从分桶到 Java 实操,再也不用翻烂文档
后端·elasticsearch·面试
爱读源码的大都督12 小时前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
星辰大海的精灵12 小时前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
天天摸鱼的java工程师12 小时前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
一乐小哥12 小时前
一口气同步10年豆瓣记录———豆瓣书影音同步 Notion分享 🚀
后端·python
LSTM9712 小时前
如何使用C#实现Excel和CSV互转:基于Spire.XLS for .NET的专业指南
后端
感哥12 小时前
Redis缓存一致性
redis
三十_12 小时前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs