Spring 事务失效的3大情况概览

  1. 方法内自调用

    解决办法:

    1. 把调用方法拆分到另外一个bean中
    2. 自己注入自己(Spring的缓存机制能够避免循环依赖,不放心可以加@Lazy注解)
    3. AopContext.currentProxy()方法获取当前的代理对象,用这个方法启动类要加注解@EnableAspectJAutoProxy(exposeProxt = true),同时也要引一下依赖
  2. 方法是private 或 final 的,代理对象就不能重写这个方法了,因此调用它的时候,实际调用的是extends的父类的普通方法

  3. 单独的线程调用方法:

    当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;
}

}

这个类的注入流程如下:

  1. Spring创建一个真实的对象 myService
  2. Spring创建一个代理对象 myServiceProxy,继承MyService类
  3. 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;

//类似构造函数的东西,把创建好的myService对象和myMapper赋值
/*一个函数*/{
targetMyService = myService;
//mapper略
}

@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,实现一人一单的代码那里:

在实现一人一单之前,通过 乐观锁 的方式解决了库存超卖问题

简单来说就是:

f93539f6e999f22039fd14104a584e4c

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

52a43d89cfda781c2315be517cfcf6c0

因此,需要给:

数据库是否充足?

当前用户买过吗?

减库存

三个操作加上事务,保证这三步操作的的原子性,因此把这三步提取出来,当成一个方法

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();
// 5.1.查询订单
int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
// 5.2.判断是否存在
if (count > 0) {
// 用户已经购买过了
return Result.fail("用户已经购买过一次!");
}

// 6.扣减库存
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1") // set stock = stock - 1
.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0
.update();
if (!success) {
// 扣减失败
return Result.fail("库存不足!");
}

// 7.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 7.1.订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 7.2.用户id
voucherOrder.setUserId(userId);
// 7.3.代金券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);

// 7.返回订单id
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); //相当于自调用,this.调用,事务注解失效了
}

@Transactional
public Result createVoucherOrder(Long voucherId) {
synchronized(userId.toString().intern()){ //这里很细节
//原有逻辑
}
}

所以应该改成:

image-20251104012043581

当然,也可以自注入解决