Yujun's Blog
Microservice(一):C端应用架构从单体到分布式实践
微服务初识:C端应用架构从单体到分布式实践
以营销服务为例,在初期开发阶段,我们可能会倾向于将其视为一个简单的单体项目,因为它似乎只关注于“活动”这一特定领域。然而,当我们将视角提升至整个C端产品生态,例如电商、外卖、或短视频平台时,对其定位的理解便会发生根本性转变。
在这些高并发、高用户活跃度的C端环境中,一个“营销平台”的复杂性和战略重要性,使其绝非一个简单的单体应用所能胜任。 相反,它将迅速演变为一个承载着核心业务逻辑(如用户活动管理、优惠券发放、促销策略动态调整、积分体系运营等)的独立微服务。
那么,我们如何将这个原本被视为单体的营销功能,重塑为一个独立的微服务,使其能够高效且无缝地与电商、外卖等核心业务应用(其他微服务)进行通信呢?
本文将依托Nacos
作为服务的统一注册与发现机制,并采用Dubbo
这一高性能RPC框架,实现服务间之间的远程调用。
Nacos安装
好的,既然我们要实践微服务,那么搭建一个稳固的运行环境是第一步。为了简化这个过程,我们使用 Docker Compose。
version: '3.8' # Docker Compose 文件版本,推荐使用3.8 networks: my-network: # 定义一个内部网络,让服务之间可以互相通信 driver: bridge services: # 数据库服务:Nacos 将其配置数据持久化到这里 💾 mysql: image: mysql:8.0.32 # 使用MySQL 8.0.32版本 container_name: mysql command: --default-authentication-plugin=mysql_native_password # 兼容旧客户端连接 restart: always # 容器退出后总是重启 environment: TZ: Asia/Shanghai # 设置时区,避免时间问题 MYSQL_ROOT_PASSWORD: 123456 # MySQL的root用户密码,请注意生产环境需更复杂 ports: - "13306:3306" # 将容器内部的3306端口映射到主机的13306端口,避免与本地其他MySQL冲突 volumes: # 挂载卷,用于数据持久化和初始化SQL脚本 # ./mysql/sql 目录下可以放置Nacos的初始化SQL文件,如 nacos-mysql.sql - ./mysql/data:/var/lib/mysql # 持久化MySQL数据 - ./mysql/log:/var/log/mysql # 持久化MySQL日志 - ./mysql/conf:/etc/mysql/conf.d # MySQL自定义配置 - ./mysql/sql:/docker-entrypoint-initdb.d # MySQL初始化SQL脚本目录 healthcheck: # 健康检查,确保MySQL服务完全启动并可用 test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] interval: 5s timeout: 10s retries: 10 start_period: 15s # 给MySQL充足的启动时间 networks: - my-network # 归属于自定义网络 # 注册中心与配置中心:微服务架构的“司令部” # 官方 GitHub 参考:https://github.com/nacos-group/nacos-docker # 访问地址:http://127.0.0.1:8848/nacos 【账号:nacos 密码:nacos】 nacos: image: nacos/nacos-server:v2.2.3-slim # 使用Nacos官方精简版镜像 container_name: nacos restart: always ports: - "8848:8848" # Nacos Web管理界面和API端口 - "9848:9848" # Nacos GRPC通信端口 environment: # Nacos 启动环境变量配置 - PREFER_HOST_MODE=hostname # 优先使用主机名模式 - MODE=standalone # 单机模式,适合开发测试,生产环境需集群部署 - SPRING_DATASOURCE_PLATFORM=mysql # 指定数据源为MySQL - MYSQL_SERVICE_HOST=mysql # MySQL服务的主机名,这里是Docker Compose服务名 - MYSQL_SERVICE_DB_NAME=nacos_config # Nacos使用的数据库名称 - MYSQL_SERVICE_PORT=3306 # MySQL服务端口 - MYSQL_SERVICE_USER=root # MySQL用户名 - MYSQL_SERVICE_PASSWORD=123456 # MySQL密码 # Nacos认证配置,生产环境务必修改为强密码和安全Token! - NACOS_AUTH_IDENTITY_KEY=2222 - NACOS_AUTH_IDENTITY_VALUE=2xxx - NACOS_AUTH_TOKEN=SecretKey012345678901234567890123456789012345678901234567890123456789 networks: - my-network # 归属于自定义网络 depends_on: # 依赖MySQL服务,确保MySQL启动并健康后Nacos才启动 mysql: condition: service_healthy
运行 docker-compose -f docker-compose-environment.yml up -d
命令。
稍等片刻,待容器完全启动后,可以通过访问 http://127.0.0.1:8848/nacos
来验证 Nacos 是否成功运行。
完成安装后,我们就拥有了一个功能完善的 Nacos 服务发现与配置中心,为接下来的 Dubbo 微服务实践打下了坚实的基础。🎉
Dubbo的使用
第一步:引入依赖
<!-- Nacos + Dubbo--> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>3.0.9</version> </dependency> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>3.0.9</version> </dependency> <dependency> <groupId>com.alibaba.nacos</groupId> <artifactId>nacos-client</artifactId> <version>2.1.0</version> </dependency>
第二步:写配置文件
dubbo: application: name: ${spring.application.name} version: 1.0 registry: id: nacos-registry address: nacos://192.168.1.108:8848 protocol: name: dubbo port: -1 scan: base-packages: cn.bugstack.trigger.api
配置的“为什么”往往比“是什么”更重要,理解了深层逻辑,即使忘了具体语法,也能快速找回思路。
- name:这是当前微服务在Dubbo和注册中心(如Nacos)中的唯一标识符。
- 在Dubbo Admin、Nacos Dashboard等管理界面上,这个名字是识别服务的关键,它也用于链路追踪、日志聚合等场景,帮助快速定位问题。
- version:定义当前应用所提供服务的版本号。
- 允许同一个服务(比如 UserService)存在多个版本(如 1.0 和 2.0)同时运行。这对于实现灰度发布(新版本先放给一小部分用户)和蓝绿部署(新旧版本并行切换)至关重要,避免“大爆炸”式升级。
- registry:注册中心的配置。
- 为注册中心配置定义一个唯一的ID。如果Dubbo应用需要连接多个注册中心(例如,一部分服务注册到Nacos,另一部分注册到ZooKeeper),这个ID可以明确区分不同的注册中心配置。
- 可以在服务级别指定使用哪个注册中心,例如:
@DubboService(registry = "nacos-registry")
指定使用名为 nacos-registry 的注册中心。 - 指定注册中心的具体连接地址。
nacos://
前缀明确指示了注册中心类型是Nacos。Dubbo会根据这个前缀加载对应的Nacos客户端实现。
- protocol:服务协议配置。指定服务暴露的RPC协议名称。
- dubbo 是Dubbo框架自带的、最高性能的私有二进制协议。Dubbo也支持其他协议,如 rest (基于HTTP)、grpc、thrift 等。可以根据具体场景选择合适的协议。
- ports指定Dubbo服务提供者对外暴露服务的端口号。
- 萨的scan:服务扫描配置,指定Dubbo需要扫描的Java包路径。
- Dubbo会在这个(或这些)包下,寻找所有被
@DubboService
(服务提供者)或@DubboReference
(服务消费者)注解的类/字段。 - 当Dubbo找到
@DubboService
时,它会将其自动注册到注册中心并暴露服务;当找到@DubboReference
时,它会从注册中心获取服务并注入代理对象。
- Dubbo会在这个(或这些)包下,寻找所有被
配置完 application.yml
,我们已经告诉Dubbo“我是谁”和“去哪里找注册中心”。接下来,我们需要明确地告诉Dubbo:“我的哪个业务功能应该被暴露为RPC服务?”以及“如何激活Dubbo在Spring Boot中的所有能力?” 这就是 @EnableDubbo 和 @DubboService 的职责所在。
// 通常位于 Spring Boot 的主应用类上 @SpringBootApplication @EnableDubbo // ⬅️ 就是它! public class DubboProviderApplication { public static void main(String[] args) { SpringApplication.run(DubboProviderApplication.class, args); } }
@DubboService(version = "1.0") // ⬅️ 标记这个类作为Dubbo服务提供者 public class RaffleActivityController implements IRaffleActivityService { // ... 业务逻辑实现 ... }
标记一个 Spring Bean,声明它是一个Dubbo 服务提供者,并指示Dubbo框架将其暴露出去,以便其他消费者可以远程调用。
以上就是 Dubbo 通过少量注解实现强大能力的精髓所在。
服务消费:引入API与注解式远程调用
好的,服务提供者已经就位,并在注册中心Nacos上“挂牌营业”了。接下来,就是服务消费者调用这些远程服务的时候了。
这是Dubbo微服务通信的最后一个核心环节,也是其“只配注解”哲学在调用端的体现。
现在,我们假设有一个“业务应用系统”(例如你图中提到的OpenAI应用、博客应用、点评应用),它需要调用营销系统中由 IRaffleActivityService
接口定义的功能。这个“业务应用系统”就是服务消费者。
引入 API 包:服务契约的桥梁 🌉
在消费者模块(dubbo-consumer
)的 pom.xml
中,首先需要引入服务提供者对外暴露的服务接口定义所在的包。
<!-- 在 dubbo-consumer 模块的 pom.xml 中 --> <dependency> <groupId>com.yourcompany.capp</groupId> <artifactId>dubbo-api</artifactId> <version>${project.version}</version> </dependency>
契约统一: 这是最核心的。dubbo-api
包中包含了 IRaffleActivityService
这样的接口定义以及任何相关的请求/响应DTO(数据传输对象)。服务提供者和消费者都必须依赖这个相同的API包,以确保双方对服务的方法签名、参数类型、返回值类型等都有一致的理解。
@DubboReference
:远程服务的“本地代理”注入 🔗
当消费者模块引入了API包后,就可以在其需要调用远程服务的地方,使用 @DubboReference
注解来“引用”这个服务了。
// 假设这是消费者模块中的一个Controller或Service package com.yourcompany.capp.consumer; import com.yourcompany.capp.api.IRaffleActivityService; // 引入API接口 import org.apache.dubbo.config.annotation.DubboReference; // Dubbo核心注解 import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ConsumerController { // 标记需要引用一个Dubbo服务 @DubboReference(version = "1.0") // 指定要引用的服务版本,确保与提供者一致 private IRaffleActivityService raffleActivityService; @GetMapping("/callRaffleService") public String callRemoteService() { // 直接像调用本地方法一样调用远程服务 String result = raffleActivityService.doSomethingOnRaffle("user123"); return "远程RaffleActivityService调用结果: " + result; } }
整个过程对开发者来说是完全透明的,你只看到本地方法调用的简单,却享受了分布式服务的强大。这就是Dubbo“只配注解”的核心价值所在。 🎉
Dubbo
和 Spring Cloud
作为微服务框架,它们只专注于解决分布式通信和基础设施的复杂性,我们核心业务逻辑依然保持纯粹,所以不要害怕微服务,其基础使用远比你想象中更为直接和简单。
然而,要真正理解它们“为何如此”运作的深层逻辑,并能在复杂场景下得心应手,则确实需要对分布式系统核心原理具备更深厚的知识储备。但这并不妨碍我们立即放手去做创造业务价值。
这一节,我们完成了微服务间调用从概念到初步实践的过程,对这些分散在网络中的这些服务如何协同工作,有了一个直观但感性的认识。
然而,仅仅实现服务间的互联互通,只是微服务的起点。真正确保C端系统在高并发、高可用场景下的稳健运行,以及应对各种突发状况,离不开一系列精密的微服务治理技术。
在后续的分享中,我们将把目光投向更高级别的挑战,深入探讨:
- 服务降级 (Service Degradation) 📉: 当系统压力过大或依赖服务不可用时,如何有策略地放弃非核心服务,保障核心功能可用,避免“雪崩效应”?
- 服务限流 (Service Limiting) 🚦: 如何限制对服务的访问速率,防止系统因流量过载而崩溃,保护核心资源?
- 服务熔断 (Circuit Breaking) 💥: 当某个依赖服务持续失败时,如何快速切断调用链路,防止故障扩散到整个系统,并适时自动恢复?