从一次“失败”的重构说起:技术探索与实践总结

Redis看门狗
2025-06-28 03:52
阅读 514

引言:为什么是这次重构?

引言:为什么是这次重构?

去年我们团队接手了一个老系统升级的项目。这个系统原本采用的是单体架构,核心模块用Java编写,运行在Tomcat上,数据库是MySQL。随着业务量的增长和用户需求的变化,原有的架构逐渐显露出几个严重问题:

  • 系统响应慢,尤其在高并发场景下
  • 功能迭代缓慢,改动一处动辄牵一发动全身
  • 模块间耦合严重,测试成本极高

于是我们决定进行一次整体的技术升级和架构调整。初衷很简单——用Spring Cloud搭建微服务架构、引入Kubernetes做容器编排、用Redis缓存提升性能……但过程远比我们想象的要复杂得多。

这篇文章不是复盘整个项目的所有细节,而是想结合我在这次实践中的真实经历,谈谈一个话题:如何在实际项目中进行有效的技术探索与落地


问题描述:理想很丰满,现实很骨感

问题描述:理想很丰满,现实很骨感

一开始,我们的计划看起来非常合理:

  1. 将原有系统按业务功能拆分为多个微服务模块(订单、用户、商品等)
  2. 使用Spring Boot + Spring Cloud构建微服务体系
  3. 部署在Kubernetes集群中
  4. 使用Redis+RabbitMQ实现数据异步解耦

听起来是不是很标准?我也这么认为。然而真正动手的时候才发现,事情没有那么简单。

实际遇到的问题包括:

  • 代码结构混乱,历史包袱沉重:大量类职责不清晰,Service层里调DAO、发MQ、写日志,什么都有。
  • 微服务边界划分不清楚:哪些逻辑应该拆出去?哪个服务该先启动?不同服务之间怎么通信最高效?
  • 开发流程不规范:本地起服务联调难、环境配置繁琐、CI/CD链路缺失。
  • 上线后性能不如预期:用了Redis却出现缓存穿透、热点key失效导致雪崩、部分接口响应时间反而更长了……

这些问题像一座座小山一样接踵而来。我们在技术选型上反复摇摆,在实施过程中踩了不少坑,也积累了不少经验。


解决方案:稳扎稳打,步步为营

解决方案:稳扎稳打,步步为营

面对一堆棘手的问题,我们决定放慢节奏,先回归本源,思考几个关键问题:

“我们要解决的核心问题是什么?”
“现有技术手段能否有效支持当前的业务形态?”
“是否有过度设计的风险?”

最终,我们将整个重构过程划分为三个阶段:

第一阶段:服务边界梳理 + 架构分层

我们在原有系统中做了一轮详细的功能梳理,画出了一张系统依赖图谱,并据此制定了初步的服务边界。这里的关键做法包括:

  • 每个服务必须有清晰的入口和出口
  • 所有跨服务调用必须通过API或消息队列
  • DB层面尽量保证独立部署和迁移能力

这一阶段我们并没有急于上新技术,而是让团队成员先理解新架构的结构和约束。

第二阶段:基础设施先行,开发协同提效

我们优先建设了以下几套基础设施:

  • 本地开发模拟服务注册中心(使用Docker)
  • 一套共享的测试公共库(含Mock Server)
  • 基于GitLab CI的流水线基础框架
  • 统一的日志采集方案(Logstash+ELK)

这些基础设施虽然看似“看不见摸不着”,但极大地提升了开发效率和协作体验。

第三阶段:服务逐步拆分 + 技术验证并行推进

我们选择了其中一个相对独立的业务模块作为试点——“优惠券发放服务”。

在这个过程中,我们重点验证了以下几个技术点:

  • 微服务间通信方式(Feign vs RPC vs Message Queue)
  • 缓存使用策略(LocalCache + Redis组合)
  • 分布式事务方案(Seata vs 本地事务表)
  • 监控体系搭建(Prometheus + Grafana)

每个技术方案我们都做了对比测试,并结合实际效果做出选择。


代码实践:从一个接口看优化过程

为了让你更直观地看到变化的过程,我拿一个简单的优惠券领取接口来说明优化思路。

最初版本的伪代码如下:

public ResponseDTO<Coupon> receiveCoupon(Long userId, String couponCode) {
    Coupon coupon = couponDAO.findByCode(couponCode);
    if (coupon == null) {
        return error("优惠券不存在");
    }
    
    User user = userDAO.findById(userId);
    if (user.isBanned()) {
        return error("用户被封禁");
    }

    Integer leftCount = inventoryDAO.getStock(coupon.getCode());
    if (leftCount <= 0) {
        return error("库存不足");
    }

    // 同步操作:发MQ + 写日志
    couponUseLogDAO.insert(...);
    messageProducer.sendMessage(...);

    return success();
}

这段代码的问题非常明显:

  • 数据库查询多
  • 没有缓存
  • 所有操作都在主线程中完成
  • 多处阻塞

经过优化后的版本变成这样:

@Async
private void asyncSendMQAndLog(CouponRecord record) {
    couponUseLogDAO.insert(record);
    messageProducer.sendMessage(record.toMessage());
}

public ResponseDTO<Coupon> receiveCoupon(Long userId, String couponCode) {
    // 使用LRU本地缓存 + Redis二级缓存
    Coupon coupon = couponCacheService.get(couponCode); 
    if (coupon == null) {
        return error("优惠券不存在");
    }

    Boolean isBanned = localUserCache.get(userId);
    if (isBanned == null) {
        isBanned = userRemoteService.isBanned(userId);
        localUserCache.put(userId, isBanned);
    }

    if (isBanned) {
        return error("用户被封禁");
    }

    Long stock = redisTemplate.opsForValue().get("stock:" + couponCode);
    if (stock == null || stock <= 0) {
        return error("库存不足");
    }

    // Redis Lua脚本减库存,原子性保障
    Long newStock = redisTemplate.execute(decreaseScript, keys, args);

    CouponRecord record = new CouponRecord(...);
    asyncSendMQAndLog(record);

    return success();
}

主要改进点:

  1. 加入本地+Redis双缓存降低DB压力
  2. 使用Redis减少热点数据库访问
  3. 将日志和发送消息异步化
  4. 减库存操作由Lua脚本保证原子性

结果:接口平均响应时间从800ms降到120ms以内,TPS提升5倍以上。


踩坑经验:那些深夜debug的小故事

说到技术探索,就不得不说说那些“踩坑”经历。印象最深的一次是在做分布式锁时碰到的问题。

我们当时在优惠券库存扣减的逻辑中使用Redis加锁机制,类似下面这种写法:

Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent("lock:coupon:"+code, "locked", 10, TimeUnit.SECONDS);
if (!lockAcquired) {
    return retryLater();
}
try {
    // do something...
} finally {
    redisTemplate.delete("lock:coupon:"+code);
}

表面上看起来没问题,但线上还是出现了超卖!

后来查了很久才发现问题所在:

  • 如果应用在执行do something...期间宕机,锁会过期自动释放,其他请求又可以进入临界区
  • 我们删除锁的方式也不安全——有可能误删别人的锁

解决方案最终采用了RedLock算法的一个简化版,并且配合Lua脚本来控制锁的获取和释放。

还有一个教训是关于服务降级和熔断。最初我们没太重视这部分,上线后某个下游服务异常造成连锁故障,整个系统大面积不可用。最后我们引入Hystrix组件并配合Sentinel限流,才把稳定性真正提上来。


效果总结:从“改”到“优”

经过三个多月的努力,这套系统最终成功上线,并带来了明显收益:

  • 接口平均响应时间下降60%
  • 新功能开发周期缩短30%左右
  • 团队协作效率显著提升
  • 部署灵活度增强,灰度发布、流量隔离、弹性伸缩都能轻松实现

更重要的是,整个团队对微服务的理解更深入了,开发习惯也发生了转变:开始注重模块划分、接口设计、日志规范、自动化测试等细节。


经验分享:技术人不该只关注“用了啥”,更要关心“为啥用它”

回顾这次重构经历,我想给正在做或者打算做类似项目的你几点建议:

1. 技术选型不要盲目追新

我们最初尝试引入Nacos作为注册中心,但由于运维经验不足、文档不完整,折腾了好几天还没搞定。最终回归Consul,发现稳定性和社区支持力度都更高。

实用原则:能跑起来的才是好技术,能维护住的才是好方案。

2. 划清服务边界比选择技术栈更重要

微服务不是万能药,前提是要有合理的业务拆分。如果服务划分不合理,再多的“高大上”技术也救不了场。

3. 要为后续演进留下余地

我们在初期预留了很多可插拔的扩展点,比如:

  • 消息队列抽象层(未来可能替换RocketMQ)
  • 缓存适配层(本地缓存可更换为Caffeine或Ehcache)
  • 日志格式标准化(便于后续接入不同监控平台)

这些看似“冗余”的设计,为后续的快速迭代提供了很大帮助。

4. 不要忽视团队成长和技术沉淀

我们每周固定安排技术分享会,每次都会围绕一个具体问题展开讨论。比如:

  • “缓存一致性如何处理?”
  • “微服务之间的数据同步最佳实践?”
  • “异步任务调度怎么设计?”

这些交流不仅解决了当下的问题,也让大家对系统有了更全面的认识。


结语:技术探索是不断试错的过程

写到这里,我不禁想起那段时间每天加班的日子。那时候,我们一边coding,一边debug,一边吵架,一边复盘。有时会觉得“这玩意儿能不能行得通啊?”,也有时候会觉得“原来还可以这样玩!”

但无论怎样,正是这些真实的挑战和困惑,让我们一步步走向成熟,也让我深刻体会到一句话:

技术的价值不在于它多么炫酷,而在于它是否真正解决了实际问题。

如果你也在做类似的技术探索,希望我的经验和教训对你有所启发。欢迎留言交流,一起探讨更多实战经验。

评论 0

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