1.修改丢失:2个修改同时对一个数据进行修改时,其中一个的修改结果被另一个给覆盖了
举例:A发起了购买商品1的请求,先看商品库存,得到剩余库存为2,可以购买,但是还没等A提交扣减库存,B也发起了购买商品1的请求,B得到剩余库存为2,同时提交了扣库存,库存改为了1,等B请求完成后,又继续处理A的请求,由于A查询的剩余库存为2,所以扣库存后剩余库存为1。这时就存在问题了,按理2次请求后的剩余库存为0,但是最终结果是1,这就是因为A的请求把B的请求的修改结果覆盖了。
2.网上给出的解决方案:给查询商品库存的sql语句后面跟上for update,即给查询加上了排他锁,这样只有等排他锁更新完成后,才能被其他请求查询更新。比如下面的语句:
select store from goods where goods_id=1 for update
然而我碰到的问题是:for update 仍然不齐作用!!!
3.描述一下我的业务中for update不起作用的情况:我的系统是运输系统,需要司机做微信小程序打卡操作,同时给车辆按装了GPS设备,系统每分钟获取一次车辆位置判断车辆是否在各装卸点的电子围栏内,并更新运输单的状态。当司机到了目的地后打了卸货卡,运输单状态应该改为已到达,但是有时候司机打卡的结果就被每分钟自动刷新定时任务给覆盖了,造成了修改丢失!我查看了后台日志,发现:每分钟定时任务开始更新某一张运输单A的状态了,查询出来的状态还是司机未打卡之前的运输中状态,在处理的过程中司机进行了打卡操作,并且修改了运输单的状态为已达到,等司机打卡操作完成后,系统继续处理定时任务,而定时任务开始的时候查询的状态仍然是运输中,定时任务在此基础上修改运输单,导致司机打卡的状态丢失。
此处分为2个类中的2个方法:
1.司机操作类-司机打卡方法,2.定时任务类-运输单状态定时任务方法
司机打卡方法上开启了事务,加了注解@Transactional,定时任务上未加事务注解
我在这2个方法中,查询运输单的SQL中都加了for update,但是仍然会修改丢失
4.分析一下我这边for update失败的原因:
for update必须在开启事务的前提下才会生效,而我的运输单状态定时任务方法中并没有开启事务。
我的司机打卡方法是开启了事务的,所以一旦发生定时任务先查询了运输单,进行状态更新的过程中司机同时打卡时,由于司机打卡是有事务的,所以会优先保证司机打卡方法进行从头到尾完整操作,司机打卡方法处理完成后才会继续来处理定时任务方法,这样就导致了司机打卡的状态被定时任务给覆盖了。
5.解决方法
可以直接在定时任务方法上加@Transactional开启事务。但是由于每次定时任务执行时会有多条运输单需要判断状态,如果在方法上开启事务,那么会造成运输单表锁定时间太长,会影响其他方法查询运输单表。所以只对每个运输单判断状态这部分代码进行开启事务,就需要把判断运输单状态这部分代码单独提出来写一个新的方法,当定时任务循环每个运输单时,就调用判断状态的方法。这样既不会锁定表时长过长,又能保证每个运输单自己的状态判断不会产生修改丢失。
然而这样问题又来了,就是单独提取的这个判断运输单状态的方法虽然加了@Transactional,但是由于定时任务方法并没有开启事务,这样就形成了下面的问题:未开启事务的方法调用同类中开启事务的方法会导致事务失效。
解决这个调用方法事务失效的办法有2个:
1.把判断运输单状态的方法写入新的类中,用定时任务方法调用这个新的类中开启了事务的方法
2.直接用类代理对象来调用本类中的开启了事务的方法
用第一种方法会多谢冗余类,后期维护会比较麻烦。所以本次处理用来第二种方法。具体操作步骤如下:
首先:去系统启动类上开启代理
@SpringBootApplication
@MapperScan(basePackages = "com.xfl.xtms.mapper")//spring容器启动时就扫描加载了dao
@ServletComponentScan(basePackages = "com.xfl.xtms.listener.*")//开启监听器
@EnableScheduling//开启定时任务功能
@EnableAsync//开启异步功能
@EnableAspectJAutoProxy(exposeProxy = true)//开启获取代理对象
public class FltmsApplication {
public static void main(String[] args) {
SpringApplication.run(FltmsApplication.class, args);
}
}
其次:在未开启事务的方法中,获取类代理对象
//定时任务1:每1分钟执行一次,修改在途运输单的时效状态以及单据状态
@Scheduled(cron = "0 */1 * * * ?")//下一个0秒执行,每隔1分钟触发任务
@Async//允许异步执行
public void updateTransportBillNotice() {
//获取当前类SchedulingTask的代理对象,调用开启事务的方法
SchedulingTask schedulingTask= (SchedulingTask) AopContext.currentProxy();
.....
.....
}
最后:在需要调用开启了事务方法的地方调用本类中开启了事务的方法
//定时任务1:每55秒执行一次,修改在途运输单的时效状态以及单据状态。检查车辆是否停留超1小时 @Scheduled(cron = "0 */1 * * * ?")//下一个0秒执行,每隔1分钟触发任务 @Async//允许异步执行 public void updateTransportBillNotice() { //获取当前类的代理对象,调用开启事务的方法,进行排他锁修改 SchedulingTask schedulingTask= (SchedulingTask) AopContext.currentProxy(); .... .... for (TransportBill transportBill : transportBillList) { //调用开启了事务的方法,防止类内部未开启事务的方法调用内部开启事务的方法而事务失效 schedulingTask.refreshTransportBillNotice(transportBill.getId()); }//循环修改每个运输单 .... .... } //开启了事务的刷新运输单状态方法 @Transactional public void refreshTransportBillNotice(int id){ .... .... }
6.关于for update的排他锁级别的说明
1.如果sql语句的查询条件中不涉及主键或索引字段,那么for update查询的结果是把整个表给加上排他锁
2.查询条件包含主键或者索引字段,这样查询的结果是只给查询结果那一行加上排他锁。
3.mysql中innodb引擎时,for update才生效