SpringBoot使用@Async和@Transactional注解优化接口
1、业务背景:
项目上有一个接口需要按照前端传递的时间段范围修改6个表的数据,接口V1版本开发完成是使用的同步方式全局@Transactional注解的方式去做的,但存在一个问题就是在这六个表中,sc_xxx_rtd和sc_xxx_minute的表数据量巨大,导致接口RT时间达到了几十秒的程度,严重影响用户使用。
2、优化思路:
使用@Async注解配合自定义异步线程池将修改6️⃣个表数据的update操作异步执行,并使用@Transactional主键保证修改数据这个操作的原子性,要不全部成功,要不全部失败;
3、代码实现:
1 /** 2 * 异步线程池配置 3 * 4 * @author zhanglei 5 * @date 2022-02-28 9:34 6 */ 7 @Configuration 8 @EnableAsync 9 public class AsyncConfig implements AsyncConfigurer { 10 11 @Override 12 @Bean(name = "asyncTaskExecutor") 13 public Executor getAsyncExecutor() { 14 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 15 executor.setCorePoolSize(20);//核心线程数 16 executor.setMaxPoolSize(40);//最大线程数 17 executor.setQueueCapacity(1000);//队列大小 18 executor.setKeepAliveSeconds(300);//线程最大空闲时间 19 executor.setThreadNamePrefix("AsyncExecutor-"); //指定用于新创建的线程名称的前缀。 20 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//拒绝策略(一共四种,此处省略) 21 executor.initialize(); 22 return executor; 23 } 24 25 @Override 26 public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { 27 System.out.println("异常处理器!!你可以自定义异常处理器~"); 28 return new MyAsyncUncaughtExceptionHandler(); 29 } 30 } 31 32 @Slf4j 33 @Component 34 class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { 35 36 @Resource 37 private AsyncTaskExceptionLogMapper asyncTaskExceptionLogMapper; 38 39 @Override 40 public void handleUncaughtException(Throwable ex, Method method, Object... params) { 41 42 log.error("occur exception class#method: " + method.getDeclaringClass().getName() + "#" + method.getName()); 43 log.error("occur exception type : " + ex.getClass().getName()); 44 log.error("occur exception exception : " + ex.getMessage()); 45 log.error("occur exception param : " + JSONUtil.toJsonStr(Lists.newArrayList(params)));
46 47 if (ObjectUtil.isNotNull(ex) && ObjectUtil.isNotNull(method)) { 48 AsyncTaskExceptionLog asyncTaskExceptionLog = new AsyncTaskExceptionLog(); 49 asyncTaskExceptionLog.setId(IdUtil.getSnowflakeNextIdStr()); 50 asyncTaskExceptionLog.setOccurClass(method.getDeclaringClass().getName()); 51 asyncTaskExceptionLog.setOccurMethod(method.getName()); 52 asyncTaskExceptionLog.setOccurType(ex.getClass().getName()); 53 asyncTaskExceptionLog.setOccurMessgae(ex.getMessage()); 54 if (ObjectUtil.isNotNull(params)) { 55 asyncTaskExceptionLog.setOccurParam(JSONUtil.toJsonStr(Lists.newArrayList(params))); 56 } 57 asyncTaskExceptionLogMapper.insert(asyncTaskExceptionLog); 58 } 59 60 } 61 }
1 /** 2 * 更新sc_data_xxx和sc_data_xxx_review的数据 3 */ 4 @Async(value = "asyncTaskExecutor") 5 @Transactional(rollbackFor = Throwable.class) 6 public void updateScData(String userName, DailyMaintenance dailyMaintenance, AppMaintenanceExamine appMaintenanceExamine, 7 String reviseStartTime, String reviseEndTime, String id, Date beforeExceptionStartTime, Date beforeExceptionEndTime) { 8 try { 9 log.info("进入异步方法内"); 10 //获取巡检模板ID、异常因子编码列表、设备编码 11 Integer templateId = appMaintenanceExamine.getExamineTemplateId(); 12 String exceptionPolCodeStr = dailyMaintenance.getExceptionPolCodeList(); 13 String deviceCode = dailyMaintenance.getDeviceCode(); 14 15 //故障异常标识 16 String flag = "D"; 17 18 List<ExceptionDataOperateLog> paramList = Lists.newArrayList(); 19 20 /* 21 * 如果异常因子编码列表为空则不修改 22 * 判断运维人员上传的异常因子编码,如果是ALL则按照设备编码和时间段去查询修改全部因子review数据 23 * 否则根据解析出的因子,按照设备编码和要修改的因子polCode以及时间段去修改对应review数据 24 * 只有查询出符合条件的数据才去执行修改,否则不去执行update,避免查询不出数据而导致修改了全表数据 25 * 也给review相关数据打标识 26 */ 27 updateReviewData(reviseStartTime, reviseEndTime, exceptionPolCodeStr, deviceCode, userName, flag, paramList); 28 29 //给原始数据打标识 30 updateOriginalFlag(reviseStartTime, reviseEndTime, templateId, exceptionPolCodeStr, deviceCode, userName, flag, paramList); 31 32 if (CollectionUtil.isNotEmpty(paramList)) { 33 //批量插入操作日志数据到日志记录表 34 exceptionDataOperateLogMapper.batchInsert(paramList); 35 } 36 37 log.info("异步方法执行完毕"); 38 } catch (Exception e) { 39 log.error("异步修改数据出现异常:", e); 40 //获取Spring容器对象 41 ApplicationContext applicationContext = SpringUtil.getApplicationContext(); 42 RevisalErrorEvent revisalErrorEvent = new RevisalErrorEvent(this); 43 revisalErrorEvent.setId(id); 44 revisalErrorEvent.setBeforeExceptionStartTime(beforeExceptionStartTime); 45 revisalErrorEvent.setBeforeExceptionEndTime(beforeExceptionEndTime); 46 applicationContext.publishEvent(revisalErrorEvent); 47 //手动回滚事务 48 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 49 } 50 }
/** * 订正异常event事件 * @author zhangl * @date 2022-04-21 14:32 */ @SuppressWarnings("ALL") @Data public class RevisalErrorEvent extends ApplicationEvent { /** * 日常运维表主键id */ private String id; /** * 之前的异常开始时间 */ private Date beforeExceptionStartTime; /** * 之前的异常开始时间 */ private Date beforeExceptionEndTime; public RevisalErrorEvent(Object source) { super(source); } }
1 /** 2 * @author zhangl 3 * @date 2022-05-25 10:10 4 */ 5 @Component 6 @Slf4j 7 public class RevisalErrorEventListener implements ApplicationListener<RevisalErrorEvent> { 8 9 @Resource 10 private DailyMaintenanceMapper dailyMaintenanceMapper; 11 12 /** 13 * 监听异步任务是否出现异常,出现则回滚日常运维表的数据 14 * 15 * @param event 16 */ 17 @Override 18 public void onApplicationEvent(RevisalErrorEvent event) { 19 String id = event.getId(); 20 Date beforeExceptionStartTime = event.getBeforeExceptionStartTime(); 21 Date beforeExceptionEndTime = event.getBeforeExceptionEndTime(); 22 DailyMaintenance dailyMaintenance = dailyMaintenanceMapper.selectById(id); 23 dailyMaintenance.setExceptionStartTime(beforeExceptionStartTime); 24 dailyMaintenance.setMaintenanceEndTime(beforeExceptionEndTime); 25 dailyMaintenance.setIfRevise(0); 26 dailyMaintenanceMapper.updateById(dailyMaintenance); 27 } 28 29 }
1 <update id="batchUpdateFlag"> 2 update sc_data_hour 3 <set> 4 <if test="flag != null and flag !=''"> 5 flag = #{flag} 6 </if> 7 </set> 8 <where> 9 <if test="startDateTime != null and startDateTime != ''"> 10 and data_time >= to_date(#{startDateTime},'yyyy-MM-dd hh24:mi:ss') 11 </if> 12 <if test="endDateTime != null and endDateTime != ''"> 13 and data_time <= to_date(#{endDateTime},'yyyy-MM-dd hh24:mi:ss') 14 </if> 15 and flag != 'D' 16 </where> 17 </update>
注意事项:
原版update的修改语句是通过where id in 的方式去修改的,但实际发现如果id的数据量特别多成千上万那种的话,sql执行的效率就非常的低,如果可以的话尽可能使用范围条件而不是in的方式