从 3 分钟到 500ms:那个完全说不通的注册 Bug
我追查了一个注册后延迟 3 分钟的问题,最后发现是“薛定谔用户”——由于写入与读取之间的数据库复制延迟,用户在同一时刻既存在又不存在。
你有没有遇到过那种用户反馈“听起来完全不合理”的 bug?“我注册后 app 要 3 分钟才加载出来。”3 分钟?这不是加载时间,这是喝杯咖啡的时间。这篇就是我如何追出 DIALØGUE 历史上最诡异 bug 之一的全过程。
谜案开始
起初一切都很平常。一个新用户用 Google SSO 注册,兴冲冲想试 DIALØGUE。然后……没反应。也不完全是没反应——他看到了我们设计得很漂亮的 loading skeleton。整整 3 分钟。
诡异点是:它只发生在_新用户_身上。老用户登录都秒进。而且不稳定——有时 1 分钟,有时 3 分钟,偶尔又立即正常。
我的第一反应是:“肯定是冷启动。”(旁白:并不是冷启动。)
调查过程
第 1 轮:怪前端
```typescript
// First suspect: The profile loading hook
useEffect(() => {
if (user) {
fetchUserProfile(); // This was taking forever
}
}, [user]);
```
我到处加计时器。API 调用确实要 3 分钟。但为什么?后端要么返回数据,要么报错,不应该就这么……卡着。
第 2 轮:怪后端
我钻进了 Supabase Edge Functions:
```typescript
// Edge Function for getting user profile
const { data: profile } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (!profile) {
// New user - create profile
await createUserProfile(userId);
}
```
看起来没毛病,应该很快才对。那就继续加日志。
第 3 轮:剧情升级
日志加满(真的是_到处_都加)后,我看到了一段离谱时间线:
[00:00] User signs in with Google
[00:01] Auth trigger fires - creates user record
[00:01] Frontend requests profile
[00:01] Edge Function queries for user... NO RESULT
[00:02] Edge Function tries to create user...
[00:02] Database constraint error: User already exists
[00:03] Function retries...
[03:00] Function finally times out
等等,什么?用户不存在,但又已经存在?薛定谔用户?T.T
真相揭示
盯数据库日志盯到眼睛发酸后,我终于看到了根因。数据库里有两个进程在竞争:
- Supabase Auth Trigger:注册时创建用户记录
- Edge Function:查不到就尝试创建用户
- 数据库复制延迟:trigger 的 INSERT 还没同步到读副本
实际过程是:
-- Auth trigger (on primary database)
INSERT INTO users (id, email) VALUES ($1, $2);
-- Edge Function (reading from replica)
SELECT * FROM users WHERE id = $1; -- Returns nothing!
-- Edge Function (trying to help)
INSERT INTO users (id, email) VALUES ($1, $2); -- CONFLICT!
函数会用指数退避重试,每次都撞上同一个竞态,直到:
- 复制终于追上(1-3 分钟)
- 或函数超时(3 分钟)
更新:真正元凶与最终稳健解法
初版文章发出后,我们继续调试。虽然后端改动确实有改善,但谜团核心仍未完全解开。真正突破来自我们把焦点从后端转到客户端 hydration 与认证流程。
真正元凶:客户端竞态条件
问题不是慢 trigger,也不是冷 Edge Function。真正问题是一个典型客户端竞态:
- OAuth 重定向: 新用户用 Google 登录后跳回我们的应用。
- 异步会话建立: Supabase 客户端库(
supabase-js)开始异步处理 URL token,建立 session。 - 过早渲染: React 应用立即渲染,并在第 2 步完成前就去读取 session。
- 失败点: 应用拿到
nullsession,于是判定用户未登录,渲染空白或错误态。几秒后 session 才可用,但已经晚了——UI 早就做了错误决策。
我们之前的临时方案(例如客户端重试)只是对抗这个根本竞态的症状处理。
解决方案:认证状态单一事实来源
正确且最终的方案,是重构前端认证状态管理,让它真正事件驱动且稳定。
1. 逻辑集中到 SupabaseProvider:
我们把 SupabaseProvider 重构成认证状态唯一权威来源,移除了其他 hooks 里的重复监听与检查。
2. 正确使用 onAuthStateChange:
修复核心是 只 依赖 Supabase 的 onAuthStateChange 监听。
// Simplified logic in SupabaseProvider.tsx
export function SupabaseProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // Start in a loading state
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null);
// We only consider authentication "finished" once this listener fires.
setLoading(false);
}
);
return () => subscription.unsubscribe();
}, []);
// ...
}
这个模式保证:在 Supabase 明确给出“session 有效”或“session 为空”之前,整个应用始终处于 loading 状态。竞态不再存在。
Happy Ending
DIALØGUE 现在新用户 onboarding 用时不到 1 秒。注册后再也没有“喝杯咖啡再回来”的等待,也不会让用户怀疑是不是自己搞坏了什么。
这个修复已在线上跑了 3 周。超时问题 0 次,竞态问题 0 次。注册流程终于回到了它本该有的顺滑速度。
花一周调这个值不值?当我看到新用户注册后几分钟内就能顺利生成第一期播客时——绝对值。 :D
你有抓过那种“它同时存在又不存在”的 bug 吗?我感觉每个开发者都至少有一个薛定谔 bug 故事。很想听听你的版本。
致敬,
Chandler
想试试现在飞快的注册流程?来 DIALØGUE 创建你的 AI 播客吧。我保证它不再要 3 分钟了 :)





