mcpField
V
vercel-kv

vercel-kv

github.com/jezweb数据与数据库1.1k314 次安装3 周前更新
代码仓库

关于 vercel-kv

vercel-kv 是面向使用 Vercel 的开发者的 KV 存储技能,介绍通过 @vercel/kv 操作 Redis 兼容存储。它提供缓存、限流和会话管理示例,并记录常见限制与问题。

主要功能

  1. 01使用 @vercel/kv 读写键值数据
  2. 02为临时数据设置过期时间
  3. 03通过流水线批量执行操作
  4. 04处理序列化、类型和扫描问题

适用场景

  1. 01缓存数据库查询结果
  2. 02按请求限制访问频率
  3. 03存储带有效期的用户会话

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill vercel-kv

安装后在 Claude Code 中输入/vercel-kv确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
assets/
example-template.txt
references/
example-reference.md
scripts/
example-script.sh
templates/
package.json
session-management.ts
simple-rate-limiting.ts

详细介绍

Vercel KV

最后更新:2026-01-21 版本:@vercel/[email protected](兼容 Redis,由 Upstash 提供支持)


快速入门

# Create KV: Vercel Dashboard → Storage → KV
vercel env pull .env.local  # Creates KV_REST_API_URL and KV_REST_API_TOKEN
npm install @vercel/kv

基本用法:

import { kv } from '@vercel/kv';

// Set with TTL (expires in 1 hour)
await kv.setex('session:abc', 3600, { userId: 123 });

// Get
const session = await kv.get('session:abc');

// Increment counter (atomic)
const views = await kv.incr('views:post:123');

重要:始终使用带命名空间的键(user:123,而非 123),并为临时数据设置 TTL。


常见模式

缓存(旁路缓存):

const cached = await kv.get(`post:${slug}`);
if (cached) return cached;

const post = await db.query.posts.findFirst({ where: eq(posts.slug, slug) });
await kv.setex(`post:${slug}`, 3600, post); // Cache 1 hour
return post;

速率限制:

async function checkRateLimit(ip: string): Promise<boolean> {
  const key = `ratelimit:${ip}`;
  const current = await kv.incr(key);
  if (current === 1) await kv.expire(key, 60); // 60s window
  return current <= 10; // 10 requests per window
}

会话管理:

const sessionId = crypto.randomUUID();
await kv.setex(`session:${sessionId}`, 7 * 24 * 3600, { userId });

Pipeline(批量操作):

const pipeline = kv.pipeline();
pipeline.set('user:1', data);
pipeline.incr('counter');
const results = await pipeline.exec(); // Single round-trip

键命名:使用命名空间,例如 user:123、post:abc:views、ratelimit:ip:endpoint


关键规则

始终:

  • ✅ 为临时数据设置 TTL(使用 setex 而非 set)
  • ✅ 使用带命名空间的键(user:123,而非 123)
  • ✅ 处理返回值为 null 的情况(键不存在)
  • ✅ 批量操作使用 pipeline

切勿:

  • ❌ 忘记设置 TTL(会造成内存泄漏)
  • ❌ 存储大于 1MB 的值(请使用 Vercel Blob)
  • ❌ 将 KV 用作主数据库(它是缓存)
  • ❌ 存储无法进行 JSON 序列化的数据(函数、BigInt、循环引用)

已知问题预防

此技能可预防 15 个已记录的问题:

问题 #1:缺少环境变量

错误:Error: KV_REST_API_URL is not defined 或 KV_REST_API_TOKEN is not defined 来源:https://vercel.com/docs/storage/vercel-kv/quickstart | GitHub Issue #759 发生原因:本地或部署环境中未设置环境变量。在单体仓库(Turborepo/pnpm 工作区)中,将 @vercel/kv 封装到共享包可能导致 Vercel 构建失败,即使本地构建正常。 预防措施:运行 vercel env pull .env.local,并确保 .env.local 位于 .gitignore 中。对于单体仓库,可以选择:(1) 在使用该包的应用中创建客户端,而非在共享包中创建;(2) 使用 Vercel Environment Variables UI 在项目级别设置变量;或 (3) 将环境变量添加到 turbo.json 流水线配置中:{ "pipeline": { "build": { "env": ["KV_REST_API_URL", "KV_REST_API_TOKEN"] } } }。

问题 #2:JSON 序列化错误

错误:TypeError: Do not know how to serialize a BigInt 或循环引用错误。此外,hset() 会将数字字符串强制转换为数字。 来源:https://github.com/vercel/storage/issues/89 | GitHub Issue #727 发生原因:尝试存储无法进行 JSON 序列化的数据(函数、BigInt、循环引用)。此外,使用 hset() 存储看起来像数字的字符串值(例如 '123456')时,hgetall() 会将其作为数字返回,导致类型不一致。 预防措施:只存储普通对象、数组、字符串、数字、布尔值和 null。将 BigInt 转换为字符串。对于包含数字字符串的哈希字段,可以选择:(1) 使用非数字前缀,例如 'code_123456';(2) 以 JSON 字符串形式存储,并在取回后解析;或 (3) 校验并重新转换类型:在 hgetall() 之后使用 String(value.field)。

问题 #3:键命名冲突

错误:返回意外数据,或数据被其他功能覆盖 来源:生产环境调试、最佳实践 发生原因:不同功能使用 cache、data、temp 等通用键名 预防措施:始终使用带命名空间的键,遵循 feature:id:type 模式。

问题 #4:未设置 TTL

错误:内存用量不断增长,旧数据永不过期 来源:Vercel KV 最佳实践 发生原因:对临时数据使用 set(),而不使用 setex() 预防措施:所有临时数据都使用 setex(key, ttl, value)。设置合适的 TTL(秒)。

问题 #5:超出速率限制(免费层级)

错误:Error: Rate limit exceeded 或命令执行失败 来源:https://vercel.com/docs/storage/vercel-kv/limits 发生原因:超出免费层级每月 30,000 条命令的限制 预防措施:在 Vercel 控制面板中监控用量,必要时升级套餐,并使用缓存减少 KV 调用。

问题 #6:存储大型值

错误:Error: Value too large 或性能下降 来源:https://vercel.com/docs/storage/vercel-kv/limits 原因:尝试在 KV 中存储超过 1MB 的值 预防措施:文件和图像请使用 Vercel Blob。KV 值应保持较小(建议小于 100KB)。

问题 #7:获取时类型不匹配

错误:TypeScript 错误、运行时类型错误。即使数据存在,泛型 kv.get<T>() 有时也会返回 null。 来源:常见 TypeScript 问题 | GitHub Issue #510 原因:kv.get() 返回 unknown 类型,需要进行类型转换或验证。此外,还存在类型推断错误:使用 kv.get<T>() 这类泛型时,即使 CLI 显示数据存在,该函数也可能返回 null,这是序列化/反序列化问题导致的。 预防措施:不要在 get() 中使用泛型。改为不指定类型参数进行获取,然后在获取后进行类型转换:const rawData = await kv.get('key'); const data = rawData as MyType | null;。使用前请通过 Zod 或类型守卫进行验证。

问题 #8:未处理管道错误

错误:静默失败、部分执行 来源:https://github.com/vercel/storage/issues/120 原因:管道执行时,单条命令可能会失败 预防措施:检查 pipeline.exec() 返回的结果数组并处理错误。

问题 #9:扫描操作效率低下

错误:查询缓慢、超时错误。在 v3.0.0+ 中,游标类型从 number 改为 string。 来源:Redis 最佳实践 | Release Notes v3.0.0 原因:对大型数据集使用 scan(),或游标处理不当。v3.0.0 引入了一项破坏性变更:扫描游标现在是 string,而不是 number。 预防措施:限制 count 参数,使用游标正确迭代,并避免在生产环境中执行全量扫描。在 v3.0.0+ 中,使用 let cursor: string = "0",并用 cursor !== "0" 进行比较(而不是 !== 0)。

问题 #10:未刷新 TTL

错误:会话过早过期、缓存提前失效 来源:生产环境调试 原因:访问时未刷新 TTL(滑动过期) 预防措施:访问时使用 expire(key, newTTL) 实现滑动窗口。

问题 #11:scanIterator() 无限循环(v2.0.0+)

错误:使用 kv.scanIterator() 时,for await 循环永不终止 来源:GitHub Issue #706 原因:v2.0.0+ 中存在一个错误,导致迭代器无法正确发出完成信号。迭代器可以正确处理键,但始终不会退出,导致函数无法返回。sscanIterator() 也会受到影响。 预防措施:使用手动 scan() 和游标代替 scanIterator()。

// Don't use scanIterator() - it hangs in v2.0.0+
for await (const key of kv.scanIterator()) {
  // This loop never terminates
}

// Use manual scan with cursor instead
let cursor: string = "0"; // v3.x uses string
do {
  const [newCursor, keys] = await kv.scan(cursor);
  cursor = newCursor;
  for (const key of keys) {
    const value = await kv.get(key);
    // process key/value
  }
} while (cursor !== "0");
问题 #12:zrange() 使用 rev: true 时返回空数组

错误:即使数据存在,kv.zrange(key, 0, -1, { rev: true }) 也会返回空数组 来源:GitHub Issue #742 原因:SDK 在处理某些键模式的反向标志时存在错误。CLI 始终会返回正确的值。移除 rev 标志后即可正确返回数据。 预防措施:省略 rev 标志并在内存中反转结果,或改用 zrevrange()。

// This sometimes returns empty array (BUG)
const chats = await kv.zrange(`user:chat:${userId}`, 0, -1, { rev: true });
// [] - but CLI shows 12 items

// Workaround 1: Omit rev flag and reverse in-memory
const chats = await kv.zrange(`user:chat:${userId}`, 0, -1);
const reversedChats = chats.reverse();

// Workaround 2: Use zrevrange instead
const chats = await kv.zrevrange(`user:chat:${userId}`, 0, -1);
问题 #13:Next.js 开发服务器首次调用时返回 Null

错误:启动开发服务器后,首次调用 kv.get() 会返回 null,后续调用则返回正确数据 来源:GitHub Issue #781 原因:Next.js 静态渲染会缓存首次响应。Next.js 开发服务器每次重新编译时都会发生此问题。 预防措施:使用 unstable_noStore() 强制动态渲染,在 fetch 调用中使用 cache: 'no-store',或添加重试逻辑。

import { unstable_noStore as noStore } from 'next/cache';

export async function getData() {
  noStore(); // Force dynamic rendering
  const data = await kv.get('mykey');
  return data; // Now returns correct value on first call
}

// Or add retry logic
async function getWithRetry(key: string, retries = 2) {
  let data = await kv.get(key);
  let attempt = 0;
  while (!data && attempt < retries) {
    await new Promise(r => setTimeout(r, 100));
    data = await kv.get(key);
    attempt++;
  }
  return data;
}
问题 #14:Vite“process is not defined”错误

错误:在 Vite 中导入 @vercel/kv 时出现 Uncaught ReferenceError: process is not defined 来源:GitHub 问题 #743 原因:Vite 默认不会为 Node.js 的 process.env 提供 polyfill。@vercel/kv 需要 process.env 存在。 预防方法:使用 Vite 的 define 提供 polyfill,使用 import.meta.env 配合 createClient,或安装 vite-plugin-node-polyfills。

// Option 1: Vite config with define
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');
  return {
    define: {
      'process.env.KV_REST_API_URL': JSON.stringify(env.KV_REST_API_URL),
      'process.env.KV_REST_API_TOKEN': JSON.stringify(env.KV_REST_API_TOKEN),
    },
  };
});

// Option 2: Use createClient with import.meta.env
import { createClient } from '@vercel/kv';

const kv = createClient({
  url: import.meta.env.VITE_KV_REST_API_URL,
  token: import.meta.env.VITE_KV_REST_API_TOKEN,
});
问题 #15:Next.js Server Actions 返回过时数据

错误:即使 KV 值已更新,Server Actions 中的 kv.get() 仍返回过时/缓存的数据 来源:GitHub 问题 #510 原因:Next.js 静态渲染会缓存 Server Action 响应。控制台输出会显示为黄色(缓存命中指示)。 预防方法:使用 unstable_noStore() 强制动态渲染,或使用路由处理程序替代 Server Actions。

// In Server Actions
'use server'
import { unstable_noStore as noStore } from 'next/cache';

export async function logChat(text: string) {
  noStore(); // Force dynamic rendering
  let n_usage = await kv.get('n_usage');
  // Now returns fresh value, not cached
}

// Or use route handlers (automatically dynamic)
// app/api/chat/route.ts
export async function GET() {
  let n_usage = await kv.get('n_usage'); // Fresh data
  return Response.json({ n_usage });
}

版本迁移指南

v3.0.0 破坏性变更

游标类型已更改(发行说明):

  • 扫描游标现在返回 string,而不是 number
  • 将比较从 cursor !== 0 更新为 cursor !== "0"
// v2.x
let cursor: number = 0;
do {
  const [newCursor, keys] = await kv.scan(cursor);
  cursor = newCursor;
} while (cursor !== 0);

// v3.x
let cursor: string = "0";
do {
  const [newCursor, keys] = await kv.scan(cursor);
  cursor = newCursor;
} while (cursor !== "0");
v2.0.0 破坏性变更

默认启用自动流水线(发行说明):

  • 现在会自动批处理命令以提升性能
  • 在边缘情况下可能导致时序问题
  • 如有需要,可通过 enableAutoPipelining: false 禁用
// If auto-pipelining causes issues, disable it:
import { createClient } from '@vercel/kv';

const kv = createClient({
  url: process.env.KV_REST_API_URL,
  token: process.env.KV_REST_API_TOKEN,
  enableAutoPipelining: false // Disable auto-pipelining
});

引入 scanIterator() 错误:v2.0.0+ 的 scanIterator() 存在无限循环错误。解决方法请参阅问题 #11。


已知限制

不支持 Redis Streams

来源:Vercel KV Redis 兼容性 | GitHub 问题 #278

尽管底层的 Upstash Redis 支持 Redis Streams(XREAD、XADD、XGROUP 等),@vercel/kv 却不支持。该包缺少流操作方法。

解决方法:直接使用 @upstash/redis 操作流:

// @vercel/kv doesn't have stream methods
// await kv.xAdd(...) // TypeError: kv.xAdd is not a function

// Use Upstash Redis client directly
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.KV_REST_API_URL!,
  token: process.env.KV_REST_API_TOKEN!,
});

await redis.xadd('stream:events', '*', { event: 'user.login' });
const messages = await redis.xread('stream:events', '0');
哈希操作返回对象而非字符串

来源:GitHub 问题 #674

kv.hgetall() 返回 JavaScript 对象,而不是 JSON 字符串。这会让预期结果经过字符串序列化的开发者感到困惑。

await kv.hset('foobar', { '1834': 'https://example.com' });

const data = await kv.hgetall('foobar');
console.log(typeof data); // "object" not "string"

// It's already an object - use directly
console.log(data['1834']); // 'https://example.com'

// If you need JSON string
const jsonString = JSON.stringify(data);

高级模式

分布式锁(防止竞态条件):

const lockKey = `lock:${resource}`;
const lockValue = crypto.randomUUID();

const acquired = await kv.setnx(lockKey, lockValue);
if (acquired) {
  await kv.expire(lockKey, 10); // TTL prevents deadlock
  try {
    await processOrders();
  } finally {
    const current = await kv.get(lockKey);
    if (current === lockValue) await kv.del(lockKey);
  }
}

排行榜(排序集合):

await kv.zadd('leaderboard', { score, member: userId.toString() });

// Note: zrange with { rev: true } has a known bug (Issue #12)
// Use zrevrange instead for reliability
const top = await kv.zrevrange('leaderboard', 0, 9, { withScores: true });
const rank = await kv.zrevrank('leaderboard', userId.toString());

最后验证日期:2026-01-21 | 技能版本:2.0.0 | 变更:新增 5 个问题(scanIterator 卡住、zrange rev 错误、Next.js 开发服务器返回 null、Vite process 错误、Server Actions 缓存过时),根据 TIER 1-2 研究结果扩展问题 #1、#2、#7、#9,并新增版本迁移指南和已知限制章节

评论

支持 Markdown