🚗 从"拖拉机"到"特斯拉":SSH老古董系统的华丽变身记

38 阅读21分钟

作者寄语:作为一个在Java战场上摸爬滚打10年的老兵,我见过太多"年久失修"的系统。今天,咱们就来聊聊如何把一台"老爷车"改装成"超级跑车"!🏎️


📚 目录

  1. 前言:老系统的"七宗罪"
  2. 战前侦察:全面体检
  3. 战略规划:制定改造蓝图
  4. 技术选型:挑选你的"武器库"
  5. 实战演练:重构的N种姿势
  6. 风险管控:别把系统搞炸了
  7. 持续优化:让系统永葆青春

🎭 前言:老系统的"七宗罪" {#前言}

什么是SSH框架?

SSH不是你用来远程登录服务器的那个SSH!这里说的是:

  • Struts(展示层框架,2000年代的网红)
  • Spring(IoC容器,唯一还活跃的)
  • Hibernate(ORM框架,对象关系映射)

生活类比:SSH框架就像诺基亚手机,在它的时代是王者,但现在... 你懂的 😅

老系统的"七宗罪"

  1. 傲慢(Monolithic):一个War包打天下,动辄几百MB
  2. 贪婪(Memory):内存占用像无底洞
  3. 懒惰(Deployment):部署一次要半小时,改一行代码重启半天
  4. 嫉妒(Technology):看着别人用Spring Boot、微服务,只能干瞪眼
  5. 愤怒(Bug):改个Bug就像拆炸弹,不知道哪里会爆
  6. 暴食(Database):数据库连接池耗尽,全局事务一锅端
  7. 色欲(Coupling):代码耦合得像意大利面,扯不清楚

真实案例

某电商系统(基于SSH)
├── war包大小:380MB 😱
├── 启动时间:8分钟 ⏰
├── 单次部署:25分钟 💀
├── 代码行数:50万+ 📚
└── 维护人员:已跑路3批 🏃

🔍 战前侦察:全面体检 {#战前侦察}

第一步:画出"家谱图"(系统架构图)

工具推荐

  • 架构图:Draw.io、PlantUML、Excalidraw
  • 代码分析:SonarQube、IDEA的依赖关系图
  • 调用链路:Arthas、JProfiler

典型SSH系统的"家谱":

graph TD
    A[浏览器] --> B[Struts Action]
    B --> C[Service层]
    C --> D[DAO层]
    D --> E[(MySQL数据库)]
    C --> F[其他Service]
    F --> D
    C --> G[定时任务]
    G --> D
    
    style A fill:#f9f,stroke:#333
    style B fill:#ff9,stroke:#333
    style E fill:#9f9,stroke:#333

生活类比:这就像给你家画户型图,知道哪个房间是厨房,哪个是卧室,水电线路怎么走。

第二步:找出"病灶"(问题盘点)

🔧 技术维度分析

维度老系统现状问题指数比喻
启动速度5-10分钟⭐⭐⭐⭐⭐像老爷爷起床,要慢慢来
部署频率周/月级别⭐⭐⭐⭐⭐像坐绿皮火车,一天一班
扩展性无法水平扩展⭐⭐⭐⭐房子太小,没法加房间
技术栈JDK 6/7 + Tomcat 7⭐⭐⭐⭐⭐用着Windows XP的电脑
代码质量圈复杂度>50⭐⭐⭐⭐代码像迷宫
测试覆盖<20%⭐⭐⭐⭐⭐裸奔状态

📊 业务维度分析

// 典型的SSH代码 - 看着就头疼 😫
public class UserAction extends ActionSupport {
    private UserService userService; // Spring注入
    
    public String execute() {
        // 业务逻辑、数据验证、权限检查全混在一起
        HttpServletRequest request = ServletActionContext.getRequest();
        String userId = request.getParameter("userId");
        
        // 没有参数验证
        User user = userService.getUserById(userId);
        
        // 直接在Action里写业务逻辑 🤦
        if(user.getType() == 1) {
            // 一大堆if-else...
        }
        
        // Session操作
        request.getSession().setAttribute("user", user);
        
        return SUCCESS;
    }
}

第三步:量化评估(给系统打分)

健康度评估表

系统健康度评分卡
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📈 性能指标:              35/100 ❌
   - 响应时间:>3秒
   - 并发支持:<500 QPS
   - CPU使用率:峰值>80%

🏗️ 架构质量:              25/100 ❌
   - 模块化程度:低
   - 代码耦合度:高
   - 可扩展性:差

🔧 维护成本:              30/100 ❌
   - Bug修复周期:>3天
   - 新功能开发:困难
   - 文档完善度:无

🚀 技术先进性:            20/100 ❌
   - 技术栈年龄:>10年
   - 社区活跃度:低
   - 安全漏洞:多

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总分:27.5/100 - 濒危状态 🆘

🎯 战略规划:制定改造蓝图 {#战略规划}

策略一:绞杀者模式(Strangler Fig Pattern)🌳

什么是绞杀者模式?

在热带雨林中,有一种榕树叫"绞杀榕"。它的种子落在大树上,生根发芽,逐渐包围宿主,最终取而代之。

技术实现

graph LR
    A[用户请求] --> B{API网关}
    B -->|新功能| C[新系统<br/>Spring Boot]
    B -->|老功能| D[老系统<br/>SSH]
    C --> E[(新数据库)]
    D --> F[(老数据库)]
    
    style C fill:#9f9,stroke:#333
    style D fill:#f99,stroke:#333

实施步骤

第一阶段:建立"包围圈"

// 1. 先搭建API网关(使用Spring Cloud Gateway)
@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            // 新功能路由到新系统
            .route("new_user_service", r -> r
                .path("/api/v2/users/**")
                .uri("http://new-user-service:8080"))
            
            // 老功能继续走老系统
            .route("legacy_system", r -> r
                .path("/api/v1/**")
                .uri("http://legacy-ssh-system:8080"))
            
            .build();
    }
}

第二阶段:逐步"蚕食"

优先级排序(先吃软柿子):

  1. 无状态的查询接口(最简单)

    • 用户查询
    • 商品列表
    • 订单详情
  2. 独立的业务模块(相对简单)

    • 消息通知
    • 文件上传
    • 日志记录
  3. ⚠️ 核心业务逻辑(需谨慎)

    • 订单创建
    • 支付流程
    • 库存管理
  4. 🔥 强耦合模块(最后啃)

    • 分布式事务
    • 复杂工作流
    • 历史数据迁移

生活类比:就像吃披萨,先从边缘吃起,最后才吃中间有肉的部分!🍕

策略二:分层改造法(Layer by Layer)

架构演进路线图

阶段1:保留外壳,改造内核 🥚
┌─────────────────────┐
│   Struts (保留)      │
├─────────────────────┤
│ Service (改造)      │ ← 重构为Spring Boot Service
├─────────────────────┤
│ DAO (改造)          │ ← MyBatis Plus替代Hibernate
├─────────────────────┤
│   Database          │
└─────────────────────┘

阶段2:替换表示层 🎨
┌─────────────────────┐
│ RESTful API (新)    │ ← Spring Boot + Vue/React
├─────────────────────┤
│ Service Layer       │
├─────────────────────┤
│ DAO Layer           │
├─────────────────────┤
│   Database          │
└─────────────────────┘

阶段3:拆分微服务 🧩
┌──────┐  ┌──────┐  ┌──────┐
│用户服务│  │订单服务│  │商品服务│
└───┬──┘  └───┬──┘  └───┬──┘
    │         │         │
    └─────────┴─────────┘
          │
    ┌─────┴─────┐
    │  API网关   │
    └───────────┘

策略三:并行开发法(Parallel Development)

场景:业务压力大,不能停,又想快速见效

方案:新老系统并行,双写验证

/**
 * 并行开发:新老系统同时运行,对比结果
 */
@Service
public class DualSystemService {
    
    @Autowired
    private LegacyUserService legacyService; // 老系统
    
    @Autowired
    private NewUserService newService; // 新系统
    
    public UserDTO getUser(Long userId) {
        // 同时调用新老系统
        UserDTO oldResult = legacyService.getUser(userId);
        
        try {
            UserDTO newResult = newService.getUser(userId);
            
            // 对比结果,记录差异
            if (!Objects.equals(oldResult, newResult)) {
                log.warn("结果不一致!Old: {}, New: {}", 
                    oldResult, newResult);
                // 发送告警
                alertService.send("数据不一致告警");
            }
        } catch (Exception e) {
            log.error("新系统调用失败", e);
        }
        
        // 先返回老系统结果(保险)
        return oldResult;
    }
}

灰度发布策略

# 流量分配配置
traffic:
  legacy: 90%  # 老系统90%流量
  new: 10%     # 新系统10%流量(小白鼠)
  
# 逐步提升
week-1: 老90% vs 新10%
week-2: 老70% vs 新30%
week-3: 老50% vs 新50%
week-4: 老30% vs 新70%
week-5: 老10% vs 新90%
week-6: 完全切换到新系统 🎉

🛠️ 技术选型:挑选你的"武器库" {#技术选型}

核心框架选择

1️⃣ Spring Boot 3.x(必选,没有之一)

为什么选它?

  • ✅ 约定大于配置,开箱即用
  • ✅ 内嵌Tomcat,告别War包
  • ✅ 自动装配,省心省力
  • ✅ 生态丰富,轮子多

对比SSH

特性SSHSpring Boot
配置文件XML地狱 😈几行YAML 😊
启动时间5-10分钟 ⏰10-30秒 ⚡
部署方式War包+Tomcat 📦独立Jar 🚀
学习曲线陡峭 📈平缓 📉

代码对比

// ❌ SSH时代:配置文件100行起步
// applicationContext.xml
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/db"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</bean>

<bean id="sessionFactory" 
      class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>User.hbm.xml</value>
        </list>
    </property>
</bean>

// ✅ Spring Boot时代:几行配置搞定
// application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db
    username: root
    password: 123456
  jpa:
    hibernate:
      ddl-auto: update

2️⃣ 数据库持久层:MyBatis Plus vs JPA

选择指南

              MyBatis Plus               JPA/Hibernate
                   ↓                           ↓
         ┌──────────────────┐         ┌──────────────────┐
         │ 适合场景:        │         │ 适合场景:        │
         │ • 复杂SQL多      │         │ • 简单CRUD       │
         │ • 性能要求高     │         │ • 快速开发       │
         │ • 老项目迁移     │         │ • 标准化优先     │
         │ • 动态SQL        │         │ • 跨数据库       │
         └──────────────────┘         └──────────────────┘
                   ↓                           ↓
              学习成本:中                 学习成本:高
              性能:优秀                   性能:一般
              灵活性:极高                 灵活性:中

MyBatis Plus示例(推荐!):

// 实体类
@Data
@TableName("t_user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String username;
    private String email;
    private LocalDateTime createTime;
}

// Mapper接口(自带CRUD,无需写SQL)
public interface UserMapper extends BaseMapper<User> {
    // 就这么简单!!!
}

// Service层
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
    
    // 复杂查询:链式调用,优雅!
    public List<User> getActiveUsers() {
        return lambdaQuery()
            .eq(User::getStatus, 1)
            .gt(User::getCreateTime, LocalDateTime.now().minusDays(30))
            .orderByDesc(User::getCreateTime)
            .list();
    }
}

3️⃣ API网关:Spring Cloud Gateway

为什么需要网关?

生活类比:API网关就像小区门卫 👮

没有网关:
每个外卖员都直接上楼敲门 → 乱套了!

有了网关:
外卖员 → 门卫(验证身份)→ 分配到对应楼栋 → 业主
                ↓
            统一管理
            - 身份验证
            - 限流熔断
            - 日志记录
            - 路由转发

代码示例

@Configuration
public class GatewayConfig {
    
    @Bean
    public RouteLocator routes(RouteLocatorBuilder builder) {
        return builder.routes()
            // 用户服务路由
            .route("user-service", r -> r
                .path("/api/users/**")
                .filters(f -> f
                    .stripPrefix(1) // 去掉/api前缀
                    .addRequestHeader("X-Gateway", "true")
                    .circuitBreaker(c -> c.setName("userCB")) // 熔断
                    .retry(3)) // 重试3次
                .uri("lb://user-service")) // 负载均衡
            
            // 订单服务路由
            .route("order-service", r -> r
                .path("/api/orders/**")
                .filters(f -> f
                    .requestRateLimiter(r2 -> r2 // 限流
                        .setRateLimiter(redisRateLimiter())))
                .uri("lb://order-service"))
            
            .build();
    }
}

4️⃣ 服务注册与发现:Nacos

为什么选Nacos?

Nacos = Naming + Configuration + Service
        ↓
   注册中心 + 配置中心 + 服务管理

一个组件顶三个,省钱!💰

使用示例

# application.yml
spring:
  application:
    name: user-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        namespace: production
      config:
        server-addr: localhost:8848
        file-extension: yml
// 服务提供者
@RestController
@RequestMapping("/users")
public class UserController {
    // 自动注册到Nacos,其他服务可发现
}

// 服务消费者
@Service
public class OrderService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    public User getUser(Long userId) {
        // 通过服务名调用,自动负载均衡
        return restTemplate.getForObject(
            "http://user-service/users/" + userId,
            User.class
        );
    }
}

完整技术栈地图 🗺️

┌─────────────────────────────────────────────────┐
│              前端层 (Frontend)                    │
│  Vue 3 / React 18 + Ant Design / Element Plus   │
└───────────────────┬─────────────────────────────┘
                    │ HTTP/HTTPS
┌───────────────────┴─────────────────────────────┐
│            API网关 (Gateway)                      │
│     Spring Cloud Gateway + OAuth2                │
│     功能:路由、限流、熔断、认证                   │
└───────────────────┬─────────────────────────────┘
                    │
        ┌───────────┼───────────┬─────────────┐
        │           │           │             │
┌───────┴──┐  ┌────┴────┐  ┌──┴──────┐  ┌──┴──────┐
│ 用户服务  │  │ 订单服务 │  │ 商品服务 │  │ 支付服务 │
│  User    │  │ Order   │  │ Product │  │ Payment │
└────┬─────┘  └────┬────┘  └────┬────┘  └────┬────┘
     │             │             │             │
     │    Spring Boot 3 + MyBatis Plus        │
     │             │             │             │
┌────┴─────────────┴─────────────┴─────────────┴───┐
│         中间件层 (Middleware)                      │
│  • Nacos (注册中心+配置中心)                       │
│  • Redis (缓存+分布式锁)                          │
│  • RocketMQ/Kafka (消息队列)                      │
│  • Elasticsearch (搜索引擎)                       │
└─────────────────────┬─────────────────────────────┘
                      │
┌─────────────────────┴─────────────────────────────┐
│              数据层 (Data)                         │
│  • MySQL 8.0 (主数据库)                           │
│  • MongoDB (日志/文档)                            │
│  • MinIO/OSS (对象存储)                           │
└───────────────────────────────────────────────────┘

┌───────────────────────────────────────────────────┐
│          基础设施 (Infrastructure)                 │
│  • Docker + Kubernetes (容器编排)                 │
│  • Jenkins/GitLab CI (CI/CD)                      │
│  • Prometheus + Grafana (监控)                    │
│  • ELK Stack (日志分析)                           │
│  • SkyWalking (链路追踪)                          │
└───────────────────────────────────────────────────┘

⚔️ 实战演练:重构的N种姿势 {#实战演练}

场景一:用户登录模块改造(热身)

原SSH代码(地狱难度)

// UserAction.java - 什么都往里塞
public class UserAction extends ActionSupport {
    private UserService userService;
    private String username;
    private String password;
    
    public String login() {
        HttpServletRequest request = ServletActionContext.getRequest();
        HttpSession session = request.getSession();
        
        // 验证参数(没有统一验证)
        if(username == null || "".equals(username.trim())) {
            request.setAttribute("error", "用户名不能为空");
            return ERROR;
        }
        
        // 查询用户(SQL注入风险)
        User user = userService.findByUsername(username);
        
        // 业务逻辑混乱
        if(user != null) {
            if(user.getPassword().equals(password)) { // 明文密码!!!
                session.setAttribute("currentUser", user);
                
                // 日志记录(格式不统一)
                System.out.println("用户登录:" + username);
                
                return SUCCESS;
            }
        }
        
        request.setAttribute("error", "用户名或密码错误");
        return ERROR;
    }
}

// UserService.java
public class UserServiceImpl {
    private UserDAO userDAO;
    
    public User findByUsername(String username) {
        // HQL查询
        String hql = "from User where username = '" + username + "'"; 
        // SQL注入漏洞!!!
        return (User)userDAO.getSession()
            .createQuery(hql)
            .uniqueResult();
    }
}

新Spring Boot代码(天堂体验)

// 1. 实体类(使用Lombok简化代码)
@Data
@TableName("t_user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20")
    private String username;
    
    private String password; // 存储BCrypt加密后的密码
    
    private Integer status; // 0-禁用 1-正常
    
    private LocalDateTime lastLoginTime;
}

// 2. DTO(数据传输对象)
@Data
public class LoginRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    private String password;
}

@Data
public class LoginResponse {
    private String token;
    private UserDTO userInfo;
}

// 3. Controller层(清爽)
@RestController
@RequestMapping("/api/auth")
@Slf4j
public class AuthController {
    
    @Autowired
    private AuthService authService;
    
    @PostMapping("/login")
    public Result<LoginResponse> login(
            @Valid @RequestBody LoginRequest request) { // 自动参数验证
        
        log.info("用户登录请求:{}", request.getUsername());
        
        LoginResponse response = authService.login(request);
        
        return Result.success(response);
    }
}

// 4. Service层(业务逻辑)
@Service
@Slf4j
public class AuthService {
    
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private PasswordEncoder passwordEncoder; // BCrypt加密
    
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    public LoginResponse login(LoginRequest request) {
        // 1. 查询用户(防SQL注入)
        User user = userMapper.selectOne(
            new LambdaQueryWrapper<User>()
                .eq(User::getUsername, request.getUsername())
        );
        
        // 2. 验证用户
        if (user == null) {
            throw new BusinessException("用户不存在");
        }
        
        if (user.getStatus() == 0) {
            throw new BusinessException("用户已被禁用");
        }
        
        // 3. 验证密码(BCrypt自动防暴力破解)
        if (!passwordEncoder.matches(
                request.getPassword(), user.getPassword())) {
            
            // 记录失败次数(防暴力破解)
            String key = "login:fail:" + user.getId();
            Long failCount = redisTemplate.opsForValue()
                .increment(key, 1);
            redisTemplate.expire(key, 15, TimeUnit.MINUTES);
            
            if (failCount > 5) {
                throw new BusinessException("失败次数过多,请15分钟后再试");
            }
            
            throw new BusinessException("密码错误");
        }
        
        // 4. 生成Token
        String token = jwtTokenUtil.generateToken(user.getId());
        
        // 5. 缓存用户信息(减少数据库查询)
        redisTemplate.opsForValue().set(
            "user:token:" + token,
            user,
            7, TimeUnit.DAYS
        );
        
        // 6. 更新最后登录时间
        userMapper.update(null,
            new LambdaUpdateWrapper<User>()
                .eq(User::getId, user.getId())
                .set(User::getLastLoginTime, LocalDateTime.now())
        );
        
        // 7. 记录日志(结构化日志)
        log.info("用户登录成功:userId={}, username={}, ip={}",
            user.getId(), user.getUsername(), 
            RequestUtil.getClientIp());
        
        // 8. 发送登录通知(异步)
        eventPublisher.publishEvent(
            new LoginSuccessEvent(user.getId()));
        
        // 9. 返回结果
        LoginResponse response = new LoginResponse();
        response.setToken(token);
        response.setUserInfo(BeanUtil.copyProperties(
            user, UserDTO.class));
        
        return response;
    }
}

对比总结

维度SSH版本Spring Boot版本
代码行数~50行~80行(但功能多10倍)
参数验证手写注解自动验证 ✅
SQL注入有风险 ❌自动防护 ✅
密码安全明文 ❌BCrypt加密 ✅
防暴力破解无 ❌Redis计数器 ✅
日志规范println ❌结构化日志 ✅
异常处理返回字符串统一异常 ✅
可测试性优秀 ✅

场景二:订单创建(困难模式)

挑战点

  1. 涉及多个表(订单、订单详情、库存、积分)
  2. 需要事务一致性
  3. 性能要求高

老系统问题

// 全局事务,锁表时间长
@Transactional
public void createOrder(Order order) {
    // 1. 保存订单
    orderDAO.save(order);
    
    // 2. 保存订单详情
    for(OrderItem item : order.getItems()) {
        orderItemDAO.save(item);
    }
    
    // 3. 扣减库存(锁表!)
    for(OrderItem item : order.getItems()) {
        Product product = productDAO.findById(item.getProductId());
        product.setStock(product.getStock() - item.getQuantity());
        productDAO.update(product); // 全表锁,性能杀手!
    }
    
    // 4. 增加用户积分
    User user = userDAO.findById(order.getUserId());
    user.setPoints(user.getPoints() + order.getAmount() / 10);
    userDAO.update(user);
    
    // 任何一步失败,全部回滚 - 导致订单失败率高
}

新系统方案(微服务+异步)

// 订单服务
@Service
@Slf4j
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 创建订单(同步处理核心流程)
     */
    @Transactional(rollbackFor = Exception.class)
    public OrderDTO createOrder(CreateOrderRequest request) {
        // 1. 参数验证
        validateOrderRequest(request);
        
        // 2. 检查库存(调用商品服务)
        checkStock(request.getItems());
        
        // 3. 创建订单(只操作订单表)
        Order order = buildOrder(request);
        orderMapper.insert(order);
        
        // 4. 保存订单详情
        saveBatchOrderItems(order.getId(), request.getItems());
        
        // 5. 发送消息(异步处理)
        OrderCreatedEvent event = new OrderCreatedEvent();
        event.setOrderId(order.getId());
        event.setUserId(request.getUserId());
        event.setItems(request.getItems());
        event.setAmount(order.getTotalAmount());
        
        // 发到消息队列,解耦!
        rocketMQTemplate.syncSend(
            "order-created-topic",
            MessageBuilder.withPayload(event).build()
        );
        
        log.info("订单创建成功:orderId={}", order.getId());
        
        return convert(order);
    }
    
    /**
     * 调用商品服务检查库存
     */
    private void checkStock(List<OrderItemRequest> items) {
        // 使用Feign调用商品服务
        StockCheckRequest stockRequest = new StockCheckRequest();
        stockRequest.setItems(items.stream()
            .collect(Collectors.toMap(
                OrderItemRequest::getProductId,
                OrderItemRequest::getQuantity
            )));
        
        Result<Boolean> result = productFeignClient
            .checkStock(stockRequest);
        
        if (!result.isSuccess() || !result.getData()) {
            throw new BusinessException("库存不足");
        }
    }
}

// 库存服务(监听消息,异步扣减)
@Component
@RocketMQMessageListener(
    topic = "order-created-topic",
    consumerGroup = "stock-consumer-group"
)
public class StockDeductListener 
        implements RocketMQListener<OrderCreatedEvent> {
    
    @Autowired
    private StockService stockService;
    
    @Override
    public void onMessage(OrderCreatedEvent event) {
        try {
            // 扣减库存(使用乐观锁)
            for (OrderItem item : event.getItems()) {
                boolean success = stockService.deduct(
                    item.getProductId(),
                    item.getQuantity()
                );
                
                if (!success) {
                    // 库存不足,发送补偿消息
                    sendCompensateMessage(event);
                    return;
                }
            }
            
            log.info("库存扣减成功:orderId={}", event.getOrderId());
            
        } catch (Exception e) {
            log.error("库存扣减失败", e);
            // 重试机制
            throw e;
        }
    }
}

// 积分服务(监听消息,异步增加积分)
@Component
@RocketMQMessageListener(
    topic = "order-created-topic",
    consumerGroup = "points-consumer-group"
)
public class PointsAddListener 
        implements RocketMQListener<OrderCreatedEvent> {
    
    @Autowired
    private PointsService pointsService;
    
    @Override
    public void onMessage(OrderCreatedEvent event) {
        // 计算积分
        int points = event.getAmount().intValue() / 10;
        
        // 增加积分
        pointsService.addPoints(event.getUserId(), points);
        
        log.info("积分增加成功:userId={}, points={}",
            event.getUserId(), points);
    }
}

// 库存扣减(乐观锁实现)
@Service
public class StockService {
    
    @Autowired
    private ProductMapper productMapper;
    
    /**
     * 乐观锁扣减库存
     */
    public boolean deduct(Long productId, Integer quantity) {
        int rows = productMapper.deductStock(productId, quantity);
        return rows > 0;
    }
}

// Mapper
@Mapper
public interface ProductMapper extends BaseMapper<Product> {
    
    /**
     * 乐观锁扣减库存
     * UPDATE product 
     * SET stock = stock - #{quantity}, version = version + 1
     * WHERE id = #{productId} 
     *   AND stock >= #{quantity}
     *   AND version = #{version}
     */
    @Update("UPDATE t_product SET " +
            "stock = stock - #{quantity}, " +
            "version = version + 1 " +
            "WHERE id = #{productId} " +
            "AND stock >= #{quantity}")
    int deductStock(@Param("productId") Long productId,
                    @Param("quantity") Integer quantity);
}

架构对比

老系统(同步强一致):
┌──────────────────────────────────────┐
│        创建订单(10秒)               │
│  ├─ 保存订单 (0.1s)                  │
│  ├─ 保存详情 (0.2s)                  │
│  ├─ 扣减库存 (5s) ← 锁表,慢!        │
│  ├─ 增加积分 (0.5s)                  │
│  └─ 发送通知 (4s) ← HTTP调用,慢!    │
└──────────────────────────────────────┘
     任何一步失败,全部回滚 ❌

新系统(异步最终一致):
┌──────────────────────┐
│  创建订单(0.3秒)    │  ← 只保存订单
│  ├─ 保存订单 (0.1s)  │
│  ├─ 保存详情 (0.2s)  │
│  └─ 发送消息 (0.01s) │  ← 发到MQ就返回
└──────────────────────┘
           │
           ├──→ 库存服务(异步扣减)
           ├──→ 积分服务(异步增加)
           └──→ 通知服务(异步发送)
           
性能提升:33倍 🚀
用户体验:秒级响应 ✅

场景三:分布式事务(地狱模式)

问题:订单支付后,需要:

  1. 更新订单状态
  2. 扣减库存
  3. 增加积分
  4. 发送通知

如何保证一致性?

方案一:Seata AT模式(推荐)

// 引入依赖
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
</dependency>

// 订单服务
@Service
public class OrderService {
    
    @Autowired
    private ProductFeignClient productClient;
    
    @Autowired
    private PointsFeignClient pointsClient;
    
    /**
     * @GlobalTransactional 开启分布式事务
     * Seata会自动协调各个服务的事务提交/回滚
     */
    @GlobalTransactional(
        name = "create-order",
        rollbackFor = Exception.class
    )
    public void createOrderWithDistributedTransaction(
            CreateOrderRequest request) {
        
        // 1. 创建订单(本地事务)
        Order order = createLocalOrder(request);
        
        // 2. 扣减库存(远程调用,也在分布式事务中)
        Result<Void> stockResult = productClient.deductStock(
            order.getId(),
            request.getItems()
        );
        
        if (!stockResult.isSuccess()) {
            // 抛异常,Seata自动回滚所有服务的事务
            throw new BusinessException("库存扣减失败");
        }
        
        // 3. 增加积分(远程调用)
        Result<Void> pointsResult = pointsClient.addPoints(
            order.getUserId(),
            order.getTotalAmount() / 10
        );
        
        if (!pointsResult.isSuccess()) {
            // 抛异常,自动回滚
            throw new BusinessException("积分增加失败");
        }
        
        // 全部成功,Seata自动提交所有事务 ✅
    }
}

方案二:可靠消息最终一致性(推荐)

/**
 * 本地消息表方案
 */
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    @Transactional(rollbackFor = Exception.class)
    public void createOrder(CreateOrderRequest request) {
        // 1. 创建订单
        Order order = buildOrder(request);
        orderMapper.insert(order);
        
        // 2. 保存本地消息(同一个事务)
        LocalMessage message = new LocalMessage();
        message.setOrderId(order.getId());
        message.setContent(JSON.toJSONString(order));
        message.setStatus(0); // 待发送
        messageMapper.insert(message);
        
        // 本地事务提交
    }
    
    /**
     * 定时任务:扫描本地消息表,发送消息
     */
    @Scheduled(fixedDelay = 5000)
    public void scanLocalMessages() {
        List<LocalMessage> messages = messageMapper.selectList(
            new LambdaQueryWrapper<LocalMessage>()
                .eq(LocalMessage::getStatus, 0)
                .lt(LocalMessage::getRetryCount, 3)
        );
        
        for (LocalMessage message : messages) {
            try {
                // 发送到MQ
                rocketMQTemplate.syncSend(
                    "order-created-topic",
                    message.getContent()
                );
                
                // 更新状态为已发送
                message.setStatus(1);
                messageMapper.updateById(message);
                
            } catch (Exception e) {
                // 发送失败,增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                messageMapper.updateById(message);
            }
        }
    }
}

// 库存服务(消费消息,保证幂等)
@Component
@RocketMQMessageListener(
    topic = "order-created-topic",
    consumerGroup = "stock-consumer"
)
public class StockListener implements RocketMQListener<String> {
    
    @Override
    public void onMessage(String message) {
        Order order = JSON.parseObject(message, Order.class);
        
        // 幂等性检查(防止重复消费)
        String key = "stock:deduct:" + order.getId();
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", 1, TimeUnit.DAYS);
        
        if (Boolean.FALSE.equals(success)) {
            log.info("重复消息,忽略:{}", order.getId());
            return;
        }
        
        // 扣减库存
        stockService.deduct(order.getItems());
    }
}

🛡️ 风险管控:别把系统搞炸了 {#风险管控}

风险一:新老系统数据不一致

问题场景

用户在老系统下单 → 库存在新系统扣减 → 数据不同步 💥

解决方案:双写+对账

@Service
public class ProductService {
    
    @Autowired
    private LegacyProductDAO legacyDAO; // 老系统DAO
    
    @Autowired
    private ProductMapper newMapper; // 新系统Mapper
    
    /**
     * 更新商品(双写)
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateProduct(Product product) {
        // 1. 写新系统
        try {
            newMapper.updateById(product);
        } catch (Exception e) {
            log.error("新系统写入失败", e);
            throw e; // 失败直接抛异常
        }
        
        // 2. 写老系统(降级处理)
        try {
            legacyDAO.update(convertToLegacy(product));
        } catch (Exception e) {
            // 记录失败,但不影响新系统
            log.error("老系统写入失败,记录到补偿队列", e);
            compensationService.record(product);
        }
    }
    
    /**
     * 定时对账任务
     */
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点
    public void reconciliation() {
        // 1. 查询新系统所有商品
        List<Product> newProducts = newMapper.selectList(null);
        
        // 2. 对比老系统数据
        for (Product newProduct : newProducts) {
            Product legacyProduct = legacyDAO.findById(
                newProduct.getId()
            );
            
            // 3. 发现不一致,记录并告警
            if (!isEqual(newProduct, legacyProduct)) {
                log.error("数据不一致!new={}, legacy={}",
                    newProduct, legacyProduct);
                
                // 发送告警
                alertService.send(
                    "数据不一致",
                    String.format("商品ID=%d数据不一致",
                        newProduct.getId())
                );
                
                // 以新系统为准,修复老系统数据
                legacyDAO.update(convertToLegacy(newProduct));
            }
        }
    }
}

风险二:性能下降

监控指标

/**
 * 自定义性能监控
 */
@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {
    
    @Around("@annotation(com.example.annotation.Monitor)")
    public Object monitor(ProceedingJoinPoint joinPoint) 
            throws Throwable {
        
        String methodName = joinPoint.getSignature().getName();
        long startTime = System.currentTimeMillis();
        
        Object result = null;
        try {
            result = joinPoint.proceed();
            return result;
            
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            
            // 记录到监控系统
            MetricsCollector.record(methodName, costTime);
            
            // 超过阈值告警
            if (costTime > 3000) {
                log.warn("方法执行超时!method={}, cost={}ms",
                    methodName, costTime);
                
                alertService.send(
                    "性能告警",
                    String.format("%s执行耗时%dms", 
                        methodName, costTime)
                );
            }
        }
    }
}

// 使用
@Service
public class OrderService {
    
    @Monitor // 自动监控性能
    public OrderDTO createOrder(CreateOrderRequest request) {
        // ...
    }
}

风险三:新系统Bug导致线上事故

方案:灰度发布+快速回滚

# Kubernetes灰度发布配置
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
    - port: 8080

---
# 老版本 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-v1
spec:
  replicas: 9  # 90%流量
  selector:
    matchLabels:
      app: user-service
      version: v1
  template:
    metadata:
      labels:
        app: user-service
        version: v1
    spec:
      containers:
      - name: user-service
        image: user-service:v1.0.0

---
# 新版本 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-v2
spec:
  replicas: 1  # 10%流量(小白鼠)
  selector:
    matchLabels:
      app: user-service
      version: v2
  template:
    metadata:
      labels:
        app: user-service
        version: v2
    spec:
      containers:
      - name: user-service
        image: user-service:v2.0.0

快速回滚脚本

#!/bin/bash
# rollback.sh - 一键回滚脚本

echo "开始回滚到v1版本..."

# 1. 缩容新版本到0
kubectl scale deployment user-service-v2 --replicas=0

# 2. 扩容老版本
kubectl scale deployment user-service-v1 --replicas=10

# 3. 验证
echo "等待Pod启动..."
sleep 10
kubectl get pods -l app=user-service

# 4. 健康检查
curl http://user-service:8080/actuator/health

echo "回滚完成!"

风险四:数据迁移失败

方案:分批迁移+校验

/**
 * 数据迁移服务
 */
@Service
@Slf4j
public class DataMigrationService {
    
    @Autowired
    private LegacyUserDAO legacyDAO;
    
    @Autowired
    private UserMapper newMapper;
    
    /**
     * 分批迁移用户数据
     */
    public void migrateUsers() {
        int pageSize = 1000;
        int pageNum = 1;
        int totalMigrated = 0;
        int totalFailed = 0;
        
        while (true) {
            // 1. 分页查询老数据
            List<LegacyUser> legacyUsers = legacyDAO.findByPage(
                pageNum, pageSize
            );
            
            if (legacyUsers.isEmpty()) {
                break; // 迁移完成
            }
            
            // 2. 转换并插入新系统
            for (LegacyUser legacyUser : legacyUsers) {
                try {
                    // 数据转换
                    User newUser = convertUser(legacyUser);
                    
                    // 插入新系统
                    newMapper.insert(newUser);
                    
                    // 标记为已迁移
                    legacyDAO.markAsMigrated(legacyUser.getId());
                    
                    totalMigrated++;
                    
                } catch (Exception e) {
                    log.error("用户迁移失败:id={}", 
                        legacyUser.getId(), e);
                    
                    // 记录失败原因
                    migrationErrorMapper.insert(
                        new MigrationError(
                            legacyUser.getId(),
                            e.getMessage()
                        )
                    );
                    
                    totalFailed++;
                }
            }
            
            log.info("第{}页迁移完成,成功{},失败{}",
                pageNum, totalMigrated, totalFailed);
            
            pageNum++;
            
            // 3. 休息一下,别把数据库搞挂了
            Thread.sleep(1000);
        }
        
        log.info("数据迁移完成!总成功:{},总失败:{}",
            totalMigrated, totalFailed);
    }
    
    /**
     * 数据校验
     */
    public void validateMigration() {
        log.info("开始数据校验...");
        
        // 1. 统计新老系统数据量
        long legacyCount = legacyDAO.count();
        long newCount = newMapper.selectCount(null);
        
        log.info("老系统用户数:{},新系统用户数:{}",
            legacyCount, newCount);
        
        // 2. 抽样对比
        List<Long> sampleIds = legacyDAO.randomSampleIds(100);
        
        for (Long userId : sampleIds) {
            LegacyUser legacyUser = legacyDAO.findById(userId);
            User newUser = newMapper.selectById(userId);
            
            if (!isEqual(legacyUser, newUser)) {
                log.error("数据不一致!userId={}", userId);
            }
        }
        
        log.info("数据校验完成!");
    }
}

🚀 持续优化:让系统永葆青春 {#持续优化}

建立完善的监控体系

监控金字塔

            ┌──────────────┐
            │  业务监控     │ ← 订单量、支付成功率
            ├──────────────┤
            │  应用监控     │ ← 接口响应时间、错误率
            ├──────────────┤
            │  中间件监控   │ ← Redis、MySQL、MQ
            ├──────────────┤
            │  基础设施监控 │ ← CPU、内存、磁盘、网络
            └──────────────┘

Prometheus + Grafana配置

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets:
          - 'user-service:8080'
          - 'order-service:8081'
          - 'product-service:8082'
// Spring Boot应用暴露指标
@Configuration
public class MetricsConfig {
    
    @Bean
    public TimedAspect timedAspect(MeterRegistry registry) {
        return new TimedAspect(registry);
    }
}

@RestController
public class OrderController {
    
    // 自动记录接口响应时间
    @Timed(value = "api.order.create", 
           description = "创建订单接口")
    @PostMapping("/orders")
    public Result<OrderDTO> createOrder(
            @RequestBody CreateOrderRequest request) {
        // ...
    }
}

建立CI/CD流水线

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

# 编译阶段
build:
  stage: build
  script:
    - mvn clean package -DskipTests
  artifacts:
    paths:
      - target/*.jar

# 测试阶段
test:
  stage: test
  script:
    - mvn test
    - mvn sonar:sonar  # 代码质量检查
  coverage: '/Total.*?([0-9]{1,3})%/'

# 部署到测试环境
deploy-test:
  stage: deploy
  script:
    - docker build -t user-service:test .
    - kubectl apply -f k8s/test/
  environment:
    name: test
  only:
    - develop

# 部署到生产环境
deploy-prod:
  stage: deploy
  script:
    - docker build -t user-service:$CI_COMMIT_TAG .
    - kubectl apply -f k8s/prod/
  environment:
    name: production
  only:
    - tags
  when: manual  # 手动触发

持续技术学习

学习路线图

第一季度:微服务基础
├─ Spring Boot深入
├─ Spring Cloud组件
└─ Docker容器化

第二季度:分布式系统
├─ 分布式事务(Seata)
├─ 分布式锁(Redis/Zookeeper)
└─ 消息队列(RocketMQ/Kafka)

第三季度:性能优化
├─ JVM调优
├─ MySQL优化
└─ 缓存策略

第四季度:DevOps
├─ Kubernetes
├─ CI/CD
└─ 监控告警

🎉 总结:重构是一场马拉松

关键要点回顾

  1. 评估现状 🔍

    • 全面分析,量化评估
    • 不要盲目开干
  2. 制定策略 🎯

    • 选对方法(绞杀者/分层/并行)
    • 循序渐进,不要激进
  3. 技术选型 🛠️

    • 选择成熟稳定的技术栈
    • 考虑团队能力和学习成本
  4. 风险管控 🛡️

    • 灰度发布,随时回滚
    • 监控告警,快速响应
  5. 持续优化 🚀

    • 建立监控体系
    • 完善CI/CD流程
    • 技术债务定期清理

时间规划参考

小型系统(<10万行代码)

  • 准备期:1-2个月
  • 重构期:3-6个月
  • 稳定期:2-3个月
  • 总计:6-11个月

中型系统(10-50万行代码)

  • 准备期:2-3个月
  • 重构期:6-12个月
  • 稳定期:3-6个月
  • 总计:11-21个月

大型系统(>50万行代码)

  • 准备期:3-6个月
  • 重构期:12-24个月
  • 稳定期:6-12个月
  • 总计:21-42个月

心态建设

重构是一场马拉松,不是百米冲刺!

           压力曲线
    高 │         ╱╲
       │        ╱  ╲
       │       ╱    ╲___
    低 │______╱         ╲____
       └─────────────────────→
        启动期 高峰期 稳定期
        
    ☕ 保持耐心
    🤝 团队协作  
    📚 持续学习
    💪 不要放弃

最后的忠告

  1. 不要想着一次重写所有代码

    • 大爆炸式重构,成功率<10%
    • 渐进式重构,成功率>80%
  2. 不要忽视业务需求

    • 重构不是目的,业务才是
    • 边重构边迭代
  3. 不要单打独斗

    • 拉上团队一起
    • 建立Code Review机制
  4. 不要追求完美

    • 80分的方案能落地,胜过100分的PPT
    • 先跑起来,再优化
  5. 不要忘记文档

    • 架构文档
    • API文档
    • 部署文档
    • 运维手册

📚 参考资料

经典书籍

  1. 《重构:改善既有代码的设计》- Martin Fowler
  2. 《微服务设计》- Sam Newman
  3. 《领域驱动设计》- Eric Evans
  4. 《凤凰项目》- Gene Kim

在线资源

社区

  • Spring中国社区
  • 掘金技术社区
  • InfoQ
  • GitHub优秀开源项目

🎊 结语

从"拖拉机"到"特斯拉"的改造之旅虽然漫长,但当你看到系统焕然一新的那一刻,所有的努力都是值得的!

记住:没有完美的系统,只有持续改进的系统!

祝你重构顺利,升职加薪!🎉🎉🎉


作者按:本文基于我10年的Java开发和架构经验总结而成。如果对你有帮助,请给个赞👍!有问题欢迎在评论区交流~

版本:v1.0
最后更新:2025-10-20
适用场景:中大型单体系统现代化改造
难度等级:⭐⭐⭐⭐ (进阶)


#系统重构 #微服务 #SpringBoot #技术选型 #架构升级