问题描述:
先说一下流程:后端保存前端提交的图表信息,然后发送异步消息到消息队列,由下游服务去处理图表信息。
部署项目到服务器,验证项目功能的时候,出现了以下错误:数据库存在数据。下游服务查不到数据库的数据
// service代码
@Override
@Retryable
@Transactional
public ChartVo genChartByAiAsyncMq(Long uid, MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest) {
// ...省略
Chart chart = Chart.builder().name(name).goal(goal)
.chartData(data).chartType(chartType)
.uid(uid).status("wait").build();
boolean save = this.save(chart);
if(!save){
log.info("保存表单失败");
throw new RuntimeException("保存表单失败");
}
List<Chart> list = this.list();
log.info("chart长度:{}", list.size());
// 发送消息,触发异步处理
biMessageProducer.sendMessage(String.valueOf(chart.getId()));
log.info("发送消息成功");
// 省略
}
下游服务处理代码
@SneakyThrows
@RabbitListener(queues = {MQConstants.BI_QUEUE_NAME}, ackMode = "MANUAL")
public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag){
log.info("receive message: {}", message);
// 检查消息是否为空
if(StringUtils.isBlank(message)){
log.info("消息不能为空");
channel.basicNack(deliveryTag, false, false);
throw new RuntimeException("消息不能为空");
}
List<Chart> list = biService.list();
log.info("图表列表长度:{}", list.size());
// 尝试将消息解析为图表ID,并查询图表信息
Long chartId = Long.parseLong(message);
Chart chart = biService.getById(chartId);
log.info("图表信息:{}", chart);
// 图表不存在时的处理
if(chart == null){
log.info("图表不存在");
channel.basicNack(deliveryTag, false, false);
throw new RuntimeException("图表不存在");
}
// 省略
}
日志输出如下:
数据库信息:
解决过程
首先说明一下,这个错误之前没有出现过,下午出错,再次测试的时候,也会出现正常的情况,只不过错误占比有点高(10次有8次获取不到数据库消息,用jemeter测试了一下)。
分析的过程:
步骤1、首先在上游服务和下游服务打印日志,查看数据库有多少条数据,上游服务显示有2条数据,下游服务显示有1条数据
步骤2、找错的时候,看见方法加了事务注解@Transactional,这个时候想到可能是事务影响(最近在看mysql相关的知识,比较敏感),然后取掉注解,重新验证,发现没有出错
原因分析
MySQL事务
我们都知道MySQL(8.x版本)的事务的隔离级别默认是可重复读(RR),那么一个事务在操作完成之前,对其他事务是看不见的,所以就说,方法中先保存图表信息到数据库,然后发送消息到消息队列,再执行方法的后续过程。发送消息到队列之后,可能数据库事务还没有提交,但是消息发送成功了,就立刻被消费者端消费,此时,消费者端查询数据库中的图表信息,当然查不到,因为生产者端的事务还没有提交。
之前没有出错,这次验证出错分析
为了写代码方便,使用的是服务器上的数据库,出现这个bug问题的时候,使用本地数据库,之后专门验证了一下,使用服务器上的数据库确实没有出现这个问题,我怀疑是本地数据库提交事务时间过长。但是反过来想,本地执行事务操作不受网络等因素影响,应该执行时间很短才是。为什么我的执行结果却是相反的,这使我很疑惑。最后我觉得只能从硬件出发,在实验室电脑上,运行项目的前后端,以及打开其他程序,内存的使用率就很高,那么在本地执行mysql事务的时候,估计时间也就很长啦(这是我目前分析的原因)
然后换了台电脑(M2芯片,24G内存),进行相同的操作,运行结果是正确的。如下:
从结果上来看,电脑内存占用率过高,导致本地mysq执行事务的时间大于服务器上mysql执行事务的时间。
好了,既然原因分析完了
那么再反思一下,如果一直使用服务器上的数据库,这个问题没有出现,系统是正常运行,这样好吗?答案是否定的,因为在真正的开发环境,我们的业务绝对会受系统配置和网络影响,针对这个场景,我们不能直接在方法上添加事务注解。
解决方法
1、手动提交事务,不使用注解,可以更细的控制事务开始和结束。
2、设置延迟队列,但是这个延迟的时间具体是多少,我们无法确定,所以最后采用第一种方法解决此问题。
@Slf4j
@Service
public class BiServiceImpl extends ServiceImpl<ChartMapper, Chart> implements BiService {
@Resource
private PlatformTransactionManager transactionManager;
@Override
@Retryable
public ChartVo genChartByAiAsyncMq(Long uid, MultipartFile multipartFile, GenChartByAiRequest genChartByAiRequest) {
TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
try{
// 省略...
Chart chart = Chart.builder().name(name).goal(goal)
.chartData(data).chartType(chartType)
.uid(uid).status("wait").build();
boolean save = this.save(chart);
if(!save){
log.info("保存表单失败");
throw new RuntimeException("保存表单失败");
}
transactionManager.commit(transactionStatus);
List<Chart> list = this.list();
log.info("chart长度:{}", list.size());
// 发送消息,触发异步处理
biMessageProducer.sendMessage(String.valueOf(chart.getId()));
log.info("发送消息成功");
// 省略...
}catch (Exception e){
log.error("AI 异步调用失败", e);
transactionManager.rollback(transactionStatus);
throw new RuntimeException("AI 异步调用失败");
}
}
}