用户注册流程

重复ID如何设置索引_并发编程

大致流程是:判断手机号是否存在,不存在则新增;id生成依赖micro-basic-service微服务的id生成器,拿到id后用户中心服务再执行插入用户记录。

虽然加了事务,此流程并发时会存在一些问题。

并发场景:

重复ID如何设置索引_并发编程_02

第一个问题:id生成失败

micro-basic-service 会报id生成失败。
原因:两个事务/进程同时来到第④步,假如查到的table_id=10,然后进程1率先执行第⑤步,把table_id自增1最终更新数据为11;此时,进程2执行到第⑤步,设置table_id = 11,会导致update rows affected = 0 引起id生成失败。

并发模拟如下

重复ID如何设置索引_mysql_03

重复ID如何设置索引_排它锁_04


并发执行手机号注册,出现了一个失败一个成功的情况。

解决思路:

把第④和第⑤步做成原子,对查询做排它锁,使用排它锁 for update。

重复ID如何设置索引_并发编程_05


实际代码如下

重复ID如何设置索引_mysql_06

对于并发导致的id生成失败的问题就解决了,但是还存在另一个问题。

重复ID如何设置索引_mysql_07


可以看到,并发两个注册,虽然不报数据库主键生成失败的错误了,但是还存在另外的问题,就是重复注册。

第二个问题:重复注册

问题的关键在于

select mobile ... where mobile = 15218621444
if !mobile {
    getNextId
    insert
}

两个进程同时来到第②步,发现不存在手机号,并发走到第③至第⑥的最终插入,由于手机号没做唯一索引,引起了相同手机号注册。
只要通过了第②步,就会进入注册逻辑。
解决思路:
同个手机号并发注册时,同时只有一个进程通过第②步。
一开始我给第②步,加上select mobile … where mobile = 15218621444 for update,结果是不生效的,实验后,发现对于不存在记录的 select for update,数据库不知道你要锁哪一行,也不会阻塞表。
而后又想了这些方案:
(1)用锁表解决,比如select count(1) where 1 … for update。让他阻塞,但是锁表性能不行。
(2)另外一种是用redis缓存锁排队。
(3)还有一种最简单的是加唯一索引。
(4)对于第一种锁表的改造,用冷数据一定能命中查询来致使锁行阻塞。

如select id order by id asc limit 1 … for update ,然后再执行后面的业务逻辑

select mobile … where mobile=15218621454;
 if !mobile
 {
 insert …
 }


牺牲一个冷数据的性能来做排它锁。
于是我试验了第四种方案,并得到验证是可行的。

重复ID如何设置索引_并发编程_08

实际代码如下

重复ID如何设置索引_排它锁_09

重复ID如何设置索引_排它锁_10


并发执行效果:

重复ID如何设置索引_并发_11


并发执行,只有一个成功。

总结:

对于先查询,后修改的场景,可以使用排它锁保证代码段落原子性。

个人觉得,多个服务之间的调用,服务2是id生成器是生成id并返回,即使服务1事务失败也不需要回滚服务2的事务,所以这个案例不需要用到分布式事务的技术。