一、前言
在日常Web前后端联合开发工作中,绝大多数接口问题要么是前端传参错误,要么是后端SQL逻辑bug,这类显性问题很好定位。但最让人头疼的是:接口无报错、状态码200、本地测试完全正常,线上高并发下偶现返回空列表数据。
-
后端:SpringBoot 2.7.x + MyBatis-Plus 3.5.3.1 + MySQL8.0,提供RESTful接口
-
部署环境:后端集群部署,Tomcat线程池复用请求,早高峰接口并发量高
2.2 前后端对接问题现象
-
前端页面:商品/业务列表偶尔空白,刷新页面后大概率恢复正常,无固定复现步骤;
-
前端网络面板:Axios请求成功,Http状态码200,后端返回data为空数组,无error错误信息;
-
后端视角:接口无异常日志、无SQL报错,数据库后台直接执行相同查询参数,能查到完整数据;
-
复现规律:仅早高峰高并发场景出现,本地前后端联调、测试环境完全无法复现。
三、前后端联合排查全过程(完整踩坑流程)
遇到前后端接口诡异问题,很多人会互相甩锅:前端觉得是后端数据问题,后端觉得是前端传参问题。下面还原完整排查流程,一步步排除两端问题。
3.1 前端侧排查(排除前端自身问题)
-
✅ 查看Axios请求请求体/请求参数:每一次异常请求的入参完全正确,参数没有丢失、篡改、为空的情况;
-
✅ 排除前端缓存问题:关闭浏览器本地缓存,添加请求时间戳,问题依旧偶发;
-
✅ 排除前端渲染bug:后端直接返回固定模拟数据,前端列表渲染完全正常;
结论:前端代码、请求逻辑、渲染逻辑无任何问题,问题100%出在后端服务。
3.2 后端初步排查(4个无效排查坑点)
后端刚开始按照常规接口bug思路排查,全部走了弯路,也是大部分Web后端开发者都会踩的坑:
-
❌ 坑1:怀疑SQL语句错误,本地执行相同参数SQL,数据正常,排除SQL问题
-
❌ 坑2:怀疑数据库连接超时,调整连接池参数,问题无改善
-
❌ 坑3:怀疑Redis缓存脏数据,清空全量缓存,依旧偶现空数据
-
❌ 坑4:怀疑事务隔离级别问题,调整事务传播机制,问题依旧存在
四、后端代码根因定位(Web高并发接口核心隐患)
新增接口全链路日志,打印每次请求的线程ID、最终执行SQL后,终于定位到核心问题:后端不规范使用ThreadLocal,Tomcat线程池复用导致线程变量污染,查询SQL条件错乱,最终查询不到数据。
核心根因(通俗易懂,前后端都能看懂) Web服务中Tomcat会复用工作线程处理请求,后端代码用全局ThreadLocal存储接口查询参数,接口请求结束后没有清空线程变量。下一个复用该线程的前端请求,会读取到上一次请求残留的旧参数,导致后端查询条件错乱,数据库查不出数据,最终前端拿到空列表。
4.1 线上有问题后端源码(还原真实bug代码)
/** * Web项目业务接口服务层 - 存在线程安全bug的原始代码 */ @Service public class BusinessServiceImpl implements BusinessService { // 全局静态ThreadLocal:Web高并发线程复用致命隐患 private static final ThreadLocal<QueryDTO> QUERY_THREAD_LOCAL = new ThreadLocal<>(); /** * 前后端对接列表查询接口 */ @Override @Transactional public List<BusinessData> getBusinessList(QueryDTO queryDTO) { // 将前端传入的参数存入线程本地变量 QUERY_THREAD_LOCAL.set(queryDTO); // 构建查询条件并返回数据给前端 List<BusinessData> dataList = businessMapper.selectList(getQueryWrapper()); // 致命漏洞:请求结束未清理ThreadLocal变量 return dataList; } // 从线程变量中获取参数构建查询条件 private QueryWrapper<BusinessData> getQueryWrapper() { QueryDTO dto = QUERY_THREAD_LOCAL.get(); QueryWrapper<BusinessData> wrapper = new QueryWrapper<>(); wrapper.eq(ObjectUtil.isNotNull(dto.getId()), "id", dto.getId()); wrapper.eq(ObjectUtil.isNotNull(dto.getStatus()), "status", dto.getStatus()); return wrapper; } }
4.2 问题发生完整流程(结合Web请求流程讲解)
-
前端发起第一次列表请求,线程A处理,参数存入ThreadLocal,正常返回数据;
-
请求结束,代码未清理线程变量,线程A依旧保留上一次请求参数;
-
高峰期前端发起第二次新请求,Tomcat复用线程A处理新请求;
-
新接口还未set新参数时,构建SQL读取到旧残留参数;
-
SQL查询条件错误,数据库无匹配数据,前端最终收到空数组。
五、两层解决方案(紧急止血 + 彻底根治,适配Web项目上线节奏)
5.1 方案一:线上紧急修复(5分钟快速上线,不改动原有代码结构)
适合Web项目线上紧急止血,最小改动代码,通过finally强制清空线程变量,保证每一次前端请求结束后,线程无残留数据。
@Override @Transactional public List<BusinessData> getBusinessList(QueryDTO queryDTO) { try { QUERY_THREAD_LOCAL.set(queryDTO); List<BusinessData> dataList = businessMapper.selectList(getQueryWrapper()); return dataList; } finally { // 无论接口成功/失败,强制清除线程变量,适配Web所有请求场景 QUERY_THREAD_LOCAL.remove(); } }
修复效果:上线后前端列表空白问题立即消失,无任何偶现异常,但是代码依旧存在设计瑕疵,仅适合临时应急。
5.2 方案二:最优根治方案(Web项目标准规范写法,一劳永逸)
从代码层面彻底干掉多余的ThreadLocal传参方式,方法直接透传前端入参,彻底杜绝线程安全问题,同时优化只读接口事务,提升Web接口并发响应速度,适合长期迭代的Web项目。
/** * Web项目最终无bug接口代码(可直接用于前后端线上对接) */ @Service public class BusinessServiceImpl implements BusinessService { @Override // 列表查询为只读接口,开启只读事务,提升Web接口响应速度 @Transactional(readOnly = true) public List<BusinessData> getBusinessList(QueryDTO queryDTO) { // 直接透传前端入参,完全摒弃ThreadLocal,从根源规避线程污染 QueryWrapper<BusinessData> wrapper = getQueryWrapper(queryDTO); return businessMapper.selectList(wrapper); } /** * 方法直接接收前端入参,无任何线程上下文依赖,接口更稳定 */ private QueryWrapper<BusinessData> getQueryWrapper(QueryDTO dto) { QueryWrapper<BusinessData> wrapper = new QueryWrapper<>(); wrapper.eq(ObjectUtil.isNotNull(dto.getId()), "id", dto.getId()); wrapper.eq(ObjectUtil.isNotNull(dto.getStatus()), "status", dto.getStatus()); // 新增排序规则,保证前端列表渲染顺序始终一致 wrapper.orderByDesc("create_time"); return wrapper; } }
六、Web前后端开发通用避坑总结(团队开发必看)
6.1 后端接口开发强制规范
-
禁止用ThreadLocal传递普通接口入参:Web接口所有前端入参,直接方法透传即可,ThreadLocal只用来存储登录用户信息、请求链路ID等全局上下文;
-
ThreadLocal必须成对清理:只要使用set,必须在finally执行remove,适配Tomcat线程池复用机制;
-
查询接口统一使用只读事务:提升Web接口并发吞吐量,减少事务锁开销。
6.2 前后端联调排查通用技巧
-
遇到偶现空数据:前端优先导出请求报文,后端打印最终执行SQL,快速定位是参数问题还是SQL问题;
-
无报错类诡异接口问题:90%都是后端线程安全、缓存、事务问题,不要盲目怀疑前端渲染;
-
线上偶现问题:优先打印线程ID、请求traceId,区分不同请求,快速定位线程复用隐患。
七、总结
本次Web前后端开发遇到的接口偶现空数据问题,看似是前后端联调的数据返回bug,本质是后端Web服务线程安全编码不规范导致。在Web高并发接口开发中,很多隐性bug不会在本地联调暴露,只会在线程复用、高并发场景集中爆发。
做前后端开发,不能只保证功能能跑,更要贴合Web服务运行底层原理(Tomcat线程池、Spring事务、线程上下文)编码,才能彻底规避这类无报错、难复现的线上疑难bug。
💬 互动交流:你们在Web前后端联调时,还遇到过哪些无报错的诡异接口bug?欢迎评论区留言交流~
👉 关注我:持续更新Web前后端开发踩坑笔记、接口联调技巧、SpringBoot实战干货,助力前后端开发避坑!

444

被折叠的 条评论
为什么被折叠?



