高并发问题优化
几种常见事务失效原因
方法自调用导致的事务失效
- 场景:在Spring中,声明式事务通常是通过AOP(面向切面编程)实现的,这意味着事务管理是通过代理对象对目标方法的调用进行增强的。然而,当同一个类中的方法A调用方法B,且方法B上使用了
@Transactional
注解时,如果这种调用是直接进行的(即非通过代理对象),那么事务的增强处理就不会被触发,从而导致事务失效。 - 原因:AOP代理通常是通过Spring的容器在运行时动态生成的,它仅对外部调用进行拦截和处理。对于类内部的直接方法调用,由于绕过了代理对象,因此无法应用事务的增强。
- 解决方案:既然调用的不是代理对象,那我们就想办法获取代理对象,通过
AopContext.currentProxy()
获取当前类的代理对象,并通过该代理对象调用方法B。但请注意,这种方式需要确保在Spring配置中启用了@EnableAspectJAutoProxy(exposeProxy = true)
异常处理不当导致的事务失效
场景:如果
@Transactional
注解的方法内部捕获了异常,并且没有将异常重新抛出,或者没有将捕获的异常类型指定为需要回滚的异常类型,那么Spring默认不会触发事务的回滚。原因:Spring根据异常的抛出情况来决定是否回滚事务。默认情况下,只有运行时异常(
RuntimeException
及其子类)和错误(Error
)会触发回滚。对于受检异常(Exception
的子类,但排除RuntimeException
及其子类),除非在@Transactional
注解中明确指定,否则不会触发回滚。解决方案
确保事务内部的方法被正确抛出
使用
@Transactional
注解的rollbackFor
属性来指定需要回滚的异常类型,包括受检异常。
非Public
方法上的事务失效
- 场景:如果
@Transactional
注解被放置在非public方法上(如private、protected或默认访问权限的方法),那么事务将不会生效。 - 原因:Spring在创建代理对象时,仅会扫描和处理public方法的注解。非public方法由于访问权限的限制,无法被代理对象拦截和处理。
- 解决方案:将事务的方法改为
public
修饰
事务传播行为不对导致的事务失效
- 场景:在一个事务方法中如果调用了其他事务方法,而事务的传播行为不当,例如使用了
REQUIRES_NEW
,就表示了进入该方法创建了一个新事务,这样会导致事务失效 - 原因:在抛出异常时,该方法作为独立事务,不会随主方法进行回滚
- 解决方案:慎用传播行为
没有被Spring管理导致的事务失效
- 场景:若一个方法没有被
Spring
管理,则事务失效 - 原因:若不被
Spring
管理,就没有人替你生成代理对象,事务自然失效 - 解决方案:注意该类有没有被
Spring
所管理
额外知识补充(事务传播行为)
- PROPAGATION_REQUIRED(默认):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
- PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- PROPAGATION_REQUIRES_NEW:创建一个新的事务,并暂停当前事务(如果存在)。
- PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则把当前事务挂起。
- PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则行为等同于
PROPAGATION_REQUIRED
。
高并发时的优化方案
变同步为异步
适用范围
变同步为异步主要用于那些业务逻辑复杂、处理时间较长、且对实时性要求不是非常高的场景。具体来说,当系统面临大量并发请求,且每个请求都需要进行多个数据库操作或调用多个远程服务时,采用同步处理方式会导致线程阻塞,影响系统整体的并发能力。此时,可以考虑将部分或全部业务逻辑异步化,以提高系统的响应速度和吞吐量。
优化方案
我们可以通过MQ(消息队列)将同步业务变为异步业务,从而提高效率,在业务处理流程中,当需要异步处理的任务产生时,将任务封装成消息发送到MQ的交换机中。交换机根据路由规则将消息发送到指定的队列。创建消费者监听队列中的消息。当队列中有消息时,消费者从队列中获取消息并进行处理。
优点
- 无需等待复杂业务处理,大大减少响应时间
- 利用MQ暂存消息,起到流量削峰整形作用
- 降低写数据库频率,减轻数据库并发压力
缺点
- 依赖于MQ的可靠性
- 降低了些频率,但是没有减少数据库写次数
合并写请求
适用范围
合并写请求主要用于那些写操作频繁且数据关联度较高的场景。例如,在电商系统中,用户可能同时购买多个商品并生成多个订单记录,这些订单记录可以合并为一个写请求发送到数据库进行处理。此外,对于日志记录、数据同步等场景,也可以考虑将多个写请求合并为一个进行处理。
优化方案
合并写请求就是指当写数据库并发较高时,不再直接写到数据库。而是先将数据缓存到Redis,然后定期将缓存中的数据批量写入数据库。由于redis时内存操作,所以写的效率大大提高,同时减少了DB操作,对数据库的压力减小
优点
- 写缓存速度快,响应时间大大减少
- 降低数据库的写频率和写次数,大大减轻数据库压力
- 降低网络开销,提高数据传输效率
缺点
- 实现相对复杂
- 依赖Redis可靠性
- 不支持事务和复杂业务