设计模式与微服务架构:API网关中的外观、服务调用中的代理、配置中心中的观察者
微服务架构火了这么多年,我一直在思考一个问题:设计模式到底还有没有用?
说实话,刚接触微服务那会儿,我也觉得设计模式是"老古董"。直到我在一个大型电商平台重构中,被分布式系统的复杂性狠狠教育了一回。那时候我才意识到——设计模式不是过时了,而是换了个马甲,继续在微服务里发光发热。
今天咱们就聊聊三个最典型的场景:API网关里的外观模式、服务调用里的代理模式、配置中心里的观察者模式。说白了,就是看看这些经典模式在微服务里是怎么"转世投胎"的。
一、API网关中的外观模式(Facade Pattern)
先说说外观模式。这个模式的核心思想很简单:给复杂子系统提供一个统一的高层接口。你想想看,微服务架构里,客户端要调用多个服务才能完成一个业务操作,这不就是典型的"复杂子系统"吗?
核心要点:API网关就是微服务架构中的外观(Facade)。它封装了后端服务的调用细节,对外提供简洁的接口。
我在一个项目中遇到过这样的场景:前端要展示一个商品详情页,需要调用商品服务、库存服务、价格服务、评价服务……光接口调用就七八个。前端同学都快疯了,每次联调都要对着接口文档一个个调。
后来我们引入了API网关,把所有这些调用封装成一个接口。前端只需要调一次网关,网关内部去协调各个服务。这就是外观模式的典型应用。
// Java示例:API网关中的外观模式
public class ProductDetailFacade {
private ProductService productService;
private InventoryService inventoryService;
private PriceService priceService;
private ReviewService reviewService;
public ProductDetailDTO getProductDetail(String productId) {
// 封装多个服务调用
Product product = productService.getProduct(productId);
Inventory inventory = inventoryService.getInventory(productId);
Price price = priceService.getPrice(productId);
List<Review> reviews = reviewService.getReviews(productId);
// 组装结果
return ProductDetailDTO.builder()
.product(product)
.inventory(inventory)
.price(price)
.reviews(reviews)
.build();
}
}
// C++示例:API网关中的外观模式
class ProductDetailFacade {
private:
ProductService* productService;
InventoryService* inventoryService;
PriceService* priceService;
ReviewService* reviewService;
public:
ProductDetailDTO getProductDetail(const std::string& productId) {
Product product = productService->getProduct(productId);
Inventory inventory = inventoryService->getInventory(productId);
Price price = priceService->getPrice(productId);
std::vector<Review> reviews = reviewService->getReviews(productId);
return ProductDetailDTO::builder()
.setProduct(product)
.setInventory(inventory)
.setPrice(price)
.setReviews(reviews)
.build();
}
};
个人经验:外观模式在API网关中还有一个好处——可以统一做降级和熔断。我曾经在网关层给每个服务调用都加了Hystrix熔断器,一旦某个服务挂了,网关能返回降级数据,而不是让前端直接看到500错误。
二、服务调用中的代理模式(Proxy Pattern)
代理模式,说白了就是给目标对象提供一个代理,由代理控制对目标对象的访问。在微服务里,服务调用几乎离不开代理模式。
你想想看,服务A要调用服务B,总不能直接new一个对象吧?服务B可能在另一台机器上,甚至可能动态扩缩容。这时候就需要一个代理来帮我们处理网络通信、负载均衡、重试这些事。
核心要点:服务调用框架(如Feign、gRPC、Dubbo)本质上都是代理模式的实现。客户端调用的接口实际上是代理对象,代理负责把本地调用转换成远程调用。
我记得有一次排查线上问题,发现某个服务调用超时特别多。查了半天,原来是代理层没有配置重试机制。后来我们在代理层加了重试策略,超时问题大幅减少。嗯,这里要注意——重试要小心幂等性问题,不是所有接口都能随便重试的。
// Java示例:Feign中的代理模式
@FeignClient(name = "user-service", url = "${user.service.url}")
public interface UserServiceProxy {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/users")
User createUser(@RequestBody User user);
}
// 实际调用时,Feign会动态生成代理对象
@Autowired
private UserServiceProxy userServiceProxy;
public void doSomething() {
// 这里调用的实际上是代理对象
User user = userServiceProxy.getUser(123L);
}
// C++示例:gRPC中的代理模式
class UserServiceClient {
private:
std::unique_ptr<UserService::Stub> stub_;
public:
UserServiceClient(std::shared_ptr<Channel> channel)
: stub_(UserService::NewStub(channel)) {}
User getUser(int64_t id) {
GetUserRequest request;
request.set_id(id);
GetUserResponse response;
ClientContext context;
// stub就是代理对象,封装了网络通信细节
Status status = stub_->getUser(&context, request, &response);
if (status.ok()) {
return response.user();
} else {
throw std::runtime_error("Failed to get user");
}
}
};
避坑指南:我曾经在代理层犯过一个低级错误——没有配置连接超时和读取超时。结果某个下游服务慢查询导致连接池被占满,整个系统都卡住了。记住,代理层一定要配置合理的超时时间,建议连接超时500ms,读取超时3s。
三、配置中心中的观察者模式(Observer Pattern)
观察者模式,也叫发布-订阅模式。在微服务里,配置中心就是观察者模式的经典应用。
为什么需要观察者模式?你想想看,传统单体应用改个配置要重启。微服务动辄几十上百个实例,重启一遍得多久?所以我们需要一个机制:配置变了,所有服务实例能自动感知并更新。
核心要点:配置中心(如Nacos、Apollo、Consul)使用观察者模式实现配置的动态更新。服务实例作为观察者订阅配置变化,配置中心作为主题(Subject)推送变更。
我个人习惯用Nacos做配置中心。它的实现思路很清晰:每个服务启动时向配置中心注册监听器,配置发生变化时,配置中心会回调监听器,通知所有订阅者。
// Java示例:Nacos中的观察者模式
@Component
public class DynamicConfigListener implements Listener {
@Value("${timeout:5000}")
private int timeout;
@PostConstruct
public void init() {
// 订阅配置变化
ConfigService configService = NacosFactory.createConfigService(
"localhost:8848");
configService.addListener("application.yml", "DEFAULT_GROUP", this);
}
@Override
public void receiveConfigInfo(String configInfo) {
// 配置发生变化时,这个方法会被调用
System.out.println("配置已更新: " + configInfo);
// 重新加载配置
refreshConfig(configInfo);
}
private void refreshConfig(String configInfo) {
// 解析新的配置并更新到内存
Properties props = new Properties();
try {
props.load(new StringReader(configInfo));
this.timeout = Integer.parseInt(
props.getProperty("timeout", "5000"));
} catch (IOException e) {
log.error("配置解析失败", e);
}
}
}
// C++示例:配置中心观察者模式
class ConfigObserver : public nacos::ConfigListener {
private:
int timeout_ = 5000;
public:
void onConfigChange(const std::string& configInfo) override {
std::cout << "配置已更新: " << configInfo << std::endl;
refreshConfig(configInfo);
}
void subscribe() {
auto configService = nacos::NacosFactory::createConfigService(
"localhost:8848");
configService->addListener("application.yml", "DEFAULT_GROUP", this);
}
private:
void refreshConfig(const std::string& configInfo) {
// 解析配置并更新
// ...
}
};
个人经验:观察者模式在配置中心里有个容易被忽略的点——配置变更的"灰度发布"。我曾经直接全量推送了一个错误配置,导致所有服务都挂了。后来我们改成了分批推送,先推10%的实例观察一段时间,没问题再全量推。这个其实就是观察者模式里的"过滤通知"策略。
四、三种模式的对比与总结
说了这么多,咱们来对比一下这三种模式在微服务中的角色:
| 设计模式 | 微服务场景 | 核心作用 | 我的建议 |
|---|---|---|---|
| 外观模式 | API网关 | 封装复杂调用,提供统一接口 | 网关层一定要做降级和熔断 |
| 代理模式 | 服务调用 | 屏蔽网络通信细节,实现远程调用 | 配置合理的超时和重试策略 |
| 观察者模式 | 配置中心 | 实现配置的动态更新 | 注意灰度发布和配置校验 |
这三种模式在微服务里其实经常配合使用。比如API网关(外观模式)内部调用其他服务时,会通过代理模式(Feign/gRPC)来发起远程调用;而这些服务的配置又通过观察者模式从配置中心动态获取。
一句话总结:设计模式不是过时的理论,而是微服务架构的"骨架"。外观模式让网关更简洁,代理模式让调用更透明,观察者模式让配置更灵活。理解了这些,你写微服务代码时心里就有底了。
好了,今天就聊到这儿。下次咱们聊聊策略模式和模板方法模式在微服务中的实战应用,这两个模式在业务编排和流程控制里特别有用。
公众号:蓝海资料掘金营,微信deep3321