在业务中,为了保证数据的一致性、准确性,常常采用了事务处理,但是这往往还不够,因为在网络应用中还存在了并发问题,因此很多时候还需要加上锁机制。而在这两者一起使用的时候,就可能会掉进一个陷阱里面。
USAGE
以我们公司的web应用为例,应用使用了TP
框架,并利用其标签位功能实现了申明式事务处理机制。只需要在配置文件中配置好哪些控制器方法需要开启事务即可,当这些控制器方法被访问时,会自动执行
M()->startTrans();
然后在方法结束时触发的标签位方法中检测是要
M()->rollback();
还是
M()->commit();
核心部分时序图如下
其中锁机制是利用
redis
实现的,这里用用户id
做了键来进行防止用户连续、快速点击造成的并发请求。
问题
没有事务的时候
- 第一个请求获得锁
- 读取表中的旧数据
- 对表中旧数据做相应判断后,修改表中数据
- 释放锁
- 第二个请求活动锁
- 读取表中的数据,此时获得的是第一个请求修改后的数据
- 剩余业务处理
- 释放锁
有事务的时候
- 开启事务
- 第一个请求获得锁
- 读取表中的旧数据
- 对表中旧数据做相应判断后,修改表中数据
- 释放锁
- 第二个请求获得锁
- 读取表中的数据,此时获得的是仍是旧数据,因为第一个请求的修改还在事务日志当中
- 执行后续业务代码,会产生和第一次请求相同的结果
- 释放锁
- 第3、4、5…次请求…
- 第一次提交事务
- 第二次提交事务…
相信已经能很明显的看到问题所在了,比如领取优惠券,如果采用上诉方式的话,用户连续点击领取(当然,前端的限制很容易绕过),就会同时插入多条领取成功的记录,即是领取了多张一样的优惠券,这就是一个八阿哥了。
流程修改
虽然申明式事务用起来确实很方便,但是除了会多少降低效率外,还会导致上面的问题,因此对此进行修改,对核心的业务操作代码使用手动控制事务的开启和关闭,并把这段代码加锁处理,这样,每次请求在提交事务后,才会释放锁,保证:每次获得锁的请求,从表中获取的数据都是最新的数据