-
首先这可能不是一个很简单的问题,很多程序员大概会这么写
这样写有问题吗?明显,并发情况两个用户同时进入17行,都查询到 null,然后都执行22行,数据库插入了两条 同样的 userName。这写法不可用。
-
既然数据库会重复,那么我们就在数据库userName字段加上唯一索引,改成下面写法
现在有问题没?明显重复的问题解决了。但是问题来了,产品要求我们数据库可以删除用户,但是不能物理删除用户,删除用户以后userName依旧可以注册,或者说这个不是userName,而是唯一的用户游戏昵称。
-
然后张三想到假删除字段和userName建立唯一索引
这时候可用吗?假删除以后,确实可以可以用这个userName重新注册,问题来了,这个新的userName还能再次删除吗?明显不能,唯一索引冲突。
-
这时候李四说我有办法,我假删除字段不用0和1 表示,我用0 表示没删除,删除时间的毫秒数表示已删除。这样就能重复删除了。
问题解决了吗?很大程度上解决了,毕竟注册的时候 userName 和 假删除字段0 只能存在一个,所以删除不可能同时删除2个,不会重复的删除时间,即便允许恢复删除,也只能吧对应userName并且状态是0 的先删除,然后再吧已经删除的改成 0,完美解决。
- 王二麻子这时候说话了,我们别的假删除都是0,1,你搞个球呀,风格另类而且一个bit能能表示的状态,你用了一个Long来存。我可以加个redis锁来解决这个问题,不用数据库的唯一索引,不搞风格不统一的假删除字段。
能解决问题吗?测试经过简单的测试,确实没问题,然后项目上线稳定允许3个月,突然有一天出现了2个userName重复的用户,大家使劲找,使劲查日志,检查代码终于发现上面37行锁释放以后事务还没有提交,但是另外一个事务已经可以获取这把锁了,另外一个事务读不到当前事务未提交的数据,然后也写了一条,userNam就重复了。
-
王二麻子说小问题,锁在事务提交之前我就释放,才出的问题,那么我就在事务提交之后在释放。
这下锁在事务执行以后释放了,完全没得问题,测试随便测测,确实没得问题上线稳定允许了1 年,确实没有在出现userName重复的问题。
-
一年后,因为项目稳定允许,给公司挣了很多钱,隔壁公司把他们整个团队都挖走了,包括王二麻子和产品经理。新来的产品经理决定在创建用户的时候初始化用户钱包。新来成程序员老李直接在插入用户下面加了一句初始化用户钱包,毕竟已经有@Transactional了,所以在一个事务环境,可以保持钱包和用户同时完成或者同时回滚。
更新上线以后,初始化钱包的逻辑是有bug,结果钱包没有初始化,用户却创建成功了。大家找了半个月然后发现doRegister是内部调用,上面的事务注解根本没生效,只是因为之前没有多次更新所以没有出问题.
-
一查代码王二麻子写的,然后大家一起骂王二傻逼,并且做了修改。
-
然后问题终于解决了用户注册和钱包初始化,终于保持一致性,之前的隐藏坑也填上了,大家开始愉快的迭代,过上了混吃等死的日子。
-
为了提高注册用户数量,产品搞了一个活动,扫码注册送电瓶车,扫码以后拿到手机号作为userName,然后写入抽检结果,并且注册。老李吧程序改成下面这样。
上线以后,活动地点网络太差,大量网络重试,同一个手机号瞬间多次执行抽取电驴的方法,然后大家发现又出现了很多重复的userName。这个userName重复的问题怎么没完没了?隔壁公司被挖过去的张三歪嘴一笑,老子早就说了用userName+删除时间做唯一索引了,你们都被王二麻子坑了!
-
老李仔细的查找2天终于发现了,原来释放锁必须要在doRegister所在的事务提交以后,register这个方式之前是没有事务环境的,现在套了一个抽奖的方法,所以就出问题了。
-
问题找到了,但是怎么解决呢?注册的doRegister要求独立提交,register要求以非事务的方式执行,抽奖方法 要求 注册和抽奖 在一个事务里面执行,这他妈是矛盾的呀?只能吧加锁的力度提到抽奖的外层,但是问题有又来了。
然后问题解决了,但是之前的原始注册没法用了。
- 老李现在很烦,他现在快爆炸了,好麻烦,好多坑。但是他又暗暗窃喜,幸好老子是Java程序员,知道重入锁,要是别的没有重入锁语言的程序员肯定想不到可以用重入锁解决。
问题又得到圆满的解决,但是我们用了这么多技术,踩了这么多坑,我们是不是忘记了我们只是想注册保持userName唯一而已?这个要求很难吗?很难吗?为什么踩了那么多坑,用了那么多特殊的技术?改了12个版本,至于吗?
-
老李终于还是疯了,被不断变化的需求和不断出现的坑逼疯了。老李疯了以后,老板直接吧老李裁了,然后花高价把王二麻子挖了回来。通过这段时间的成长,王二麻子看着被改的面目全非的代码心里蹦出两个字垃圾。所有的问题好像都是引入redis锁开始,然后问题就像滚雪球一样的出,虽然总是可以解决,但是这里真的必须要用redis的锁吗?数据库写入操作不是已经有写锁了吗?而且这个写锁,基本有了redis锁以后,依旧不会还会有,2把锁?更安全吗?没有,更加麻烦倒是真的。然后王二麻子吧代码改成下面这个样子。
查询用户并且拿到写锁,后面直到事务提交的锁才释放,不会有锁提前释放的问题,不会有上层事务的影响,锁少了一把,锁定后执行的逻辑也没有变多,代码还少了很多,逻辑也简单了,redis锁的效率会更高吗?不见得,正常影响效率的不是获取 锁的时间,而是等锁的时间和获取锁以后做的事情。用redsi锁,依旧会获取数据库写锁,只是拿到锁之后做的事情明显 for update 更加少。在可以命中索引的情况下,for update 也是行级锁。锁的力度不会比redis 指定key 大。