业务场景如下:
用户频繁(疯狂)点击送礼物按钮给主播刷礼物
代码实现业务步骤:
1.查询礼物金额
2.查询用户钱包金额
3.查询钱包余额是否充足
4.用户钱包减去礼物金额
5.主播钱包增加礼物金额
代码方案:
1.因为涉及金额问题,且操作中需要对用户和主播的钱包都进行相应操作,所以需要使用@Transactional事物注解。
2.因用户频繁点击场景,为了防止用户钱包余额校验等金额问题出现异常,需要使用分布式锁或synchronized单机锁进行安全处理。
为什么要设计的这么严格:如果线上用户/主播钱包出现问题,赔不起。。。。
单机场景代码思路:
@Service
public class DemoService{
@Transactional
public void synchronized update() {
//1.查询礼物金额
//2.查询用户钱包金额
//3.判断用户余额是否充足
//4.用户钱包减钱
//5.主播钱包加钱
}
}
问题产生:测试同学在对该功能进行测试时发现------在疯狂点击(频繁请求接口)的情况下,用户钱包金额会被扣为负值。
问题原因分析:增加日志打印个步骤逐行分析发现,第一次请求完成后,第二次请求进入代码查询出的钱包金额与第一次请求的原始钱包金额是一样的,说明第一次请求完成后事物还没有提交完成,但是第二次请求已经查询了用户当前的钱包金额,所以造成了测试出的问题,出现了脏数据。
问题根源:@Transactional注解使用SpringAOP实现,会在update()方法执行之前开启事物,之后执行update()方法synchronized加锁,当方法执行完成synchronized锁释放,再提交事物结束整个流程。
问题就产生在锁释放后,马上有一个线程请求进来开始执行方法读取用户钱包金额,但这时第一次请求的事物并没有提交完成,用户最新的钱包数据还没有写入数据库。所以第二次请求读取的用户钱包数据其实就是错误的脏数据。
解决方案:
1.首先在业务上对用户钱包进行update操作时进行where校验,避免出现负数或金额不足仍可进行送礼物操作的可能。
sql如下:
update user_wallet set
deposit = deposit - #{deposit},
update_time = now()
where user_id = #{userId} and deposit >= #{deposit}
2.避免synchronized的方法被@Transactional注解注释,但可以将synchronized写在方法外层,如将上面的synchronized移动到controller层,再将service层的方法标注上@Transactional事务注解
如下所示:
@Controller
public class DemoController{
//controller层使用同步锁
public void synchronized updateUser(){
demoService.update();
}
}
@Service
public class DemoService{
//service层开启事物
@Transactional
public void update() {
//1.查询礼物金额
//2.查询用户钱包金额
//3.判断用户余额是否充足
//4.用户钱包减钱
//5.主播钱包加钱
}
}