|

Aimee

Write the Code. Change the World.

同一个手机号,注册出了两个账号

· 分享镜

同一个手机号,注册出了两个账号

排查日志发现:同一个手机号,在 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

登录后参与评论。

还没有评论,来抢沙发吧。

回到顶部