微服务架构核心概念业务示例
一、什么是微服务
概念
微服务是一种架构风格,将单体应用拆分为一组小型、独立部署的服务,每个服务围绕特定业务能力构建,通过轻量级通信机制(通常是 HTTP/消息队列)协作。
与单体架构对比
| 对比项 | 单体架构 | 微服务架构 |
|---|---|---|
| 部署单元 | 整个应用一起打包部署 | 每个服务独立部署 |
| 数据库 | 所有模块共享一个数据库 | 每个服务有自己的数据库 |
| 通信方式 | 方法调用(进程内) | HTTP/MQ(跨进程) |
| 扩展方式 | 整体扩容 | 按需扩容单个服务 |
| 故障影响 | 一个模块崩溃,整体不可用 | 一个服务故障,其他不受影响 |
| 团队协作 | 所有人改同一个代码仓库 | 各团队负责自己的服务 |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、服务间通信:OpenFeign
概念
OpenFeign 是 Spring Cloud 提供的声明式 HTTP 客户端。你只需要定义一个接口并加上注解,Feign 自动帮你完成 HTTP 请求的构造、发送和响应解析。
与手动 HTTP 调用的对比
手动 RestTemplate 方式(繁琐):
java
String url = "http://xxx-order/api/xxx/order/result/receive";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<OmsOperationDto> entity = new HttpEntity<>(dto, headers);
ResponseEntity<RestControllerResult> response =
restTemplate.postForEntity(url, entity, RestControllerResult.class);
Feign 声明式方式(简洁):
java
RestControllerResult<Boolean> result = xxxOrderFeign.noticeOms(dto);
通用代码示例
1. 定义 Feign 接口
java
/**
* 远程服务调用接口.
* 声明式定义:只需写接口 + 注解,无需实现类.
*/
@FeignClient(
value = "${service.name.user-service}/api", // 服务名(从配置读取)
fallbackFactory = UserFeignFallbackFactory.class // 降级处理
)
public interface UserFeign {
@GetMapping("/user/info/{userId}")
RestControllerResult<UserDto> getUserInfo(@PathVariable("userId") Integer userId);
@PostMapping("/user/batch-query")
RestControllerResult<List<UserDto>> batchQuery(@RequestBody List<Integer> userIds);
}
2. Feign 降级工厂(Fallback Factory)
java
/**
* Feign 降级工厂.
* 当远程服务不可用时执行降级逻辑,避免级联故障.
*/
@Component
public class UserFeignFallbackFactory implements FallbackFactory<UserFeign> {
@Override
public UserFeign create(Throwable cause) {
return new UserFeign() {
@Override
public RestControllerResult<UserDto> getUserInfo(Integer userId) {
log.warn("调用用户服务降级, userId={}, cause={}", userId, cause.getMessage());
return RestControllerResult.fail("用户服务暂不可用");
}
@Override
public RestControllerResult<List<UserDto>> batchQuery(List<Integer> userIds) {
log.warn("批量查询用户降级, cause={}", cause.getMessage());
return RestControllerResult.fail("用户服务暂不可用");
}
};
}
}
3. 业务代码中调用
java
@Service
public class OrderServiceImpl {
@Resource
private UserFeign userFeign;
public OrderDetailVo getOrderDetail(Integer orderId) {
Order order = orderRepository.findById(orderId);
// 像调用本地方法一样调用远程服务
RestControllerResult<UserDto> userResult = userFeign.getUserInfo(order.getUserId());
if (Boolean.TRUE.equals(userResult.getSuccess())) {
// 组装返回数据
OrderDetailVo vo = new OrderDetailVo();
vo.setOrder(order);
vo.setUser(userResult.getData());
return vo;
}
return null;
}
}
Feign 的核心配置
yaml
feign:
sentinel:
enabled: true # 开启熔断
client:
config:
default:
connectTimeout: 60000 # 连接超时 60s
readTimeout: 60000 # 读超时 60s
loggerLevel: basic # 日志级别
三、服务注册与发现
概念
微服务数量众多,每个服务可能有多个实例。服务注册与发现解决的问题是:调用方如何知道目标服务的真实地址?
┌──────────┐ 注册 ┌──────────────────┐ 发现 ┌──────────┐
│ 服务A │──────→│ 注册中心 │←──────│ 服务B │
│ 实例1 │ │ (Nacos/Eureka) │ │ │
│ 实例2 │ │ │ │ │
│ 实例3 │ │ 服务A → [实例列表]│ │ │
└──────────┘ └──────────────────┘ └──────────┘
工作流程
- 服务A 启动后向注册中心注册自己的地址
- 服务B 需要调用服务A 时,先从注册中心获取服务A 的实例列表
- 通过负载均衡策略选择一个实例发起调用
- 注册中心通过心跳检测剔除不健康实例
实现方式
请求链路:Feign → API Gateway → 目标服务,网关负责服务路由和负载均衡。
四、服务熔断与降级
概念
当远程服务出现故障(超时、异常),如果不做处理,调用方会一直等待,最终拖垮自身。熔断器就像电路保险丝,当故障达到阈值时"断开",直接走降级逻辑。
三种状态
┌──────────────────────────────────────────┐
│ │
▼ │
┌──────────┐ 故障率达标 ┌──────────┐ │
│ CLOSED │ ────────────→ │ OPEN │ │ 探测成功
│ (正常) │ │ (熔断) │ │
└──────────┘ └─────┬────┘ │
▲ │ 等待超时 │
│ ▼ │
│ ┌──────────┐ │
│ 探测失败 │HALF-OPEN │─────────┘
└────────────────── │ (半开) │
└──────────┘
- CLOSED:正常调用,统计失败率
- OPEN:直接走降级逻辑,不再调用远程服务
- HALF-OPEN:放行少量请求探测,成功则恢复,失败则继续熔断
通用代码示例
java
/**
* 带降级的远程调用示例.
* 降级逻辑保证主流程不受下游故障影响.
*/
@Service
@Slf4j
public class NotificationService {
@Resource
private NotificationFeign notificationFeign;
/**
* 发送通知(不影响主业务).
* 即使通知服务挂了,主业务继续执行.
*/
public void sendNotification(String message) {
try {
RestControllerResult<Boolean> result = notificationFeign.send(message);
if (!Boolean.TRUE.equals(result.getSuccess())) {
log.warn("发送通知失败: {}", result.getMessage());
}
} catch (Exception e) {
// 通知失败不影响主流程
log.warn("通知服务调用异常", e);
}
}
}
五、消息队列(MQ)
概念
消息队列是服务内部异步解耦的核心组件。生产者发送消息到队列,消费者从队列拉取消息处理,两者时间上解耦。
与事件中心的定位区别
┌──────────────────────────────────────────────────────────┐
│ 同一个服务/系统内部 │
│ │
│ 业务方法 ──MQ──→ 异步处理方法 │
│ (同一个代码仓库,同一个团队维护) │
│ │
│ 用途:解耦耗时操作、削峰填谷、保证顺序 │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ 跨系统/跨团队 │
│ │
│ a服务 ──事件中心──→ b服务 │
│ (不同代码仓库,不同团队维护) │
│ │
│ 用途:系统间通知、发布/订阅 │
└──────────────────────────────────────────────────────────┘
通用代码示例
生产者
java
/**
* MQ 消息生产者.
* 发送消息到指定队列.
*/
@Component
@Slf4j
public class OrderMqSender {
@JshRabbitProducer(
queue = "${mq.queue.order-process}",
exchange = "${mq.exchange.order}")
private JshRabbitTemplate rabbitTemplate;
public void sendOrderProcess(Integer orderId) {
if (orderId != null) {
rabbitTemplate.send(orderId);
log.info("发送订单处理消息, orderId={}", orderId);
}
}
}
消费者
java
/**
* MQ 消息消费者.
* 通过 @Profile 控制是否启用.
*/
@Profile("order-process-consumer") // 本地不想消费时可去掉此 profile
@Component
@Slf4j
public class OrderMqConsumer {
@Resource
private OrderProcessService orderProcessService;
@JshRabbitConsumer(queue = "${mq.queue.order-process}")
public void consume(Integer orderId) {
log.info("消费订单处理消息, orderId={}", orderId);
try {
orderProcessService.process(orderId);
} catch (Exception e) {
log.error("订单处理失败, orderId={}", orderId, e);
throw e; // 抛出异常触发 MQ 重试
}
}
}
MQ 的作用:
- 异步化:不阻塞主业务事务
- 重试:消费失败自动重试
- 削峰:高并发时消息排队处理
六、分布式事务与数据一致性
问题
微服务各自有独立数据库,一个业务操作可能跨多个服务,无法使用传统的数据库事务保证一致性。
方案:本地消息表(最终一致性)
┌─────────────────────────────────────────────────┐
│ 同一个数据库事务 │
│ │
│ ① 执行业务逻辑(更新发货单状态等) │
│ ② 插入消息记录(a_send_b_record_log) │
│ ③ 事务提交 │
└───────────────────────┬─────────────────────────┘
│
▼ 事务提交后
┌─────────────────────────────────────────────────┐
│ ④ 发送 MQ(携带 logId) │
│ ⑤ 消费者读取消息记录,推送事件中心 │
│ ⑥ 成功/失败更新消息状态 │
│ ⑦ 失败可重试(运维接口/定时任务) │
└─────────────────────────────────────────────────┘
通用代码示例
java
/**
* 本地消息表实现最终一致性.
*/
@Service
public class OrderService {
@Resource
private OrderRepository orderRepository;
@Resource
private MessageLogRepository messageLogRepository;
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDto dto) {
// ① 业务操作
Order order = new Order();
order.setOrderCode(dto.getOrderCode());
order.setStatus("CREATED");
orderRepository.save(order);
// ② 同一事务内写消息记录
MessageLog msgLog = new MessageLog();
msgLog.setBizKey(order.getOrderCode());
msgLog.setEventType("order.created");
msgLog.setPayload(JSON.toJSONString(order));
msgLog.setStatus(0);
messageLogRepository.save(msgLog);
// ③ 注册事务提交后的回调
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
mqSender.send(msgLog.getId());
}
}
);
}
}
为什么不用分布式事务(2PC/Seata)?
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 2PC/XA | 强一致 | 低(锁等待) | 高 | 金融转账 |
| Seata AT | 强一致 | 中 | 中 | 短事务、同步场景 |
| 本地消息表 | 最终一致 | 高 | 低 | 通知类、可接受短暂延迟 |
| TCC | 最终一致 | 高 | 高 | 库存扣减等 |
示例需求是"通知系统b",属于通知类场景,不需要b的即时响应,用本地消息表 + 最终一致性是最合适的方案。
七、API 网关
概念
API 网关是微服务的统一入口,所有外部请求先经过网关,由网关路由到具体服务。
┌─────────────────┐
外部请求 ──────────→│ API Gateway │
│ │
│ 路由规则: │
│ /a-order → A │──→ a 服务
│ /b → B │──→ b服务
│ /order → C │──→ 订单服务
└─────────────────┘
网关的职责
| 职责 | 说明 |
|---|---|
| 路由转发 | 根据 URL 前缀将请求转发到对应服务 |
| 负载均衡 | 同一服务多实例时分发请求 |
| 鉴权认证 | 统一校验 JWT Token |
| 限流熔断 | 保护后端服务不被打爆 |
| 协议转换 | HTTPS → HTTP,外部加密内部明文 |
八、配置中心
概念
微服务众多,每个服务有各自的配置(数据库、MQ、第三方地址等)。配置中心提供统一管理和动态刷新。
通过配置实现多环境切换
yaml
# 本地开发
eventcenter:
event:
route-event-to-oms:
url: https://xxx.net/.../send # 测试环境
# 生产环境(配置中心覆盖)
eventcenter:
event:
route-event-to-oms:
url: https://xxx/.../send # 生产环境
代码中通过 @Value 注入,无需修改代码即可切换环境:
java
@Value("${eventcenter.event.route-event-to-oms.url}")
private String routeEventToOmsEventUrl;
九、服务间调用的安全认证
JWT Token 传递
微服务间调用需要传递身份信息。项目通过 Feign 请求拦截器自动带上 JWT Token:
用户请求 → 网关(验证JWT)→ a服务 → Feign调用b服务(携带JWT)
通用示例:Feign 请求拦截器
java
/**
* Feign 请求拦截器.
* 自动将当前请求的 JWT Token 传递给下游服务.
*/
@Configuration
public class FeignInterceptorConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
// 从当前请求上下文获取 Token
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
String token = attributes.getRequest().getHeader("Authorization");
if (token != null) {
requestTemplate.header("Authorization", token);
}
}
};
}
}