问题背景
在业务实现当中,多线程并发操作会带来一些安全问题上的挑战。例如,在秒杀业务中,我们不仅要考虑多线程并发执行时对库存的考虑,还要考虑每个用户的请求是否由一个线程发出,当一个用户的请求由多个线程发出时,可能是脚本代刷的情况,这同样会导致业务出现异常。
方法级别的锁
假设我们的秒杀实现方法是:
public boolean yourServiceMethod(Long someId) {}
考虑到并发问题,我们选择对方法加锁,
@Transactional
public synchronized boolean yourServiceMethod(Long someId) {}
但是这样添加锁,锁的粒度太粗了,在使用锁过程中,控制锁粒度 是一个非常重要的事情,因为如果锁的粒度太大,会导致每个线程进来都会锁住,所以我们需要去控制锁的粒度。这样的加锁方式仍然没法满足防止同一用户的多个线程的请求。
理想的锁粒度
应该是细化到用户级别,而不是整个对象实例。比如在秒杀场景中,我们希望同一个用户的请求被串行化(一个用户不能同时发起多个秒杀请求),但允许不同用户同时进行秒杀。
用户级别的锁
锁的粒度应该根据业务需求进行调整,比如通过 用户ID 进行加锁。可以通过 synchronized(userId)
或者 synchronized(userId.toString().intern())
来锁定不同用户的请求,保证相同用户在一个时间只能有一个秒杀请求被处理,但允许不同用户并行秒杀。
事务失效
解决了实现用户级别的锁的问题,此时还是存在着问题。即:如果在方法内部加锁,可能会导致当前事务还没有提交,但是锁已经释放也会导致问题。所以选择将当前方法包裹起来,确保事务不会出现问题,同时也保证了锁的粒度:
// 获取用户ID
Long userId = user.getId();
synchronized (userId.toString().intern()) {
return this.yourServiceMethod(someId);
}
但是以上的方法仍有问题,因为我们调用的方法其实是this调用的,事务要想生效需要使用代理。
Spring 通过 AOP(面向切面编程) 实现事务管理,具体是通过代理对象来增强目标对象的方法,使其具备事务管理的功能。在代码中,@Transactional
注解被用来标识事务,Spring 会为该方法创建代理,代理对象负责开启、提交或回滚事务。(Spring默认使用JDK动态代理)
Long userId = user.getId();
synchronized (userId.toString().intern()) {
// 获取代理对象(事务)
Service proxy = (Service) AopContext.currentProxy();
return proxy.yourServiceMethod(someId);
}
通过代理进行外部调用,便可以解决事务失效问题。
外部调用 vs. 内部调用:如果是外部调用(从类外部调用目标方法),代理对象会接收到方法调用,并正确处理事务。然而,当类内部的方法调用类内另一个带有 @Transactional
注解的方法(即自调用)时,调用是直接通过 this
对象进行的,绕过了代理对象,导致事务拦截器无法介入处理事务逻辑。
Spring事务失效原因
- 自调用导致事务失效:Spring的事务管理是通过AOP代理实现的。如果一个类中的方法直接调用另一个标注了
@Transactional
的方法(自调用),事务不会生效。因为事务代理是在外部调用时才生效,内部调用绕过了代理。 - 方法不是
public
- 异常处理不当
- 事务传播行为不当
- AOP代理类型不匹配:Spring默认使用JDK动态代理处理事务,如果事务类没有实现接口且没有强制使用CGLIB代理,可能会导致事务失效。
- 数据库不支持事务:某些数据库引擎(如MySQL的MyISAM)不支持事务管理。如果使用了不支持事务的数据库引擎,事务自然不会生效。
- 多线程环境:Spring的事务管理是基于线程绑定的(ThreadLocal)。如果在多线程环境中使用事务,可能会导致事务失效,因为事务状态在不同线程间无法共享。