Spring 事务失效的3大情况概览
方法内自调用
解决办法:
- 把调用方法拆分到另外一个bean中
- 自己注入自己(Spring的缓存机制能够避免循环依赖,不放心可以加@Lazy注解)
- AopContext.currentProxy()方法获取当前的代理对象,用这个方法启动类要加注解@EnableAspectJAutoProxy(exposeProxt = true),同时也要引一下依赖
方法是private 或 final 的,代理对象就不能重写这个方法了,因此调用它的时候,实际调用的是extends的父类的普通方法
单独的线程调用方法:
当MyBatis执行sql的时候,会从ThreadLocal中获取数据库连接对象,如果开启事务的线程和执行SQL的线程是同一个,就能拿到数据库连接对象,如果不是同一个线程,就拿不到,这时会新建一个数据库连接,这个数据库链接默认直接提交
解决方法:不能在事务方法内开启多线程操作数据库,可以开启多线程给每个线程添加事务
Spring底层注入和事务注解的剖析
Spring的注入过程:
由表入里,层层递进,直至底层
从底层开始注入,底层注入之后注入上层
Spring注入细节:
假定一个类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component public class MyService{ @Autowired private MyMapper myMapper; public void simpleMethod(){ lockMethod_2(); return; } @Transactional public void lockMethod_1(){ lockMethod_2(); return; } @Transactional public void lockMethod_2(){ simpleMethod(); return; } }
|
这个类的注入流程如下:
- Spring创建一个真实的对象 myService
- Spring创建一个代理对象 myServiceProxy,继承MyService类
- SpringProxy中,把myService对象赋值给成员变量targetMyService(持有一个真实的MyService实现类对象),把上一层创建好了的myMapper对象也赋值给成员变量targetMyMapper
最终代理类伪代码长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public class MyServiceProxy extends MyService { private MyService targetMyService; private MyMapper targetMyMapper; { targetMyService = myService; } @Override public void simpleMethod(){ super.lockMethod_2(); return; } @Override @Transactional public void lockMethod_1(){ targetMyService.lockMethod_2(); return; } @Override @Transactional public void lockMethod_1(){ targetMyService.simpleMethod(); return; } }
|
方法自调用场景演示:
黑马点评P54,实现一人一单的代码那里:
在实现一人一单之前,通过 乐观锁 的方式解决了库存超卖问题
简单来说就是:

而在这个基础上,需要添加限制订单数量(这里是一人一单)的功能,这个功能需要 单独查一次数据库,因此这个方法中必然涉及两次数据库的操作,在这两次的间隙内,就会出现和超卖不加锁一样的问题,流程图如下:

因此,需要给:
数据库是否充足?
当前用户买过吗?
减库存
三个操作加上事务,保证这三步操作的的原子性,因此把这三步提取出来,当成一个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Transactional public synchronized Result createVoucherOrder(Long voucherId) {
Long userId = UserHolder.getUser().getId(); int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count(); if (count > 0) { return Result.fail("用户已经购买过一次!"); }
boolean success = seckillVoucherService.update() .setSql("stock = stock - 1") .eq("voucher_id", voucherId).gt("stock", 0) .update(); if (!success) { return Result.fail("库存不足!"); }
VoucherOrder voucherOrder = new VoucherOrder(); long orderId = redisIdWorker.nextId("order"); voucherOrder.setId(orderId); voucherOrder.setUserId(userId); voucherOrder.setVoucherId(voucherId); save(voucherOrder);
return Result.ok(orderId); }
|
但是这样做会产生一个问题,这就涉及到锁的底层原理,直接在方法上加锁,相当于锁住了 这个方法的 方法对象,导致这个方法只能单线程执行,但我们的目的不是锁这个方法,而只是想锁里面的三步操作
注意区别!锁方法和锁方法体还能不一样?当然不一样,重点就在于,锁这个过程究竟所的是什么东西:
方法:锁住方法对象
方法体:细节到真正需求,可以锁用户id,这样这个方法可以同时执行很多次,但是方法体内,同一个id只能顺序执行
所以改造成2.0版本:
1 2 3 4 5 6 7
| @Transactional public Result createVoucherOrder(Long voucherId) { Long userId = UserHolder.getUser().getId(); synchronized(userId.toString().intern()){ } }
|
**细节 userId.toString().intern()**:synchronized是锁一个对象,直接锁userId的话,其实同一个id进来,会new出新的userId对象,这很好理解,因此是锁不住的
所以,我们先把他 细节tostring,变成字符串包装类(此时也不行,也是对象)
再然后,调用 .intern() 方法,这个方法牵扯到jvm的底层对于一些数据类型的优化,在这里,就是内容相同的String对象的共同地址的缓存,因此可以将同一个id锁住
但是,为了最大限度的保证安全,我们可以这么做:这样也牵扯出了注解失效的场景
1 2 3 4 5 6 7 8 9 10 11 12
| { Long userId = UserHolder.getUser().getId(); createVoucherOrder(userId); }
@Transactional public Result createVoucherOrder(Long voucherId) { synchronized(userId.toString().intern()){ } }
|
所以应该改成:

当然,也可以自注入解决