如何技术探索与实践?

唐智
2025-06-29 15:23
阅读 536

从一次重构谈起:技术探索与实践的那些事

背景介绍:为什么要写这篇文章?

事情还得从去年说起,那时候我所在的团队在维护一个内部使用的用户权限中心服务。这个服务已经运行了好几年,功能不算多,但因为早期是业务驱动快速开发,代码结构比较乱,性能也不太理想,尤其是随着公司用户量上涨,接口响应时间越来越长,偶发的线程阻塞和内存溢出问题也频繁出现。

当时,我们决定对这个模块进行一次彻底的重构,同时也在考虑是否引入一些新技术来提升整体架构的稳定性和可维护性。正好那段时间我也在思考一个问题:作为工程师,在面对不确定的技术选型和复杂系统时,如何有效地进行技术探索,并最终落地到生产环境?

今天就想通过这次亲身经历,聊聊我在项目中的一些探索过程、技术选型的取舍,以及最后的实践心得。希望能给有类似困扰的朋友带来一点启发。


项目背景与核心挑战

我们要重构的服务是一个用户鉴权模块,主要职责包括:

  • 用户登录认证(OAuth2 + JWT)
  • 用户权限管理(RBAC模型)
  • 接口级访问控制
  • 审计日志记录

最初是用 Spring Boot 1.x 搭建的,MySQL 做持久化存储,Redis 用于缓存权限数据。由于初期没有统一的设计规范,整个系统呈现出一种“各自为政”的状态,不同的权限逻辑分散在多个 service 中,调用链错综复杂,导致后续排查问题非常困难。

最严重的问题出现在去年 Q3 的压测过程中:当并发请求达到 500TPS 时,QPS 开始出现断崖式下跌,而且 GC 频繁 Full GC。后来排查发现,一方面是线程池配置不合理,另一方面是大量冗余的对象创建导致 JVM 内存压力陡增。


技术方案选择与初步设计

目标设定

我们的目标很明确:

  • 提升系统性能和稳定性
  • 清晰架构边界,便于后期扩展
  • 引入更现代的技术栈以增强维护便利性
技术选型对比

第一个要决定的就是是否升级框架版本。Spring Boot 1.x 已经不在主流支持范围内,社区生态也逐渐往 2.7 / 3.x 过渡。我们评估后决定直接迁移到 Spring Boot 2.7,这样既能保持相对稳定的 API 兼容性,也能享受到新特性带来的便利。

接下来是数据库和缓存部分的优化:

项目 原始方案 新方案 变化说明
数据库 MySQL 单实例 MySQL 主从 + Sharding
缓存层 Redis 集群部署 Redis + Caffeine
线程模型 默认线程池 自定义线程池 + RateLimiter
日志监控 Logback Logback + ELK + Prometheus

此外,为了减少外部系统的依赖耦合,我们还引入了 Apache Kafka 来异步处理审计日志和事件广播。


关键实现思路与代码片段

1. 架构分层与责任划分

我们采用了清晰的四层结构:

Controller → Service → Repository → Domain (DDD)

并基于接口定义做了解耦设计。例如权限相关的操作抽象出了 PermissionService

public interface PermissionService {
    boolean checkPermission(String userId, String resourceId, String action);
    List<String> listUserResources(String userId);
    void updatePermission(PermissionUpdateRequest request);
}
2. 多级缓存策略

针对高频读取的权限信息,我们做了本地缓存 + 分布式缓存的双保险机制:

public class PermissionCachingService implements PermissionService {

    private final LoadingCache<PermissionKey, Boolean> localCache;
    private final RedisTemplate<String, Boolean> redisTemplate;

    public PermissionCachingService(RedisTemplate<String, Boolean> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.localCache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(this::loadFromRemote);
    }


![技术应用场景-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062915/50299c32-4a15-4b3a-841b-4f459eaf12f4.jpg)


    private Boolean loadFromRemote(PermissionKey key) {
        return redisTemplate.opsForValue().get(key.toString());
    }

    @Override
    public boolean checkPermission(String userId, String resourceId, String action) {
        PermissionKey key = new PermissionKey(userId, resourceId, action);
        return Optional.ofNullable(localCache.getIfPresent(key))
            .orElse(loadFromRemote(key));
    }
}

这里使用的是 Caffeine + Redis 的组合,兼顾了性能与一致性。

3. 高频场景下的熔断限流

为了避免突发流量冲击系统,我们在网关层(基于 Zuul)增加了 RateLimiter,并在内部服务之间引入 Hystrix 进行降级兜底。

虽然现在主流趋势偏向 Resilience4j 和 Sentinel,但在当时的框架兼容性下,Hystrix 是更稳妥的选择。

hystrix:
  threadpool:
    default:
      coreSize: 20
      maximumSize: 30
      maxQueueSize: 100

踩坑经验分享

1. 升级 Spring Boot 版本后的类加载冲突

原本以为只是简单的版本升级,结果上线后发现有些 Bean 无法正确注入。排查后发现,是因为 Spring Boot 2.7 默认启用的 spring.factories 加载方式已经发生变化,部分老插件不兼容导致组件扫描失败。

解决方案也很简单:将老式的 META-INF/spring.factories 改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 格式,同时逐步替换掉不再推荐使用的 starter 包。

2. Redis Cluster 下的 Key Hash 问题

为了提高缓存命中率,我们曾尝试使用 Redis Cluster 存储权限信息,但由于某些 key 的命名不规范,出现了 hash slot 分配不均的问题,甚至导致部分节点负载异常高。

解决办法是统一使用 {userId}:{resourceId} 这样的格式,确保 key 分配均衡,并增加了一个 hash tags 标记 {userId},强制相同 user id 的 key 被分配到同一个 slot 中。

3. Kafka 异常堆积导致日志丢失

审计日志采用异步发送到 Kafka 的方式,但在压测阶段发现日志经常延迟十几秒才落地,甚至出现消息丢失的情况。

原因在于我们一开始用了 fire and forget 的方式发送消息,没有开启重试或确认机制。后来改为:

kafkaTemplate.send(topic, message).addCallback(
    success -> {
        // 成功回调
    },
    ex -> {
        // 失败重试逻辑
    });

并通过引入 Dead Letter Queue 机制兜底处理失败消息,这才解决了数据丢失问题。


最终效果与收益总结

经过三个迭代周期的重构与打磨,我们实现了以下成果:

指标 重构前 重构后 提升幅度
QPS ~800 ~3500 337%
平均响应时间 800ms 150ms -81%
GC Full GC 次数 每小时2~3次 每天不足一次 显著降低
故障恢复速度 数小时 分钟级自动恢复 提升明显

不仅提升了服务的整体性能,而且通过模块拆分和接口隔离,大大降低了后续新增功能的开发成本。


经验建议与未来规划

结合这次项目的体会,我想给正在做技术探索的你几点建议:

  1. 不要盲目追新,技术选型要符合当前业务发展阶段
    很多同学一听到某个“新兴”技术就跃跃欲试,其实更适合的做法是先理解其背后的设计理念和适用场景。比如我们当时就没选择用 DDD 的全套框架,而是吸收其思想来指导代码结构设计。

  2. 重视技术债的识别与治理
    尤其是在重构旧系统时,必须有一个清晰的边界划分机制,避免一边改一边引入新的混乱。我们当时制定了 “修改即清理” 的原则:凡是有改动的类,都要同步完成接口封装和注释补充。

  3. 持续监控 + 快速反馈闭环
    不要等到系统崩溃了才发现问题。我们这次在重构过程中就建立了完整的监控体系,包括:

    • Prometheus + Grafana 实时指标监控
    • ELK 收集日志
    • 接口埋点+ TraceID 跟踪
  4. 团队沟通至关重要
    技术方案不是闭门造车的结果。每次做关键技术决策前,我们都会组织小型讨论会,邀请前端、测试、运维的同学一起参与评审,真正做到了“让每个人都理解变化”。


写在最后

说实话,技术探索从来都不是一件轻松的事。它需要我们不断地学习、实验、验证,还要承受失败和质疑的风险。但正是这些不断试错的过程,让我意识到:“技术落地的本质,不是你用了多少酷炫的新玩意儿,而是你能否真正为业务创造价值。

希望这篇文章能给你一些不一样的视角,如果你也有类似的实践经验,欢迎留言交流。我们一起成长,共同进步。


作者简介:本文作者是一位在互联网一线奋战多年的 Java 工程师,热爱开源,热衷于技术架构演进与系统优化,现任某大型电商平台后端架构师。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝