Java 后端实战进阶:从踩坑到架构的系统化笔记
副标题:5 年工业级项目实战经验沉淀,覆盖 MySQL 调优、Spring 全家桶、微服务、中间件、线上排障的硬核方法论
专栏简介
本专栏是一位 Java 后端开发工程师在工业互联网、电力行业、物联网等真实项目中积累的 5 年实战精华。不同于学院派的教程式写作,每一篇内容都源自真实生产环境的踩坑、排查、优化与重构经历。
专栏内容严格遵循"问题 → 原因分析 → 解决方案 → 最优实践"的四步闭环方法论,确保每个知识点都经得起生产环境的检验。所有案例均来自实际业务场景,涉及设备联动系统、智能巡视平台、告警中心、数据中台等多个工业级项目。
无论你是刚入行 1~3 年的初中级开发者,还是希望系统梳理实战经验的中高级工程师,本专栏都能为你提供直接可复用的方法论和代码范式。
适配人群
- 1~3 年 Java 后端开发者:想从 CRUD 跃升到能独立承担复杂模块开发
- 3~5 年中高级工程师:希望系统化沉淀经验、提升架构思维
- 转行/自学 Java 的开发者:需要一个有实战视角的进阶指南
- 技术团队 Lead/架构师:可作为团队 Code Review 和技术培训的参考资料
学习收益
完成本专栏学习后,你将获得:
- MySQL 索引优化的系统方法论:掌握索引失效的 11 种场景及其替代方案,能快速定位慢查询
- Spring 生态的深度实战能力:理解事务传播、AOP 原理、Bean 生命周期等核心机制
- Redis 缓存的正确使用姿势:解决缓存一致性问题,掌握 key 设计策略与批量操作
- 线上问题排查的标准思路:建立从日志到 jstack 到 Arthas 的完整排查体系
- 高质量代码的设计原则:掌握方法拆分、参数设计、异常处理的最佳实践
- 微服务架构的落地经验:从服务拆分到网关设计到分布式事务的完整路径
专栏目录
模块一:MySQL 实战调优(8 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 01 | 索引失效的 11 种场景与替代方案 | 最左匹配、函数导致失效、% 前缀模糊、类型隐式转换等 |
| 02 | 慢查询排查三板斧:EXPLAIN + 慢查询日志 + Arthas | 从定位到优化的标准流程 |
| 03 | JOIN 优化:INNER JOIN vs LEFT JOIN 的正确选择 | 小表驱动大表、子查询转连接查询、ON 与 WHERE 的区别 |
| 04 | 批量操作的坑:MySQL 一次能接收多大数据 | foreach 性能调优、INSERT 批量优化、分批提交策略 |
| 05 | 数据库设计中的"备用字段"哲学 | 冗余字段的利弊、宽表 vs 窄表、历史表与流水表设计 |
| 06 | 分库分表实战:从单表到按月分表的演进 | 文件服务分表案例、数据迁移策略、查询适配方案 |
| 07 | GROUP BY 深度解析:HAVING、WHERE、ORDER BY 的陷阱 | 分组查询最新一条数据、GROUP_CONCAT 行转列技巧 |
| 08 | 生产环境 SQL 优化的 7 个黄金法则 | 覆盖索引回表、UNION vs UNION ALL、FORCE INDEX 的使用时机 |
模块二:MyBatis / MyBatis-Plus 深度实战(6 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 09 | MyBatis 动态 SQL 减少 if-else 的艺术 | 动态 SQL 替代 Java 分支判断、discriminator 鉴别器的使用 |
| 10 | MyBatis 传参的 10 个隐藏坑 | Integer 值为 0 被转为空串、foreach 批量性能、statementType 用法 |
| 11 | 从 MyBatis 迁移到 MyBatis-Plus 的实战收益 | 代码迁移效率提升、单表 CRUD 标准化、代码生成器的定制 |
| 12 | PageHelper 分页原理与"只能分页一次"的陷阱 | ThreadLocal 拦截机制、为什么 Service 层调用分页会失效 |
| 13 | MyBatis TypeHandler:解决数据库字段与 Java 类型的映射难题 | JSON 类型存储、枚举映射、自定义类型处理器 |
| 14 | MyBatis-Plus Join 连表查询实战 | MPJBaseMapper 使用、连表分页、替代 XML 映射的方案 |
模块三:Spring 生态深度解析(8 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 15 | Spring 事务的 5 个诡异场景 | 检修区域事务问题、事务失效的常见原因、@Transactional 最佳实践 |
| 16 | Spring AOP 实战:日志切面与统一异常处理 | @Around 环绕通知、自定义注解 + 切面的通用方案 |
| 17 | Bean 生命周期全链路:从 @PostConstruct 到 BeanPostProcessor | 初始化回调、Aware 接口、BeanFactoryPostProcessor 的区别 |
| 18 | Spring Boot 过滤器与拦截器:从原理到最佳实践 | Filter vs Interceptor 的本质区别、白名单、防重放、二次鉴权的实现 |
| 19 | Spring Boot 异步编程:从 @Async 到 CompletableFuture | 线程池配置、异步变同步、CountDownLatch 的正确用法 |
| 20 | Spring Boot 配置管理:@ConfigurationProperties 的正确姿势 | 与 @Value 的区别、多环境配置、配置热更新 |
| 21 | Spring Boot 统一返回体设计:一个被低估的基础能力 | 返回体标准化、错误码体系、全局异常处理器 @RestControllerAdvice |
| 22 | Spring Bean 注入避坑指南 | @Autowired vs @Resource、头重脚轻问题、循环依赖的 3 种解决方案 |
模块四:Redis 实战与缓存策略(5 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 23 | 缓存一致性问题的排查与解决 | 典型案例:变电站配置更新后查询不生效的根因分析 |
| 24 | Redis Key 设计策略:从命名规范到版本控制 | 模块化 key 管理、版本号嵌入 key、过期时间设计 |
| 25 | Redis 批量操作与 Lua 脚本实战 | KEYS 命令的风险、批量删除的正确姿势、EVAL 脚本编写 |
| 26 | 缓存穿透、击穿、雪崩的工业级解决方案 | 布隆过滤器、互斥锁、随机过期时间、请求降级策略 |
| 27 | Redis 在设备联动系统中的实战应用 | 点位缓存、配置缓存、告警规则缓存的设计与维护 |
模块五:线上问题排查方法论(5 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 28 | 线上服务重启后数据丢失的排查 | 内存队列积压丢失、持久化方案选型、消息可靠性保障 |
| 29 | jstack + jps 定位线程池爆满问题 | xx现场线程泄漏案例、线程 dump 分析方法 |
| 30 | 日志打印的艺术:从排障利器到性能杀手 | 日志级别控制、@Slf4j topic 分离、时间格式化、入参出参打印规范 |
| 31 | 反向隔离装置调试:网络抓包到防火墙排障 | Wireshark 抓包、tcpdump 命令、MAC 地址配置错误排查 |
| 32 | 生产环境 JAR 包不生效的诡异问题 | lib 替换不生效、反编译验证、Docker 环境镜像一致性 |
模块六:高质量代码设计原则(7 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 33 | 方法设计的 5 条军规 | 单一职责、主方法不超过 30 行、方法调用不超过 3 层 |
| 34 | 参数设计的正反案例 | 独立参数对象、从外到内 vs 从内到外的组装策略 |
| 35 | 异常处理最佳实践 | try-catch 最短路径原则、第三方调用必须 try-catch、错误码体系设计 |
| 36 | 代码重构实战:从面向过程到面向对象 | patrol-linkage 重构案例、共性代码提取、策略模式应用 |
| 37 | 设计模式在业务开发中的实战运用 | 策略模式处理多厂商逻辑、适配器模式对接第三方系统 |
| 38 | Java 8 Stream API 实战技巧 | 去重、排序空值安全、collectingAndThen、Optional 防空指针 |
| 39 | 代码鲁棒性:让程序在异常情况下也能优雅运行 | 输入校验、边界检查、超时重试、防御性编程 |
模块七:微服务架构落地(6 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 40 | Spring Cloud Alibaba 核心组件全景 | Nacos、Sentinel、Seata、RocketMQ、Gateway 的协同工作 |
| 41 | Nacos 注册中心与配置中心的实战配置 | 版本对齐、服务注册、动态配置刷新、命名空间设计 |
| 42 | API 网关设计:从路由到鉴权 | Spring Cloud Gateway 路由配置、统一鉴权过滤器、请求限流 |
| 43 | 分布式事务:Seata AT 模式实战 | 本地消息表 vs TCC vs Saga 模式选型、异常回滚处理 |
| 44 | 服务间调用:Feign vs RestTemplate vs gRPC | 动态代理原理、超时配置、熔断降级策略 |
| 45 | 多系统数据对接的标准化架构 | 统一数据模型(CDM)、适配器模式、接口幂等性设计 |
模块八:中间件与工具实战(5 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 46 | MQTT 在设备信号采集中的实战 | 发布/订阅模式、消息可靠性、Spring Integration MQTT 集成 |
| 47 | Netty 字节序处理:大小端转换 | readShort vs readShortLE、协议报文解析实战 |
| 48 | Docker 容器化部署的 10 个踩坑点 | 日志限制、宿主机挂载、镜像构建、iptables 配置 |
| 49 | MinIO 文件服务的性能优化 | 大批量图片上传测试(1 秒 150 张)、预签名 URL、public/private 策略 |
| 50 | 工具链效率提升:Postman、Apifox、IDEA 高级技巧 | 书签功能、代码生成、接口 Mock、Ctrl+Shift+F 全局搜索 |
模块九:项目架构与工程实践(5 篇)
| 序号 | 篇名 | 核心要点 |
|---|---|---|
| 51 | 从规范到系统:电力行业的架构设计方法论 | 规范文档驱动、系统功能架构梳理 |
| 52 | 接口文档管理的最佳实践 | Swagger、Smart-Doc、Torna 对比、接口备注编号规范 |
| 53 | 多项目代码复用与脚手架建设 | 公共模块抽取、基础框架搭建、包命名规范 |
| 54 | 代码评审 Checklist:30 条实战规则 | 入参校验、N+1 查询、魔法数字、日志打印、异常处理 |
| 55 | 需求变更应对策略:从小步快跑到数据迁移 | 联动功能改造案例、新老数据交割、共性代码提取 |
模块一:MySQL 实战调优
01 | 索引失效的 11 种场景与替代方案
开篇
索引优化是后端开发者性价比最高的技能之一。一次精准的索引调整,可能让一条原本耗时 3 秒的查询降到 10 毫秒以内,效果立竿见影。
但在实际开发中,索引失效的场景比我们想象的多得多。下面总结的 11 种场景,均来自生产环境的真实案例。
场景一览
1. 不满足最左匹配原则
sql
-- 假设联合索引 idx_a_b_c (a, b, c)
-- 以下查询不会走索引
SELECT * FROM t WHERE b = 1 AND c = 2;
-- 以下查询会走索引
SELECT * FROM t WHERE a = 1 AND b = 1;
SELECT * FROM t WHERE a = 1; -- 走索引的最左前缀
替代方案:调整查询条件顺序,确保联合索引的最左列出现在 WHERE 中。
**2. SELECT ***
sql
-- 全字段查询无法利用覆盖索引,导致大量回表
SELECT * FROM station_signal WHERE station_id = '123';
-- 优化:只查询索引列
SELECT id, station_id, signal_name FROM station_signal WHERE station_id = '123';
替代方案 :利用覆盖索引,将查询列包含在索引中,避免回表。
3. LIKE 以 % 开头的模糊查询
sql
-- 索引失效
SELECT * FROM device WHERE device_name LIKE '%变压器%';
-- 索引生效
SELECT * FROM device WHERE device_name LIKE '变压器%';
替代方案:使用全文索引(FULLTEXT)、ElasticSearch,或业务上避免前置模糊。
4. NOT EXISTS / NOT IN(非主键索引)
sql
-- 索引失效
SELECT * FROM device WHERE id NOT IN (SELECT device_id FROM station_device);
-- NOT IN 主键索引可以走索引
SELECT * FROM device WHERE id NOT IN (1, 2, 3);
替代方案:用 LEFT JOIN + IS NULL 替代 NOT EXISTS。
5. ORDER BY 使用不同方向
sql
-- 索引失效
SELECT * FROM patrol_task
WHERE station_id = '123'
ORDER BY create_time ASC, task_status DESC;
替代方案:统一排序方向,或使用 FORCE INDEX 强制指定索引。
6. OR 关键字前后不全是索引列
sql
-- 索引失效
SELECT * FROM device WHERE station_id = '123' OR device_type = 'camera';
-- 索引生效:OR 前后都是索引列
SELECT * FROM device WHERE station_id = '123' OR device_name = 'xxx';
7. VARCHAR 类型未加引号
sql
-- MySQL 隐式类型转换导致索引失效
SELECT * FROM device WHERE station_id = 123; -- station_id 是 VARCHAR 类型
-- 正确写法
SELECT * FROM device WHERE station_id = '123';
生产案例 :三维系统和站端对接时,点位重要等级在三维端是
int类型,在集控站端是varchar类型,导致查询条件隐式转换,索引完全失效。
8. 在索引列上使用函数
sql
-- 索引失效
SELECT * FROM patrol_record
WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-15';
-- 优化:不改索引列,改条件
SELECT * FROM patrol_record
WHERE create_time >= '2024-01-15 00:00:00'
AND create_time < '2024-01-16 00:00:00';
9. 在索引列上做运算
sql
-- 索引失效
SELECT * FROM device WHERE id + 1 = 100;
-- 优化
SELECT * FROM device WHERE id = 99;
10. 没有加 WHERE 或 LIMIT 的全表扫描
sql
-- 没有 WHERE 条件
SELECT * FROM device ORDER BY create_time DESC;
-- 优化:加 LIMIT
SELECT * FROM device ORDER BY create_time DESC LIMIT 1000;
11. 对不同的索引进行 ORDER BY
sql
-- 两个不同的索引列排序,MySQL 只能选择其中一个
SELECT * FROM device
WHERE station_id = '123'
ORDER BY device_name, create_time;
替代方案:建立包含排序字段的联合索引。
避坑总结
| 原则 | 说明 |
|---|---|
| 建联合索引优于多个单索引 | 能建联合索引,就别建单个索引 |
| 删除无用索引 | 高并发系统要控制索引数量 |
| 能用 WHERE 过滤不用 HAVING | HAVING 在分组后过滤,性能更差 |
| 必要时用 FORCE INDEX | 当优化器选择不当时手动干预 |
02 | 慢查询排查三板斧:EXPLAIN + 慢查询日志 + Arthas
标准排查流程
接口响应慢 → 确认是否数据库交互慢(加日志计时)→ 开启慢查询日志
→ DESC / EXPLAIN 查看执行计划 → 分析索引使用情况 → 优化 SQL
第一步:接口分段计时
java
long start = System.currentTimeMillis();
List<Device> devices = deviceMapper.selectByStation(stationId);
log.info("查询设备耗时: {}ms", System.currentTimeMillis() - start);
第二步:查看执行计划
sql
EXPLAIN SELECT * FROM patrol_task
WHERE station_id = '123' AND task_status = 1
ORDER BY create_time DESC LIMIT 10;
关注字段:
type:ALL(全表扫描)→ 需要优化key:实际使用的索引rows:预估扫描行数Extra:Using filesort(需要优化排序)、Using temporary(临时表)
第三步:开启慢查询日志
sql
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 超过 1 秒记录
实战案例:视图导致的缓存命中率低
问题 :v_station_node 视图没有包含所有变电站的点位数据,导致部分变电站查询返回空值,缓存命中率低。
排查过程:
- 发现接口响应慢 → 确认是数据库查询慢
- EXPLAIN 发现走了全表扫描
- 分析发现视图过滤了部分数据
- 改为查询主表,缓存命中率大幅提升
经验 :查询主表始终是基准,基于主表做优化,再用 Redis 加速查询。
03 | JOIN 优化:INNER JOIN vs LEFT JOIN 的正确选择
ON 与 WHERE 的致命区别
sql
-- 错误写法:LEFT JOIN 的过滤条件放在 WHERE 中,退化为 INNER JOIN
SELECT t.*, IFNULL(t2.is_config, 0) AS isConfig
FROM as_station_blltree t
LEFT JOIN as_node_config t2
ON t.station_id = t2.station_id AND t2.node_id = t.data_id
WHERE t.station_id = '123'
AND t2.is_config = 1; -- 这一行会导致 LEFT JOIN 变 INNER JOIN
-- 正确写法:LEFT JOIN 的过滤条件放在 ON 中
SELECT t.*, IFNULL(t2.is_config, 0) AS isConfig
FROM as_station_blltree t
LEFT JOIN as_node_config t2
ON t.station_id = t2.station_id
AND t2.node_id = t.data_id
AND t2.is_config = 1 -- 条件放在 ON 中
WHERE t.station_id = '123';
核心原则 :WHERE 子句在连接完成后过滤,会把 LEFT JOIN 变成 INNER JOIN 的效果。需要保留左表全部数据时,过滤条件必须放在 ON 子句中。
小表驱动大表
sql
-- 优化前:大表在前面
SELECT * FROM patrol_record r -- 100 万行
INNER JOIN station s ON r.station_id = s.id; -- 100 行
-- 优化后:小表在前面
SELECT * FROM station s -- 100 行
INNER JOIN patrol_record r ON s.id = r.station_id; -- 100 万行
避坑总结
- 能用 INNER JOIN 不用 LEFT JOIN,除非业务确实需要保留左表全部数据
- 多个 INNER JOIN 的顺序不影响结果,但影响性能
- 子查询转连接查询可以显著提升性能
- for 循环里写查询是最费劲的------阿里巴巴开发规范明确禁止
04 | 批量操作的坑:MySQL 一次能接收多大数据
批量插入的 MySQL 限制
java
// MyBatis foreach 批量插入
INSERT INTO patrol_signal (station_id, signal_name, signal_code)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.stationId}, #{item.signalName}, #{item.signalCode})
</foreach>
问题 :当 list 包含上万条数据时,MySQL 的 max_allowed_packet 参数限制了单次请求的最大数据量,超出后会直接报错。
解决方案:分批提交,每批 500~1000 条。
java
// 分批插入
List<List<Signal>> batches = Lists.partition(signals, 500);
for (List<Signal> batch : batches) {
signalMapper.batchInsert(batch);
}
foreach 的性能问题
MyBatis 的 <foreach> 会对每个参数做占位符替换,当参数量过大时:
- SQL 语句过长,解析慢
- MySQL 的预编译缓存无法复用
优化方案 :MyBatis-Plus 的 saveBatch 方法内部已实现分批。
java
// MyBatis-Plus 批量插入,自动分批
signalService.saveBatch(signals, 500);
UPDATE 批量操作
sql
-- 同时 UPDATE 多条记录(CASE WHEN 方式)
UPDATE device
SET device_status = CASE
WHEN id = 1 THEN 'online'
WHEN id = 2 THEN 'offline'
ELSE device_status
END
WHERE id IN (1, 2, 3);
-- REPLACE INTO / ON DUPLICATE KEY UPDATE
INSERT INTO patrol_config (station_id, config_key, config_value)
VALUES ('123', 'interval', '30')
ON DUPLICATE KEY UPDATE config_value = VALUES(config_value);
05 | 数据库设计中的"备用字段"哲学
冗余字段的利弊
在对接第三方系统时,备用字段(extra / extend / reserve)是救命的。
sql
CREATE TABLE device_info (
id BIGINT PRIMARY KEY,
station_id VARCHAR(64),
device_name VARCHAR(255),
device_type INT,
manufacturer VARCHAR(128),
extra_attr JSON -- 存储厂家特有属性
);
JSON 字段查询:
sql
-- MySQL 5.7+ 支持 JSON 字段查询
SELECT * FROM device_info
WHERE JSON_SEARCH(extra_attr, 'one', 'camera') IS NOT NULL;
宽表 vs 窄表
窄表(标准化设计):一个页面用到多张表的属性,每张表只存自己的属性。查询时需要多表 JOIN。
宽表(反范式设计):将常用字段冗余到一张表,减少 JOIN 次数,提高查询效率。
实战经验 :宽表在查询场景下确实方便,但在更新场景下存在数据一致性问题。选择宽表还是窄表,取决于读多还是写多。
流水表与实时表
| 表类型 | 用途 | 特点 |
|---|---|---|
| 流水表 | 存储全量历史数据 | 只增不改 |
| 实时表 | 存储最新状态 | 覆盖更新 |
| 历史表 | 按月/年归档 | 减少主表数据量 |
sql
-- 文件服务分表示例
pt_upload_file -- 当前月
pt_upload_file_202401 -- 历史数据
pt_upload_file_202402
08 | 生产环境 SQL 优化的 7 个黄金法则
- 索引优化是见效最快的方法,但后续仍需改代码
- 覆盖索引减少回表:将 SELECT 列包含在索引中
- UNION ALL 替代 UNION:UNION 会做去重操作,性能开销大
- 能用 WHERE 过滤不用 HAVING:HAVING 在分组后过滤
- EXISTS 替代 IN (大表场景):
WHERE EXISTS (SELECT 1 FROM ...)效率更高 - 小表驱动大表:嵌套查询中,外层应该是小表
- 通过 SQL 处理的逻辑,随着数据量增长都会变慢------唯一能做的是保证单个 SQL 的稳定性
核心认知:SQL 写得好,下班下得早。
模块二:MyBatis / MyBatis-Plus 深度实战
09 | MyBatis 动态 SQL 减少 if-else 的艺术
动态 SQL 替代 Java 分支判断
正面案例(MyBatis 动态 SQL):
xml
<select id="selectByCondition" resultType="DeviceDTO">
SELECT * FROM device
<where>
<if test="stationId != null and stationId != ''">
AND station_id = #{stationId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY create_time DESC
</select>
<where> 标签会自动处理第一个 AND,比 WHERE 1=1 更优雅。
discriminator 鉴别器
xml
<resultMap id="deviceMap" type="Device">
<discriminator column="device_type" javaType="int">
<case value="1" resultType="CameraDevice"/>
<case value="2" resultType="RobotDevice"/>
<case value="3" resultType="UAVDevice"/>
</discriminator>
</resultMap>
10 | MyBatis 传参的 10 个隐藏坑
坑 1:Integer 值为 0 被转为空串
xml
<!-- 当 param 值为 0 时,test="param != ''" 仍然为 true -->
<if test="param != null and param != ''">
AND status = #{param}
</if>
原因 :MyBatis 对 Integer 类型做 OGNL 表达式判断时,0 != '' 结果为 true。
解决方案 :对 Integer 类型只判断 != null。
坑 2:Mapper 传多个参数
java
// 方式一:@Param 注解
List<Device> selectByPage(
@Param("stationId") String stationId,
@Param("pageNum") int pageNum
);
11 | 从 MyBatis 迁移到 MyBatis-Plus 的实战收益
| 维度 | MyBatis | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 手写 XML | 自动生成 |
| 代码迁移 | Mapper XML 与实体绑定 | 更好的解耦,迁移更灵活 |
| 字段变更 | 修改多处 XML | 只改实体类 |
| 分页 | 手动集成 PageHelper | 内置分页 |
实战心得:用 MyBatis-Plus 代码更好迁移。代码转换的地方比较高效。
模块三:Spring 生态深度解析
15 | Spring 事务的 5 个诡异场景
场景 1:同类方法调用事务失效
java
@Service
public class DeviceService {
public void batchUpdate(List<Device> devices) {
for (Device d : devices) {
updateOne(d); // 直接调用同类方法,@Transactional 不生效
}
}
@Transactional
public void updateOne(Device device) {
deviceMapper.update(device);
}
}
解决方案:注入自身代理或拆到不同 Service。
场景 2:try-catch 吞掉异常
java
@Transactional
public void transfer(TransferDTO dto) {
try {
accountMapper.deduct(dto.getFromId(), dto.getAmount());
accountMapper.add(dto.getToId(), dto.getAmount());
} catch (Exception e) {
// 异常被 catch 吞掉,事务不会回滚
}
}
解决方案:catch 后手动回滚。
18 | Spring Boot 过滤器与拦截器
Filter vs Interceptor 本质区别
| 维度 | Filter | Interceptor |
|---|---|---|
| 规范 | Servlet 规范 | Spring 框架 |
| 实现原理 | 函数回调 | 动态代理 |
| 作用范围 | 所有请求 | 仅 Controller |
| 配置方式 | web.xml 或 @Component | WebMvcConfigurer |
21 | Spring Boot 统一返回体设计
java
@Data
@Builder
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
return Result.<T>builder().code(200).msg("success").data(data).build();
}
public static <T> Result<T> error(int code, String msg) {
return Result.<T>builder().code(code).msg(msg).build();
}
}
全局异常处理:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
return Result.error(500, "系统繁忙,请稍后重试");
}
}
模块四:Redis 实战与缓存策略
23 | 缓存一致性问题的排查与解决
典型案例
现象 :变电站增加所属组织分组后,查询一直不生效。
根因:更新数据库后没有清除对应缓存。
解决方案:
java
public void updateStationGroup(String stationId, String groupId) {
stationMapper.updateGroup(stationId, groupId);
String cacheKey = "station:detail:" + stationId;
redisTemplate.delete(cacheKey);
}
缓存更新策略
| 策略 | 适用场景 | 一致性 |
|---|---|---|
| Cache Aside | 读多写少 | 最终一致 |
| Write Through | 写多读少 | 强一致 |
| Write Behind | 高并发写入 | 最终一致 |
27 | Redis 在设备联动系统中的实战应用
批量删除 Redis Key
java
// Lua 脚本批量删除
String luaScript = "local keys = redis.call('keys', ARGV[1]) " +
"for i=1,#keys,5000 do " +
"redis.call('del', unpack(keys, i, math.min(i+4999, #keys))) " +
"end return #keys";
redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.emptyList(),
"dsa:file-id:*"
);
项目经验:redis key 的管理要特别注意,不然后期项目没法维护。MQ 同样也需要管理起来。
模块五:线上问题排查方法论
29 | jstack + jps 定位线程池爆满问题
生产案例:xx现场线程泄漏
bash
# 1. 查看Java进程
jps -l
# 2. 打印线程栈
jstack 35184 > thread_dump.log
# 3. 分析
grep -c "BLOCKED" thread_dump.log
根因:不断创建新线程,没有使用线程池复用。
30 | 日志打印的艺术
java
// 使用 topic 分离不同模块日志
@Slf4j(topic = "ROBOT_INFO")
public class RobotService {
log.info("机器人信号上报: {}", signal);
}
@Slf4j(topic = "LINKAGE_ERROR")
public class LinkageService {
log.error("联动执行失败: {}", errorMsg);
}
模块六:高质量代码设计原则
33 | 方法设计的 5 条军规
- 单一职责:一个方法只干一件事
- 主方法不超过 30 行
- 方法调用不超过 3 层:Controller → Service → Mapper
- 参数用独立对象,不耦合继承关系
- 凡靠日志说明业务的,十之八九是方法没拆够细
35 | 异常处理最佳实践
try-catch 最短路径原则
java
// 好:在可能出错的最近处 try-catch
queryDevices();
assembleData();
try {
sendInstruction(); // 调用第三方,最可能出错
} catch (Exception e) {
log.error("指令发送失败", e);
updateStatus(taskId, "SEND_FAILED");
}
第三方调用必须 try-catch
涉及第三方的都要 try-catch,不然报错没日志。
36 | 代码重构实战
好的代码是设计 出来的,不是从头到尾写出来的。在一个大系统里面抽丝剥茧然后出设计再重构升级才是真大佬。
模块七:微服务架构落地
40 | Spring Cloud Alibaba 核心组件全景
| 组件 | 作用 | 替代方案 |
|---|---|---|
| Nacos | 服务注册 + 配置中心 | Eureka + Config |
| Sentinel | 限流熔断 | Hystrix |
| Seata | 分布式事务 | 手动补偿 |
| RocketMQ | 消息队列 | Kafka |
| Gateway | API 网关 | Zuul |
45 | 多系统数据对接的标准化架构
统一入口 + 适配器模式
java
@PostMapping("/api/data/receive")
public Result<?> receiveData(@RequestBody DataRequest request) {
String adapterId = request.getAdapterId();
DataAdapter adapter = adapterFactory.getAdapter(adapterId);
return adapter.process(request.getBody());
}
模块八 & 九:中间件与工程实践(精华要点)
49 | MinIO 文件服务性能优化
yaml
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
51 | 规范驱动的架构设计
电力行业项目基于规范文档开发:阅读规范 → 提取功能要求 → 设计系统架构 → 可持续迭代。
54 | 代码评审 Checklist
- 入参做非空校验
- 不在 for 循环里写查询
- 批量操作分批提交
- 第三方调用必须 try-catch
- 不使用魔法数字
- Redis key 包含模块前缀
- 数据更新后清除缓存
附录:开发方法论速查
SQL 编写口诀
能用 INNER JOIN 不用 LEFT JOIN
能用 WHERE 不用 HAVING
能用 UNION ALL 不用 UNION
能用覆盖索引不做回表
代码质量口诀
方法只做一件事,主方法不超三十行
参数封装成对象,入参出参全打印
第三方必 try-catch,异常不吞就近处理
排查问题口诀
分段计时定瓶颈,EXPLAIN 看执行计划
日志分级 topic 分,入参出参全记录
jstack 看线程状态,tcpdump 抓网络包
声明:本专栏所有案例均来自真实生产环境实战经验,涉及的业务场景做了脱敏处理。