关于 vercel-kv
vercel-kv 是面向使用 Vercel 的开发者的 KV 存储技能,介绍通过 @vercel/kv 操作 Redis 兼容存储。它提供缓存、限流和会话管理示例,并记录常见限制与问题。
主要功能
- 01使用 @vercel/kv 读写键值数据
- 02为临时数据设置过期时间
- 03通过流水线批量执行操作
- 04处理序列化、类型和扫描问题
适用场景
- 01缓存数据库查询结果
- 02按请求限制访问频率
- 03存储带有效期的用户会话
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill vercel-kv安装后在 Claude Code 中输入/vercel-kv确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonassets/example-template.txtreferences/example-reference.mdscripts/example-script.shtemplates/package.jsonsession-management.tssimple-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');
哈希操作返回对象而非字符串
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,并新增版本迁移指南和已知限制章节