用户注册功能的设计与实现
一、需求背景
实现用户两步式注册功能:
步骤1:填写注册信息(username, password, nickname, email),点击”下一步”发送验证码
步骤2:输入邮箱验证码,完成注册并自动登录
技术栈:Python + FastAPI + SQLAlchemy + Redis + PostgreSQL
二、核心问题
2.1 验证顺序问题
初版实现先验证邮箱验证码,再检查用户名/邮箱是否存在。问题在于验证码验证成功后会被删除,如果后续检查失败,用户需要重新获取验证码。
解决方案:调整验证顺序,先做轻量数据库查询,最后验证验证码。
# 优化后的顺序
1. 检查用户名是否已存在
2. 检查邮箱是否已注册
3. 验证邮箱验证码(确保前面都通过)
4. 创建用户
2.2 两步式注册的数据存储问题
第一步填写的注册信息需要临时存储,在第二步验证时使用。核心问题:
- 存储位置:前端还是后端?
- 并发冲突:不同邮箱抢注同一用户名如何处理?
- 数据过期:如何清理未完成的注册数据?
三、方案对比
3.1 方案 A:Redis Token 存储
数据结构:
Key: register_pending:{register_token}
Value: {username, password, nickname, email}
Expiry: 10分钟
流程:
- pre-register:检查用户名/邮箱 → 生成 token → 存 Redis → 发验证码 → 返回 token
- complete-register:根据 token 获取数据 → 验证码验证 → 创建用户
问题:不同邮箱可以同时预注册相同用户名,导致后完成的注册失败
3.2 方案 A+:双重索引
数据结构:
register_pending:email:{email} = {username, password, nickname}
register_pending:username:{username} = {email} # 索引
改进:在 pre-register 时检查用户名是否被其他邮箱占用
优点:彻底解决并发冲突
缺点:实现复杂,需维护索引一致性和过期
3.3 方案 C:简化存储 + 分布式锁(最终选择)
设计思路:体量较小,决定允许重复预注册,在创建用户时用分布式锁保证并发安全
数据结构:
register_pending:{email} = {username, password, nickname} # 10分钟过期
email_code:{email} = "123456" # 5分钟过期
register_lock_username:{username} # 分布式锁,10秒超时
register_lock_email:{email} # 分布式锁,10秒超时
选择理由:
- 实现简单,不需要维护索引
- 通过分布式锁保证并发安全
- Redis 过期机制自动清理数据
- 并发冲突概率低,用户体验可接受
四、技术实现
4.1 核心流程
pre_register:
def pre_register(params):
# 1. 检查数据库
# 2. 存储到 Redis(以邮箱为key)
# 3. 发送验证码
complete_register:
def complete_register(params):
# 1. 获取 Redis 中的注册信息
# 2. 验证邮箱验证码
# 3. 获取分布式锁
# 4. 再次检查(防止并发)
# 5. 创建用户
# 6. 清理 Redis
# 7. 自动登录
4.2 分布式锁设计
为什么需要双重锁?
场景:用户A和B同时用不同邮箱注册相同用户名
只锁邮箱:
├─ A获取email:test1@qq.com锁,B获取email:test2@qq.com锁
├─ A创建用户testuser,B创建用户testuser
└─ 数据库报错 ❌
双重锁(用户名+邮箱):
├─ A获取username:testuser锁
├─ B尝试获取username:testuser锁 → 阻塞
├─ A创建用户 → 释放锁
└─ B获取锁 → 检查 → 用户名已存在 ✅
锁参数:
timeout=10:锁自动过期时间,防止死锁blocking_timeout=3:获取锁的最大等待时间- 嵌套释放:确保锁按顺序释放
4.3 并发场景
| 场景 | 处理方式 | 结果 |
|---|---|---|
| 用户修改信息重新提交 | 同邮箱覆盖 Redis 数据 | 使用最新信息注册 ✅ |
| 不同邮箱抢注同一用户名 | 分布式锁串行化 | 后者检查失败 ✅ |
| 同一用户并发点击 | 第一次创建,第二次 Redis 已清理 | RegisterExpiredException ✅ |
五、方案对比
| 维度 | 方案A(Token) | 方案A+(双索引) | 方案C(分布式锁) |
|---|---|---|---|
| 实现复杂度 | 低 | 高 | 中 |
| 并发安全 | 中 | 高 | 高 |
| 用户体验 | 中 | 高 | 中 |
| 可维护性 | 高 | 低 | 高 |
选择方案C的原因:并发抢注概率低,方案C用较低的实现成本达到可接受的效果。
六、总结
- 验证顺序优化:先轻量级检查(数据库查询),后宝贵资源验证(验证码)
- 分布式锁应用:通过双重锁(用户名+邮箱)保证并发安全
- 双重检查机制:pre-register 检查一次,complete-register 再检查一次
- 自动清理:Redis 过期机制自动清理未完成的注册数据