如何在SvelteKit+Supabase应用中实现存储配额校验与用量更新?
存储配额校验与原子更新的最佳实践(SvelteKit + Supabase场景)
针对你的场景,数据库层面的原子操作是解决并发上传竞态问题的核心,以下是具体的最佳实践方案:
一、推荐方案:Postgres函数+触发器(彻底解决并发问题)
Supabase的storage.objects表支持自定义触发器,只是需要手动编写Postgres逻辑,具体步骤如下:
1. 创建配额校验与更新函数
编写PL/pgSQL函数,在文件存入storage.objects前自动校验用户配额,并原子更新已用存储:
CREATE OR REPLACE FUNCTION check_and_update_storage_quota() RETURNS TRIGGER AS $$ DECLARE user_quota INT; user_used INT; new_file_size INT; BEGIN -- 锁定用户行,防止并发修改时的竞态 SELECT storage_quota, storage_used INTO user_quota, user_used FROM users WHERE user_id = NEW.owner_id FOR UPDATE; new_file_size = NEW.size; -- 校验剩余空间 IF (user_used + new_file_size) > user_quota THEN RAISE EXCEPTION '存储空间不足,剩余配额:% bytes', (user_quota - user_used); END IF; -- 原子更新已用存储 UPDATE users SET storage_used = user_used + new_file_size WHERE user_id = NEW.owner_id; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER;
2. 创建触发器关联到存储表
让函数在storage.objects插入新文件时自动执行:
CREATE TRIGGER trigger_check_storage_quota BEFORE INSERT ON storage.objects FOR EACH ROW EXECUTE FUNCTION check_and_update_storage_quota();
3. 配置权限
确保Supabase的storage角色有执行函数和访问用户表的权限:
GRANT EXECUTE ON FUNCTION check_and_update_storage_quota() TO storage; GRANT SELECT, UPDATE ON users TO storage;
二、备选方案:改进API端点(原子化RPC调用)
如果不想用触发器,可通过Supabase RPC函数实现原子化的校验+更新,避免分步操作的竞态问题:
1. 创建RPC函数
CREATE OR REPLACE FUNCTION update_storage_used_if_quota_enough(user_id UUID, add_size INT) RETURNS JSON AS $$ DECLARE user_quota INT; user_used INT; BEGIN SELECT storage_quota, storage_used INTO user_quota, user_used FROM users WHERE user_id = $1 FOR UPDATE; IF (user_used + add_size) > user_quota THEN RETURN '{"success": false}'::JSON; END IF; UPDATE users SET storage_used = user_used + add_size WHERE user_id = $1; RETURN '{"success": true}'::JSON; END; $$ LANGUAGE plpgsql SECURITY DEFINER;
2. SvelteKit端点调用RPC
在/api/upload-params中直接调用该RPC,确保校验和更新在一个事务内完成:
import { supabase } from '$lib/supabase'; export async function POST({ request }) { const { userId, fileSize } = await request.json(); const { data, error } = await supabase.rpc('update_storage_used_if_quota_enough', { user_id: userId, add_size: fileSize }); if (error || !data.success) { return new Response( JSON.stringify({ error: error?.message || '存储空间不足' }), { status: 400 } ); } // 生成Uppy所需的AWS上传参数 const uploadParams = await generateAWSUploadParams(); return new Response(JSON.stringify(uploadParams), { status: 200 }); }
关键注意事项
- 并发处理:两种方案都用了
FOR UPDATE锁定用户行,确保同一时间只有一个请求能修改该用户的存储数据,彻底避免竞态。 - 删除文件的反向更新:可添加
AFTER DELETE触发器,在文件删除时自动减去对应大小的已用存储。 - 分片上传适配:如果用Uppy分片上传,只需关注最终合并后的文件大小(Supabase存储会生成单条
storage.objects记录),无需处理分片的中间步骤。 - 错误提示:前端需捕获上传时的数据库异常,将配额不足的信息反馈给用户。
内容的提问来源于stack exchange,提问作者Christian
相关产品推荐
相关产品推荐

