在上一节 出海技术人的“全能基础设施套装” —— Serverless DB 篇 中,我们聚焦于 PostgreSQL Serverless 数据库 (如 Supabase、Neon) 在出海项目中的实践与部署。本节我们将视野扩展到 Serverless 架构下非关系型存储的关键组成部分:缓存服务 与 对象存储。这两者往往是出海项目初期最容易忽略、但又最容易踩坑的模块。
无论你是正在部署 AI 服务、社交平台、内容工具还是轻量电商应用,只要架构基于 Vercel、Cloudflare 等 Serverless 平台,非结构化数据的存储与缓存将成为绕不过去的一环。
在本文中你可以了解到:
- 为什么需要外部存储
- 如何借助 Upstash Redis 构建高性能缓存服务
- 如何通过 Cloudflare R2 构建支持公开访问、私有权限与全球加速的高性价比对象存储服务
- 实战案例:在实际项目中,如何用 Flydrive 集成本地与远程存储策略
- 踩坑避雷指南与最佳实践
为什么需要外部存储?
在上一篇文章 出海技术人的“全能基础设施套装” —— Serverless DB 篇 中,我们特别提到过 Vercel 环境的特点 —— 无法持久化数据到本地,也不能使用内存存储临时数据,所以我们需要将缓存数据和持久化数据统统外部化。
因此,一个面向出海的技术栈中,我们还需要补全这两项:
- ✅ 缓存系统 —— 如 Redis
- ✅ 对象存储 —— 如 S3 / R2
我们可以将用户的 Session 信息存储在 Redis 中作为缓存,同时如果访问量较大的时候,也可以使用 Redis 来缓存热点数据,避免频繁访问数据库,提高访问性能。
同时,用户上传的文件、图片等非结构化数据也需要存储在对象存储中,如 AWS S3、Cloudflare R2 等,用户访问的时候直接访问对象存储服务,避免了后端的流量转发,减轻了后端负担。同时,对于访问量特别大的热点静态资源,如 js,css,多媒体资源等也可以考虑放在对象存储服务,可以节省 Vercel 的流量费用,这玩意并不便宜,后期可能是吃钱大户,而对象存储服务的流量费用通常更低,甚至有免费额度。
我们的项目 mcp-oauth 就遇到这个问题,它基于 Vercel 的 @vercel/mcp-adapter 开发,当部署到 Vercel 的环境时,需要额外配置外部的 Redis 服务:
- OAuth 登录流程的 Session 数据,必须放在外部缓存系统中(如 Redis),因为 Vercel 的 Serverless 函数是无状态的,无法在不同请求间共享内存。
- SSE 长连接事件需要状态追踪,也依赖 Redis 的 Stream、Key-Value 结构。
接下来,我们将分别介绍优秀的存储解决方案 —— Upstash Redis 和 Cloudflare R2。
🔥 Upstash Redis:云原生的 Redis 服务
Redis 是大家再熟悉不过的内存数据库,常用来存储缓存数据。但 Upstash 给了它一个 Serverless 的“新壳”,像之前提到的 Supabase 和 Neon 一样,Upstash 同样提供了服务端的连接池实现,在 Vercel 环境中使用可以大幅提升连接性能。
Upstash 是一家专注于为 Serverless 应用场景 提供 Serverless 数据平台的云基础设施公司。它提供了很多数据平台服务,例如:
- Redis: KV 数据库
- Vector: 向量数据库
- QStash: 消息队列
- Workflow: 工作流引擎
- Search: Serverless AI 搜索引擎
Upstash Redis 上手非常简单,只需三步:
- 注册 Upstash 账号,创建 Redis 项目。
- 设置项目名,选择大区,创建后会自动生成 Token 和 Read Only Token,以及连接地址。对于部署在 Vercel 的项目,建议 Primary Region 选择 N. Virginia, USA (us-east-1),这样可以获得更低的延迟与更好的访问速度。
- 使用任何 Redis 客户端(如 ioredis、Upstash SDK)连接即可。
import { Redis } from "@upstash/redis";
const redis = Redis.fromEnv();
const cacheKey = `item:${itemId}`;
// Check cache
const cachedItem = await redis.get(cacheKey);
if (cachedItem) {
console.log("Cache hit");
return JSON.parse(cachedItem);
}
免费额度
同样的,Upstash 他们家的服务也提供了免费额度,对于初创项目应该够用了。Redis 服务的免费额度如下:
- Data Size 256 MB
- Monthly Commands 500 K
- Max Monthly Bandwidth: 10 GB
☁️ Cloudflare R2:零出口流量费的对象存储
Cloudflare 是全球领先的边缘计算平台,提供了丰富的 Serverless 计算、存储和网络服务。它的产品线包括:
- Serverless 数据库(D1)
- Serverless 函数(Workers)
- CDN 加速(Workers KV、R2 对象存储)
- 安全防护(WAF、DDoS 防护)
等等,最重要的是,他们家的定价相当良心,及时不花一分钱也能享受相当高额度的基础网站服务,如 R2 和 CDN 等等。如此业界良心的厂商被用户尊称为“赛博佛祖”!
他们家的其他网站服务我们会在今后的文章中详细介绍。本文我们重点关注的是 Cloudflare 的对象存储服务:R2。
R2:对标 S3 的对象存储方案
网站开发不可避免的涉及上传文件的需求,比如用户上传的图片,文档等,以及网站本身提供的大容量附件下载等。传统的解决方案是使用 AWS S3,OSS 这样的对象存储服务,但现在,我们有了其他的选择,就是 R2。
R2 完美兼容 AWS S3 接口,开发者可以直接复用已有 SDK 与工具链。
相比其他存储,🚀 你会爱上它的理由:
- 💰 免费额度极高:10 GB 免费存储,10 万次读写请求/月。
- 🌍 绑定自定义域名即可自动 CDN 加速
- 🧾 零出口费用,天然适合出海产品抗刷流量
R2 的定价相当诱人 (官方提供了一个 价格计算器,方便估算成本),绑定域名就会自动实现 CDN 加速,使用上大大简化了,非常适合初创项目。
在开发上,R2 兼容 S3 的 API,可以直接使用 AWS SDK、MinIO 等工具进行操作。以下是一个简单的使用 aws js SDK 的示例:
import {
S3Client,
ListBucketsCommand,
ListObjectsV2Command,
GetObjectCommand,
PutObjectCommand,
} from "@aws-sdk/client-s3";
const S3 = new S3Client({
region: "auto",
endpoint: `https://${ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: ACCESS_KEY_ID,
secretAccessKey: SECRET_ACCESS_KEY,
},
});
console.log(await S3.send(new ListBucketsCommand({})));
只需要将 region
改为 auto
,并将 endpoint
指向 R2 的地址即可在不改变代码的情况下使用 R2,做到了平滑切换。
⚠️ R2 的限制与注意事项
R2 虽然在接口上完全兼容 S3,但是需要特别注意,R2 并未完全实现 S3 的所有功能。比如这些:
- ⚠️ 不完全兼容 S3 的 ACL 功能。R2 目前不提供像 S3 那样的细粒度 ACL 控制,S3 可以细到每个 Object 都有独立的 ACL 权限。R2 只支持 Bucket 级别的 public 和 private 权限。
- ⚠️ 不支持所有 Metadata 字段,如
content-disposition
,你无法通过设置这个头控制文件下载时的默认文件名,而这个功能在 S3 是生效的。
R2 的详细兼容性文档可以参考官方列表: S3 API compatibility
使用的时候要特别小心,因为你的代码在用到这些功能的时候,相关设置可能被悄悄忽略,无报错但实际无效,需谨慎验证行为是否符合预期。
☁️ R2 的使用
我们将从 Bucket 的创建与权限设置开始,介绍如何使用 R2 进行对象存储。以及提供一些代码示例,展示如何在项目中集成 R2。
🪣 Bucket 的创建与权限设置
总体来说,R2 相比 S3 的使用过程做了大幅简化,精简掉了很多不太常用的功能,所以上手使用上也会更简单。同时 R2 对于 Bucket 名称限制也会更宽松,只需要账号下唯一即可,不需要像 S3 那样整个 Region 全局唯一。
从创建 Bucket 开始,R2 就不需要像 S3 那样需要设置复杂的权限和策略,只需要简单地填写一个 Bucket name 即可。创建后的 Bucket 默认是私有的,所有的读写操作必须要授权 (通过 AK 方式或者 Signed URL 方式)。Bucket 设置中可以设置为公开,但 R2 的公开权限仅限于读取操作,并且对整个 Bucket 生效,对于写入操作始终为私有,不支持 S3 的公共读写权限。
所以实际开发中,我们需要将可以公开读取的资源和需要授权读取的资源分开成不同的 Bucket 存储。比如我们创建两个 Bucket,一个用于存储用户上传的头像,可以设置为公开读取,另一个 Bucket 用于上传用户的私密文件,权限设置为私有。
设置 R2 的 Bucket 为公共读取的方法是在 Bucket 设置中开启 公共开发 URL 或者添加 自定义域,前者会分配一个 r2.dev
域名的 URL,后者则可以绑定自己的域名,然后通过拼接 Object 的 Key 来访问资源。使用 *.r2.cloudflarestorage.com
域名则始终是私有的,无法公开访问。
🧰 后端使用 FlyDrive 轻松实现本地存储与 R2 切换
为了方便在本地环境使用 local (本地磁盘) 存储和产品环境使用 R2,我们推荐使用 FlyDrive 这个文件库。它提供了统一的 API 来处理不同存储驱动,包括本地磁盘、S3、GCS 等,这样方便我们在不同存储驱动上切换,实现本地开发环境与产品环境的无缝对接。
由于 R2 兼容 S3 的 API,我们可以直接使用 FlyDrive 的 S3 驱动来连接 R2,这里是我们的代码示例:
import type { S3ClientConfig } from "@aws-sdk/client-s3";
import { DriveManager } from "flydrive";
import { FSDriver } from "flydrive/drivers/fs";
import { S3Driver } from "flydrive/drivers/s3";
import type { FileResourceType } from "../../consts";
import { env } from "../../env";
function createS3Config(): S3ClientConfig {
const accessKeyId = env.R2_ACCESS_KEY_ID;
const secretAccessKey = env.R2_SECRET_ACCESS_KEY;
const endpoint = env.R2_ENDPOINT;
return {
credentials: {
accessKeyId,
secretAccessKey,
},
endpoint,
region: "auto",
} as S3ClientConfig;
}
function createS3Driver(bucket: string, visibility: "private" | "public") {
return new S3Driver({
...createS3Config(),
supportsACL: false,
bucket,
visibility,
});
}
const driveManager = new DriveManager({
default: "local",
services: {
local: () =>
new FSDriver({
location: "./uploads/resources",
visibility: "private",
}),
r2: () => createS3Driver("resources", "private"),
},
});
export function getDriveManager(resourceType: FileResourceType) {
const driveManagerType = env.FILE_DRIVER_TYPE;
if (driveManagerType === "local") {
return driveManager.use("local");
}
if (driveManagerType === "r2") {
return driveManager.use("r2");
}
throw new Error(`Unsupported drive manager type: ${driveManagerType}`);
}
我们定义了一个 getDriveManager
函数,根据环境变量 FILE_DRIVER_TYPE
返回对应的存储驱动。这样在本地开发时可以使用本地磁盘存储,而在生产环境中则切换到 R2,同时保持接口不变。
🔒 如何让前端「直连 R2」却又「安全上传」?
由于 R2 的费用便宜很多,并且没有出口流量费,所以我们期望前端可以直接对接 R2,而不需要通过后端进行文件上传和下载。所以我们可以通过后端授权来实现前端对 R2 的访问。
对于读写都需要授权的场景,我们需要后端生成一个预签名 URL (Signed URL),前端通过这个 URL 进行文件上传和下载。这样可以确保只有授权的用户才能访问这些私密文件,并且设置一个合理的过期时间,防止 URL 被滥用。场景示例代码如下:
// 后端部分代码
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const command = new PutObjectCommand({
Bucket: bucketName,
Key: fileNameWithUUID,
ContentType: contentType,
ContentDisposition: `attachment; filename="${encodeURI(fileName)}"`,
});
const client = new S3Client({
/*...*/
});
const signedUrl = await getSignedUrl(client, command, {
expiresIn: SIGNED_URL_EXPIRATION,
});
我们直接通过 aws sdk 提供的 getSignedUrl
方法来生成预签名 URL,通过我们自己的权限控制逻辑来限制访问。前端获取到 Signed URL 后,就可以直接使用这个 URL 进行文件上传或下载,而不需要经过后端转发。前端示例代码:
// 前端部分代码
async uploadFileWithSignedUrl(
file: File,
signedUrl: string,
contentType: string,
): Promise<void> {
const uploadResponse = await fetch(signedUrl, {
method: "PUT",
body: file,
headers: {
"Content-Type": contentType,
"Content-Disposition": `attachment; filename="${encodeURI(file.name)}"`,
},
mode: "cors",
});
if (!uploadResponse.ok) {
throw new Error("Failed to upload file");
}
}
⚠️ 别忘了配置好 R2 的 CORS 规则,否则前端会报错!
如果不需要精准的控制 signed URL 可用的 command,也可以直接使用 FlyDrive 提供的 getSignedUrl
方法来生成预签名 URL: getSignedUrl
🎉 这样我们就可以使用系统的权限控制策略来控制前端对 R2 的访问,而不需要在前端暴露任何敏感信息。
🔓️ 可公共读取的资源的访问
对于可以公共读取的资源,我们可以将 Bucket 设置为公开 (Public),这样就可以直接通过公开 URL 访问这些资源,而不需要经过后端授权。而 FlyDrive 也提供了拼接公开 URL 的方法,用户不需要自己去拼接完整的 URL,以下是示例代码:
const disk = new Disk(
new S3Driver({
cdnUrl: "https://pub-jg21.r2.dev",
endpoint: "https://jg21.r2.cloudflarestorage.com",
bucket: "testing-drive",
})
);
const URL = await disk.getUrl("avatar.png");
console.log(URL); // https://pub-jg21.r2.dev/avatar.png
在初始化 S3Driver
时,我们可以指定 cdnUrl
,这样就可以直接通过 getUrl
方法获取到公开资源的 URL,而不需要手动拼接,将这个 URL 直接返回给前端即可。
注意:
FlyDrive 当前版本的 S3 驱动目前有一个限制,就是不能支持非英文字符,会报错: The key "xxxx" has unallowed characters
,比如中文这种,即使使用 URL encode 也无法绕过,于是我就这个问题给官方提交了 issue: issue#9。
官方承诺会在未来修复,但是目前版本需要规避使用非英文字符的文件名。
🌐 绑定自定义域名,开启全球加速体验
R2 默认创建的 Bucket 都是私有的,想要公开访问,需要手工在设置中开启。R2 开启公开访问有两种 URL,一种是 公共开发 URL,使用 *.r2.dev
域名,另一种是 自定义域名,可以使用用户自己的域名。
r2.dev
域名是 R2 提供的开发环境域名,不建议产品环境使用,原因有下:
- ⚠️ 无 CDN 缓存优化
- ⚠️ 有限速
所以在本地开发期间用用还行,但上线后还是建议绑定自己的域名,不但可以满速体验,而且自动全球 CDN 加速。
作为对比,可以参考我们以前的文章 —— 使用阿里云 OSS+CDN 部署前端页面与加速静态资源。像阿里云 OSS 和 AWS S3 这样的存储服务,CDN 加速和存储服务是分开的,用户需要自己经过繁琐的配置才能实现 CDN 加速。相比之下,R2 的体验可以说是「从头省到尾」。
不过 R2 和其他对象存储服务不同,它要求你必须把域名转移到 Cloudflare 托管才能使用自定义域名,而其他云服务往往只需要自行添加 CNAME 解析即可。
如果你的域名并非在 Cloudflare 购买,那么你需要将域名转移到 Cloudflare 解析 (只是解析域名,实际域名的续费仍然在原域名服务商处,除非你使用 域注册 —> 转移域 功能将域名完全转出到 Cloudflare),进入 Cloudflare 控制台首页,在右边的界面中点击 + 添加域 按钮,收入你的域名,然后按照提示完成域名的添加即可,最后一步会给出一组 NS
解析记录,你需要在原域名服务商处添加这组 NS
解析记录。
注意:
一旦你在原域名服务商处添加了 Cloudflare 的 NS
解析记录,你的域名解析就会完全转移到 Cloudflare DNS 托管,在原域名服务商处的 DNS 解析将不再生效,为了保证服务零中断,你需要在添加 NS
解析记录之前,先在 Cloudflare 控制台添加好所有的 DNS 解析记录。
完成域添加之后,就可以在 R2 的 Bucket 的 设置 —> 自定义域 设置中添加你期望的域名,如 r2.yourdomain.com
,剩下的步骤只需要一路下一步,Cloudflare 就会自动为你完成 CNAME 解析的添加,并且自动生成 SSL 证书,实现 HTTPS 访问。
小结
本篇我们聚焦两款在圈内广受好评的神器:
- 🚀 Upstash Redis —— 用几行代码就能把缓存、Session 存储搞定。无需部署,无需维护
- ☁️ Cloudflare R2 —— 兼容 S3 无缝迁移,且没有出流量费,配合 CDN 加速,让全球用户秒开不再是奢望。
不仅讲了为什么,更讲了怎么用,还带你避坑、拆包实战。
这些工具之所以值得写进“基础设施套装”,不仅仅因为它们“酷”,而是因为它们在真实的出海项目里,真的跑得动、扛得住、养得起。
本文包含付费内容,需要会员权限才能查看完整内容。