技术探索与实践的一些思考
开篇:为什么写这篇文章

做技术这件事,从来都不是一条直线。我们总是在不断试错、调整、再尝试的过程中前进。回想我自己这些年的开发经历,从最开始的“能跑起来就行”,到现在讲究架构设计、代码质量、可维护性、系统稳定性……这一路上踩过的坑、掉过的坑、甚至有时候爬出来以后才发现那其实还是个坑。
这篇文章不是什么高谈阔论的技术原理剖析,也不是空洞的架构理论讲解。我想分享的是我在真实项目中遇到的技术挑战、当时的决策思路、实践中踩过的雷,以及最后落地的效果。希望通过这些实际经验,给大家一些启发或者避坑的参考。
问题描述:一次高并发场景下的性能优化挑战

事情发生在两年前,我参与了一个金融类的风控系统项目。该系统的主责是对用户交易行为进行实时风险评估和拦截,核心指标是:
- 峰值QPS要达到3000+
- 单笔请求延迟控制在200ms以内
- 系统整体可用性要保障在99.99%以上
刚接手时,整个系统是基于 Spring Boot + MyBatis + MySQL 架构搭建的,部署在单 IDC 的多个节点上。随着数据量增加、规则引擎复杂度上升,系统逐渐暴露出几个严重的问题:
- 响应延迟波动大:有时几毫秒,有时直接飙到上千毫秒。
- 热点规则查询频繁导致数据库压力过大。
- 系统无法水平扩展,扩容需要手动修改配置。
- 日志追踪困难,出问题后定位效率极低。
这些痛点在一次压测中被彻底暴露出来——QPS 刚过 1500,就开始出现大面积超时和熔断,线上也开始有用户投诉卡顿。项目压力山大,客户对交付节奏非常紧张。
解决方案:技术选型与架构升级

面对这种情况,我们必须重构系统。目标很明确:提升性能、提高可用性、增强可观测性。
技术选型的几个关键点:
1. 引入缓存分层策略(Redis + Caffeine)
最开始我们用的都是数据库直查,但很多规则其实是热点配置。于是我们在业务层加了两个缓存层:
- 本地缓存(Caffeine):用于缓存高频访问且变更频率不高的规则。
- 分布式缓存(Redis):作为本地缓存失效后的兜底方案,并处理变更通知机制。
小插曲:刚开始只用了 Redis,结果网络抖动时 QPS 直接崩了。后来加上本地缓存后,即使 Redis 出问题,也能维持基本服务稳定。
2. 使用异步非阻塞编程模型(Reactor Netty + WebFlux)
为了提升吞吐能力,我们将原来传统的 Spring MVC 模式升级为 WebFlux,结合 Reactor 编程模型,将部分 IO 调用(如数据库、Redis、外部接口)改为非阻塞方式处理。
public Mono<ResponseEntity<String>> evaluateRisk(Flux<RequestData> dataStream) {
return dataStream
.flatMap(req -> ruleService.checkRule(req)
.onErrorResume(ex -> {
// 错误降级处理逻辑
return fallbackHandler.handleEx(req);
}))
.collectList()
.map(results -> ResponseEntity.ok("Processed"));
}
这种异步流水线式的处理,使每个请求之间的耦合降低,资源利用率大幅提升。
3. 引入 Kafka 实现解耦与削峰填谷
系统中有大量的异步记录行为(比如事件日志、风险记录等)。原来的做法是同步写库,导致每次都要等待数据库返回。我们通过引入 Kafka 进行异步写入,不仅提升了主流程性能,也增强了系统的容错能力和伸缩性。
4. 使用 Zipkin + Sleuth 实现全链路追踪
为了解决日志难以追踪的问题,我们集成了 Spring Cloud Sleuth 和 Zipkin,每个请求都有一个全局 traceId,可以在 Kibana 或 Zipkin 中快速定位问题根因。
5. 服务治理:Nacos + Sentinel
为了应对服务依赖不稳定的情况,我们使用了 Sentinel 做限流、降级、熔断,配合 Nacos 做配置中心和服务发现。这样一来,我们可以动态调整规则,比如某个依赖服务有问题时,可以自动切换备用策略或直接降级。
代码实践:关键实现片段
示例一:本地缓存 + Redis 复合读取
public Rule getRule(String ruleId) {
// 先查本地缓存
Rule rule = caffeineCache.getIfPresent(ruleId);
if (rule == null) {
// 本地没有再去 Redis 查
String redisKey = "rules:" + ruleId;
String json = redisTemplate.opsForValue().get(redisKey);
if (json != null) {
rule = objectMapper.readValue(json, Rule.class);
// 回填本地缓存
caffeineCache.put(ruleId, rule);
} else {
// 最终降级逻辑或从 DB 加载
rule = loadFromDB(ruleId);
if (rule != null) {
redisTemplate.opsForValue().set(redisKey, objectMapper.writeValueAsString(rule));
caffeineCache.put(ruleId, rule);
}
}
}
return rule;
}
示例二:Sentinel 限流规则配置(Java)
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("evaluateRisk");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(3000); // 设置 QPS 限制
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
踩坑经验:那些让人头大的时刻
Redis 数据一致性问题
我们一开始没考虑缓存穿透和雪崩情况,结果上线第一天就遇到了缓存失效集中重建导致数据库打爆的问题。解决办法是:- 本地缓存设置随机过期时间;
- Redis 加布隆过滤器;
- 数据库读写分离 + 熔断降级。
WebFlux 并发模型理解不够透彻
一开始我们把所有的数据库操作都放到Schedulers.boundedElastic(),结果线程池打满,反而影响性能。最终通过合理分配线程池资源、拆分任务粒度解决了问题。Kafka 消费积压问题
上线初期某天凌晨 Kafka 消息突然大量积压,排查发现是消费端异常未提交 offset 导致反复重试。后来增加了消息重试队列 + 自动告警机制来规避。TraceId 在跨语言调用中丢失
有一段时间微服务中有些 Go 写的服务,TraceId 无法透传,导致链路中断。通过统一定义 header 传递格式,并让网关自动注入 TraceId 来解决。
效果总结:优化后的表现
经过三个月的重构和优化,系统上线后效果显著:

| 指标 | 优化前 | 优化后 |
|---|---|---|
| 峰值QPS | 1500 | 4200+ |
| P99延迟 | >800ms | <160ms |
| 日均错误数 | 500+ | <10 |
| 扩展性 | 需手动调整 | 支持弹性扩容 |
| 排查耗时 | 平均2小时+ | 平均10分钟内 |
更令人欣慰的是,在后续几次促销活动中,系统稳如老狗,客户满意度大幅提升。
经验分享:给同行的一些建议
不要迷信“新技术”
技术本身没有好坏之分,关键是是否适合当前的业务场景。比如 WebFlux 不一定比传统 Spring MVC 快,它更适合 IO 密集型任务。盲目跟风可能会让你陷入不必要的复杂性。性能优化要从源头入手
很多人上来就想到换数据库、加缓存,其实真正有效的是先做 profiling。找出真正的瓶颈在哪,否则很可能只是在救火,治标不治本。提前规划可观测性
日志、监控、链路追踪,这些都不是锦上添花的东西,而是必备的基础建设。尤其在微服务时代,缺了这些工具,调试就像蒙眼开车。团队沟通比代码更重要
架构做得再漂亮,如果其他同学看不懂、改不动,那也是白搭。文档、图示、设计说明一定要清晰。必要时开个小会讲清楚比一堆注释更有用。持续集成/部署不能省
CI/CD 是保障高质量交付的关键环节。虽然前期搭建成本可能比较高,但它带来的收益远大于投入,特别是在多分支、多环境协作的情况下。
写在最后

回头看这个项目,虽然中间踩了不少坑,但现在想想,每一步都值得。技术的成长,从来都不是平滑的曲线,而是一次次解决问题后的积累。
希望我的这次实践经历能给你带来一些共鸣或启发。技术这条路,道阻且长,但只要脚踏实地,总会看到光。
如果你也在类似场景下奋斗着,欢迎留言交流,一起踩坑,一起成长。

评论 0