码农戏码

新生代农民工的自我修养

0%

SaaS部署标准化与定制化二-微服务整合

在《[[SaaS部署标准化与定制化]]一》中,落地实践中总体原则:构建时用 Maven 控制依赖是否打进包;运行时用 Spring 条件装配隔离缺失的类

看似很简单的措施,其实底层映射出不少经典设计思想:面向接口编程、SOLID中的DIP原则、DDD供应商模式

下面再谈另一种现实场景:微服务整合部署

标准化

在公有云上,我们部署的是标准化SaaS,按客户价值分类,这些服务客户是我们的 “现金奶牛“

这类客户应该覆盖大部分的用户群体,比如 GitHub 免费用户,或是 Salesforce 标准版这样的用户。对于这类客户,目标就是降低成运营成本。比如通过自动化脚本实现低成本运维。再比如,通过自助式服务来解决服务开通等问题。

这类客户没有特殊需求,都是产品标准功能。标准化能把高昂的研发成本摊薄到海量用户上,就可以降低边际成本,实现规模化盈利。

此时,我们需要的客户数量越多越好,而从架构角度,稳定性就是关键因素。至少保证“4个9的高可用性”。

为了主业务的高可用、高性能。我们通常使用拆分、隔离原则。
在DDD拆分域中,通常分为:核心域、支撑域、通用域。

对应组织层面,就会有各个业务团队,各个中台团队,基础架构团队。把一个服务,拆分成上百个微服务来支撑整个系统的良好运行。

定制化

标准客户是”现金奶牛“,定制化就是服务于高价值客户。

如果不是高价值客户,也没必要折腾定制化。高价值也并不单单是价格,也可能是市场标杆价值

标准化部署时,为了高可用,核心业务与非核心业务要隔离,每个服务都至少需要2个pod,
为了应对突发高峰,还有冗余资源

形象一点,米国入侵战争时,别看深入敌后的就一支海豹突击队,其实后面可能有一个航母编队在支撑。

这些成本通过规模化都能拉平。而在定制化部署时,云资源是客户提供的,每次部署前,都要提前申请,而且申请是可能会被拒绝的,并且在招标时,这也是一个中标重要因素。

在公有云,如果有100个微服务,至少需要200个pod,1个pod如果至少需要1核 CPU,那至少需要200核。

这时应对方案
1、压缩单个pod资源,比如低流量服务只配置0.5核
2、减少pod数量,比如只要50个微服务,把为了灵活性,运维性的服务减掉,像配置中心服务

而业务微服务,把多个微服务整合到一个微服务中,使用本地调用平替掉远程调用,达到减少微服务部署的要求

在 Spring Boot 中实现本地调用与 Feign 远程调用的无缝切换,核心思路是面向接口编程,并结合 Spring 的条件装配机制(Conditional)。通过读取配置文件中的属性,让 Spring 容器在启动时自动决定注入本地实现还是远程代理。

我们假设项目的 GroupId 为 com.example,整体分为三个核心模块:product-apiproduct-coreorder-service(消费方)。


1. 模块一:product-api(轻量级契约模块)

职责:仅存放对外暴露的接口定义、DTO/POJO 实体类、Feign 客户端接口。
依赖:极轻量,只依赖 Web 和 OpenFeign。

1
2
3
4
5
6
7
8
9
10
11

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 纯契约接口(不带 @FeignClient 注解)
public interface ProductApi {
@GetMapping("/product/{id}")
ProductInfo getProductById(@PathVariable("id") Integer id);
}

// 2. Feign 客户端(继承契约接口)
@FeignClient(name = "product-service")
public interface ProductFeignClient extends ProductApi {
// 无需重复写 @GetMapping,直接继承自 ProductApi
}

// 3. DTO 实体类
@Data
public class ProductInfo {
private Integer id;
private String name;
}

2. 模块二:product-core(核心业务模块)

职责:存放真正的业务逻辑、数据访问层。
依赖:依赖 product-api、MyBatis、数据库驱动等。

1
2
3
4
5
6
7
8
9
10
11
12
13

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>

</dependencies>

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 核心业务服务(实现契约接口)
@Service
public class ProductCoreServiceImpl implements ProductApi {
@Autowired
private ProductMapper productMapper;

@Override
public ProductInfo getProductById(Integer id) {
// 真正的业务逻辑只在这里写一次
return productMapper.selectById(id);
}
}

// 2. 数据访问层
@Mapper
public interface ProductMapper {
@Select("SELECT * FROM product WHERE id = #{id}")
ProductInfo selectById(Integer id);
}

3. 服务提供方:product-service(独立部署的微服务)

职责:作为独立服务运行,对外暴露 HTTP 接口。
依赖:依赖 product-core、Spring Boot Web。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. Controller 仅作为协议适配层,实现契约接口
@RestController
public class ProductController implements ProductApi {
@Autowired
private ProductCoreServiceImpl productCoreService;

@Override
public ProductInfo getProductById(Integer id) {
// 参数和路径已经在 ProductApi 中定义好了,这里直接委托给 Core
return productCoreService.getProductById(id);
}
}

// 2. 启动类(记得配置 MapperScan)
@SpringBootApplication
@MapperScan("com.example.product.core.mapper")
public class ProductServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ProductServiceApplication.class, args);
}
}

4. 消费方:order-service(核心亮点:无缝切换)

职责:调用商品服务,支持通过配置一键切换本地/远程调用。
依赖

  • 始终依赖 product-api(为了获取 ProductApi 接口和 ProductFeignClient)。
  • 按需依赖 product-core(如果配置为 local,则必须引入;如果仅用 remote,可不引入)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0.0</version>
</dependency>

<dependency>
<groupId>com.example</groupId>
<artifactId>product-core</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

核心代码(条件装配配置类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 消费方的配置类,负责决定注入哪个实现
@Configuration
@EnableFeignClients(basePackages = "com.example.product.api") // 扫描 Feign 客户端
public class ProductClientConfig {

// 1. 远程模式:注入 Feign 代理(Feign 本身也是 ProductApi 的实现类)
@Bean
@ConditionalOnProperty(name = "service.product.mode", havingValue = "remote")
public ProductApi remoteProductApi(ProductFeignClient feignClient) {
return feignClient;
}

// 2. 本地模式:直接注入 CoreService(跳过 Web 层,性能最高,零重复代码)
@Bean
@ConditionalOnProperty(name = "service.product.mode", havingValue = "local", matchIfMissing = true)
public ProductApi localProductApi(ProductCoreServiceImpl coreService) {
return coreService;
}
}

消费方业务代码(完全解耦)

1
2
3
4
5
6
7
8
9
10
11
// 订单服务,只认 ProductApi 接口
@Service
public class OrderService {
@Autowired
private ProductApi productApi; // Spring 会根据配置自动注入本地或远程实现

public void createOrder(Integer productId) {
ProductInfo product = productApi.getProductById(productId);
System.out.println("订单创建成功,商品名称:" + product.getName());
}
}

消费方配置文件

1
2
3
4
# application.yml
service:
product:
mode: local # 切换为 remote 时,Spring 会自动将 Feign 代理注入到 OrderService 中

这套架构是目前微服务演进中最标准的“SDK 抽取模式”,既保留了微服务的灵活性,又兼顾了单体的高性能。