同一个手机号,注册出了两个账号
· 分享镜
同一个手机号,注册出了两个账号
排查日志发现:同一个手机号,在 300ms 内发来了两个注册请求(网络抖动,客户端自动重试),两个都通过了"手机号是否已注册"的校验,两个都成功插入了 user 表。
数据库里,同一个手机号对应了两条记录。
为什么应用层校验拦不住
最直观的实现:
// 查一下有没有
User existing = userDao.findByPhone(phone);
if (existing != null) {
throw new BizException("该手机号已注册");
}
// 没有,插入
userDao.insert(new User(phone, ...));
两个并发请求同时执行"查一下有没有",都查到"没有",都继续执行"插入",都插入成功。
查和插之间有时间窗口,这是应用层校验天生的缺陷。并发量不大时这个问题不显眼,偶尔发生一次,数据就脏了。
数据库唯一索引:最后一道闸
真正能堵住并发插入的,是数据库的唯一索引:
ALTER TABLE user ADD UNIQUE INDEX uk_phone (phone);
并发两个插入,数据库行锁保证只有一个能成功,另一个抛 DuplicateKeyException。捕获异常,返回"该手机号已注册"即可:
try {
userDao.insert(new User(phone, ...));
} catch (DuplicateKeyException e) {
// 唯一键冲突 = 刚被别的请求抢先注册了
throw new BizException("该手机号已注册,请直接登录");
}
唯一索引不需要额外的锁,也不需要事务——数据库本身的并发控制就能保证只有一行写入成功。
三个需要唯一约束的字段
注册场景里,通常有多个字段需要唯一性保证:
| 字段 | 原因 |
|---|---|
phone | 手机号是主要登录凭证,必须唯一 |
email | 如果支持邮箱登录,同理 |
username | 用户自选昵称,需要唯一(如果业务要求) |
三个字段分别加唯一索引,不要合并成联合唯一索引——三个是独立的约束,任何一个重复都应该拦截。
第三方登录的特殊情况
微信/支付宝等第三方登录,绑定的是 open_id(每个应用对每个用户唯一),不是手机号。这里同样要在 (platform, open_id) 上加唯一索引:
UNIQUE INDEX uk_third_party (platform, open_id)
并发注册场景:用户用微信扫码,微信侧返回了 open_id,客户端连发了两次注册请求,同样会触发重复插入。唯一索引同样能兜住。
数据库唯一索引是防止数据重复的最终防线,不能省。应用层的"查了再插"是用户体验优化(快速返回友好提示),不是并发安全保证。两层都要有,但不能只靠应用层。
评论(0)
登录后参与评论。
还没有评论,来抢沙发吧。

