当前位置: 首页 > news >正文

一场跨越技术栈的诡异异常侦破记

一场跨越技术栈的诡异异常侦破记

时间:2016年夏天
技术栈:SSM + Dubbo + jQuery EasyUI
患者:电商后台管理系统订单模块


第一幕:平静的表象

那是一个炎热的下午,我刚完成订单导出功能的开发。代码逻辑清晰,本地测试完美:

@Service
public class OrderExportServiceImpl implements OrderExportService {public List<OrderDTO> queryOrdersForExport(OrderQuery query) {// 看似正常的查询逻辑return orderMapper.selectByCondition(query);}public void exportOrders(HttpServletResponse response, OrderQuery query) {List<OrderDTO> orders = queryOrdersForExport(query);// 生成Excel并输出ExcelUtil.export(response, orders, "订单列表");}
}

前端调用也很简单:

function exportOrders() {$('#exportBtn').linkbutton('disable');var queryForm = $('#queryForm').serialize();window.location.href = '/order/export?' + queryForm;
}

第二幕:诡异现象初现

测试环境部署后,测试同事反馈:"导出功能时好时坏,有时候能成功,有时候浏览器直接下载一个0字节的文件"

更诡异的是:

  • 没有任何错误日志
  • 成功和失败没有明显规律
  • 本地开发环境从未复现

第三幕:深入调查

第一次诊断:添加详细日志

@Slf4j
@Service
public class OrderExportServiceImpl implements OrderExportService {public void exportOrders(HttpServletResponse response, OrderQuery query) {log.info("开始导出订单,查询条件:{}", query);try {List<OrderDTO> orders = queryOrdersForExport(query);log.info("查询到{}条订单记录", orders.size());ExcelUtil.export(response, orders, "订单列表");log.info("订单导出完成");} catch (Exception e) {log.error("订单导出异常", e);throw new RuntimeException("导出失败", e);}}
}

结果:日志显示一切正常,查询到数据,执行了导出,但浏览器还是偶尔收到0字节文件。

第二次诊断:怀疑Dubbo超时

考虑到订单查询涉及多个Dubbo服务调用:

public List<OrderDTO> queryOrdersForExport(OrderQuery query) {List<OrderDTO> orders = orderMapper.selectByCondition(query);for (OrderDTO order : orders) {// Dubbo调用用户服务UserDTO user = userService.getUserById(order.getUserId());// Dubbo调用商品服务  ProductDTO product = productService.getProductById(order.getProductId());order.setUserName(user.getName());order.setProductName(product.getName());}return orders;
}

检查Dubbo配置:

<dubbo:service interface="com.xxx.UserService" ref="userService" timeout="5000" retries="2"/>

结果:调整超时时间到10秒,问题依旧。

第四幕:突破性发现

在连续监控了几天后,终于发现了一个规律:当导出数据量较大时(超过1000条),失败概率显著增加

第三次诊断:线程堆栈分析

使用jstack分析应用线程状态,发现了一个关键线索:

"http-nio-8080-exec-5" #31 daemon prio=5 os_prio=0 tid=0x00007f9b3822c800 nid=0x1e5e waiting on condition [0x00007f9b0f7e6000]java.lang.Thread.State: TIMED_WAITING (parking)at sun.misc.Unsafe.park(Native Method)- parking to wait for  <0x00000000f0a8c2c8> (a java.util.concurrent.CompletableFuture$Signaller)at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)at java.util.concurrent.CompletableFuture$Signaller.block(CompletableFuture.java:1695)at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3323)at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1729)at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)at com.xxx.OrderExportServiceImpl.queryOrdersForExport(OrderExportServiceImpl.java:45)

关键发现:线程在等待CompletableFuture.get(),但代码中根本没有使用CompletableFuture

第五幕:真相大白

仔细检查MyBatis的Mapper配置,发现了问题根源:

<!-- 订单Mapper配置 -->
<select id="selectByCondition" parameterType="OrderQuery" resultMap="OrderResultMap" fetchSize="1000" timeout="30">SELECT * FROM orders <where><if test="status != null">AND status = #{status}</if><if test="startTime != null">AND create_time >= #{startTime}</if><if test="endTime != null">AND create_time <= #{endTime}</if></where>ORDER BY create_time DESC
</select>
<!-- 问题所在:这个resultMap被其他查询引用 -->
<resultMap id="OrderResultMap" type="OrderDTO"><id property="id" column="id"/><result property="orderNo" column="order_no"/><!-- 其他字段映射 --><!-- 这个association导致了N+1查询问题 --><association property="user" column="user_id" select="com.xxx.UserMapper.selectById"/><association property="product" column="product_id" select="com.xxx.ProductMapper.selectById"/>
</resultMap>

问题根源

  1. MyBatis的<association>标签在数据量大时会产生N+1查询问题
  2. 虽然我们在Service层显式调用了Dubbo服务,但MyBatis仍然会尝试执行关联查询
  3. 当数据量较大时,大量的数据库连接请求导致某些请求被静默丢弃
  4. Dubbo的异步调用与MyBatis的懒加载机制产生了微妙的冲突

第六幕:解决方案

方案一:优化MyBatis配置

<!-- 为导出功能专门创建一个resultMap,不使用association -->
<resultMap id="OrderExportResultMap" type="OrderDTO"><id property="id" column="id"/><result property="orderNo" column="order_no"/><result property="userId" column="user_id"/><result property="userName" column="user_name"/><result property="productId" column="product_id"/><result property="productName" column="product_name"/><!-- 其他直接字段 -->
</resultMap>
<select id="selectForExport" parameterType="OrderQuery" resultMap="OrderExportResultMap">SELECT o.*,u.name as user_name,p.name as product_nameFROM orders oLEFT JOIN users u ON o.user_id = u.idLEFT JOIN products p ON o.product_id = p.id<where><if test="status != null">AND o.status = #{status}</if><if test="startTime != null">AND o.create_time >= #{startTime}</if><if test="endTime != null">AND o.create_time <= #{endTime}</if></where>ORDER BY o.create_time DESC
</select>

方案二:重构Service逻辑

@Service
public class OrderExportServiceImpl implements OrderExportService {@Autowiredprivate OrderMapper orderMapper;public void exportOrders(HttpServletResponse response, OrderQuery query) {// 1. 先查询基础数据List<OrderExportVO> orders = orderMapper.selectForExport(query);// 2. 批量获取用户和商品信息(避免循环中的Dubbo调用)Set<Long> userIds = orders.stream().map(OrderExportVO::getUserId).collect(Collectors.toSet());Set<Long> productIds = orders.stream().map(OrderExportVO::getProductId).collect(Collectors.toSet());Map<Long, String> userMap = userService.batchGetUsers(userIds);Map<Long, String> productMap = productService.batchGetProducts(productIds);// 3. 组装数据List<OrderDTO> result = orders.stream().map(order -> {OrderDTO dto = new OrderDTO();// 属性拷贝BeanUtils.copyProperties(order, dto);dto.setUserName(userMap.get(order.getUserId()));dto.setProductName(productMap.get(order.getProductId()));return dto;}).collect(Collectors.toList());// 4. 导出ExcelUtil.export(response, result, "订单列表");}
}

方案三:前端优化

function exportOrders() {var $btn = $('#exportBtn');var originalText = $btn.linkbutton('getText');$btn.linkbutton({disabled: true,text: '导出中...'});// 显示进度提示$.messager.progress({title: '请稍候',msg: '数据导出中...',text: '正在准备数据'});var queryForm = $('#queryForm').serialize();// 使用Ajax先检查数据量$.get('/order/export/check?' + queryForm, function(result) {if (result.data.count > 5000) {$.messager.confirm('提示', '本次导出数据量较大(' + result.data.count + '条),可能需要较长时间,是否继续?', function(r) {if (r) {doExport(queryForm, $btn, originalText);} else {resetButton($btn, originalText);$.messager.progress('close');}});} else {doExport(queryForm, $btn, originalText);}});
}function doExport(queryForm, $btn, originalText) {var iframe = $('<iframe style="display:none"></iframe>');$('body').append(iframe);iframe.on('load', function() {setTimeout(function() {resetButton($btn, originalText);$.messager.progress('close');iframe.remove();}, 1000);});iframe.attr('src', '/order/export?' + queryForm);
}

第七幕:经验总结与思考

技术层面的反思:

  1. 框架特性理解:深刻理解所用框架的底层机制,特别是像MyBatis这样的ORM框架的懒加载、缓存等特性
  2. 分布式事务边界:在Dubbo等分布式架构中,要明确事务边界,避免跨服务的复杂事务操作
  3. 资源管理:数据库连接、HTTP连接等资源要及时释放,特别是在大数据量操作时

工程实践改进:

  1. 代码审查清单:将"避免N+1查询"、"合理使用异步"等加入代码审查清单
  2. 监控体系完善
// 添加性能监控
@Around("execution(* com.xxx..*Service.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();try {return joinPoint.proceed();} finally {long cost = System.currentTimeMillis() - start;if (cost > 1000) { // 超过1秒记录警告日志log.warn("方法执行缓慢: {},耗时: {}ms", joinPoint.getSignature(), cost);}}
}
  1. 分层架构规范:明确各层的职责,避免在数据访问层混入业务逻辑

人生感悟:

这次调试经历让我明白,在软件开发中:

  • 最可怕的不是报错,而是不报错
  • 巧合背后往往有必然
  • 理解原理比掌握API更重要

从那以后,我们团队建立了"诡异问题档案馆",记录和分享各种奇葩问题的解决方案。这个习惯在后续的技术栈迁移到Spring Cloud + Vue时,同样帮助我们解决了很多疑难杂症。

记住,每个诡异的bug都是提升技术深度的机会!💪

http://www.dtcms.com/a/470895.html

相关文章:

  • 基于之前说麦克斯韦方程的挠场和涡旋场和特斯拉的非赫兹波
  • 铁路项目建设 网站wordpress站点
  • i.mx8上Qt应用星期显示异常
  • 08_Matplotlib数据可视化
  • 怎么上传自己做的网站wordpress注册跳转
  • 荆州做网站建设学校网站多钱
  • RDKit | 深入探讨用于捕获立体化学特征的分子指纹
  • Windows Server 2019域域控制器如何设置编辑WMI筛选器
  • Hive SQL:where 与 having(城市分组,年龄筛选)
  • C# 枚举(Enum)
  • 松江网站建设哪家好手机网站开发计划
  • 模板网站如何做seo网站维护html模板
  • PostgreSql 判断字符串是否为null 或 空字符串
  • 我做外贸要开国际网站吗个人网站cms
  • VR全景园区完整实现方案
  • GPS与北斗组合单点定位算法MATLAB实现
  • PostgreSQL 中 CTE 的使用
  • 网站开发语言怎么样wordpress底板版权
  • 【C语言加油站】C语言文件操作详解:从“流”的概念到文件的打开与关闭​
  • 涪陵网站建设公司国内可以上的网站
  • 国产CAD皇冠CAD(CrownCAD)三维建模教程:变压器
  • 网站博客程序logo智能设计一键生成器
  • 网站空间可以通过什么获取专业做鞋子的网站有哪些
  • 优秀企业网站设计欣赏电商网站建设意义
  • PwnKit提权漏洞复现:原理分析+环境搭建+渗透实践(CVE-2021-4034)
  • 李宏毅机器学习笔记19
  • 腾讯建设网站视频下载深圳坪山天气
  • 群晖wordpress主机兰州seo新站优化招商
  • Go语言实现HTML转PDF
  • 深入解析Java NIO:从BIO到Reactor模式的网络编程演进