场景是这样的,用户发起一笔交易,从一个数据域扣一个数值,生成一个订单。
在最初的代码里,没有对这块进行一些处理,从业务逻辑上来看,是可以走通的。
业务逻辑完整,于是我在测试类中,创建了一个多线程的测试方法,来同步访问逻辑层的处理接口。这时候就可以看到数据出现异常了:一笔扣除,产生了多条订单。
原因其实很自然,多个线程同时访问逻辑层时,“查校账户数据”那步,本质上是拿出此刻的数据,然后做一个判断。在你还未修改这个数据之前(持久化),所有同步过来的判断都为“真”,没有任何控制的情况下,就会插入多笔同样的订单。当然,在每次请求都有一定间隔的情况下,很难出现。
这时候可以有多种处理方式,并不一定每种都是最好或不好,只就能实现来说说。
使用synchronized或lock锁synchronized可以直接在接口方法上加,改起来自然是最快的,如果你的事务在逻辑处理上没有问题,这样做在单节点的情况下,确实可以实现。但是自然不推荐。
synchronized锁的是对象,这样会让你实现类里其他的方法也阻塞了,程序响应会很长,肯定是非常不可取的。再加之即使是锁了 方法,那不同用户操作不同账户,应可并发进行更合理,所以这样一来,这个情景下,这两个都可以排除。
使用数据库行级锁分析了情景后,发现主要问题其实是在一条数据在使用过程中,在他的某值在某个合理数值下时,做一些其他操作,之后来修改这条数据的这个值。因此,在使用这条数据时,对它进行锁定就行了。当然,这必须要在一个事务内完成。
实现这个,在springboot中也比较简单,首先,在实现方法上打开事务:
@Transactional然后在做更新这条数据和拿到这条数据做检验之间,进行锁定。
以mysql,Oracle为例,简单点的策略,在select语句后后,加上for update,就可以实现锁,但是这里一定注意,确保你的where字段让你的SQL定位到一行为好,特别是这种情形,比如唯一的用户主键、订单主键来做定位条件会比较好。
而这个时候的释放锁,也很符合业务逻辑,即update这个数据对象后,就会自动释放锁。
示例:
// 获取行级锁 Order orderLock = orderMapper.getOrderLockById(order.getId()); if(orderLock.getStatus().equals(OrderStatus.SUCCESS)) { orderLock.setStatus(OrderStatus.TRANOUT); // 其他逻辑 }// 释放锁 orderMapper.updateByPrimaryKeySelective(orderLock);这样通过事务和行级锁,即使并发,程序应用依然会在数据库有行锁的情形下,只对那行数据操作一次,直到它完成当次的任务。