Spring非事务方法使用事务的性能问题及使用建议
一、现象
弱网环境下通过@Autowired注入service获取数据,以下两种形式性能差距巨大(均无数据库操作)。
- 代码生成的service
继承了CrudServiceImpl的Service
public class ChannelConfigServiceImpl extends CrudServiceImpl<ChannelConfigDao, ChannelConfigDTO> implements ChannelConfigService {
public List<SmsTemplateConfigDTO> findSmsChannelTemplateInfo(ChannelTemplateConfigDTO configDTO) {
return null;
}
}
循环10,000次调用耗时26秒(数据库在远程)。
- 移除继承的service
简单Service
public class ChannelConfigServiceImpl implements ChannelConfigService {
public List<SmsTemplateConfigDTO> findSmsChannelTemplateInfo(ChannelTemplateConfigDTO configDTO) {
return null;
}
}
循环10,000次调用耗时45毫秒。
二、跟踪
机器配置
CPU:I7-9750H(12c)
内存:16G
调用情况
自定义如下线程池,充分利用CPU多核性能,共执行10,000次调用。
自定义线程池
private static final ThreadPoolExecutor THREAD_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
1000L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100000),
r -> new Thread(r, "sms_pool_" + r.hashCode()));
执行过程中的异常情况
- CPU异常,并发下占用率极低
- 网卡流量异常,本地调用不应该产生网卡流量
- 查看GC未见异常
- 线程皆为RUNNING状态,未发生等待和阻塞,说明线程一直在执行逻辑
- 根据以上情况怀疑线程存在远程调用,dump线程查看线程状态,发现大量线程在等待socket返回mysql数据
- 根据线程堆栈定位到sql执行位置com.mysql.jdbc.ConnectionImpl.execSQL函数,断点观察此方法实际入参和执行的sql存在以下三种
- SET autocommit=1
- SET autocommit=0
- commit
- 推测执行了大量事务提交。
- 回到业务代码查看CrudServiceImpl类发现class定义上存在@Transactional注解。
三、结论
在class上定义@Transactional注解会使派生类的所有方法自动增加事务,即使方法没有数据库操作也会增加事务提交动作,在特定场景下会严重降低系统性能。
四、解决办法
临时解决办法:可在不需要事务的方法上增加(已验证)
@Transactional( propagation = Propagation. NEVER)
解决办法:移除class上的@Transactional注解,以业务为准在有需要的方法上增加事务
五、附录
spring事务的7种传播特性
1 REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
2 SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行
3 MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
4 NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行
5 NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常
6 REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个已经存在的事务挂起
7 NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务