导语: 在技术选型的十字路口,许多团队都面临这样的抉择:是用经典的单体架构快速上线,还是采用流行的微服务架构应对未来?今天,我们将通过一个完整的"图书馆管理系统"项目,从零开始构建两种架构的完整实现,在真实代码和运行结果中,揭示两种架构的本质差异、适用场景,并提供科学的决策框架。
1. 一个真实的架构决策困境
项目背景:某大学需要开发一个新的"智慧图书馆管理系统",核心功能包括:图书管理、读者管理、借阅管理、预约管理、罚款计算等。团队有6名开发人员,需要在4个月内上线第一版。
团队争议:
- 主张单体架构的成员:"我们人手有限,时间紧迫。单体架构开发速度快,部署简单,一个应用搞定所有功能,出了问题也容易排查。等系统稳定、用户量上来后再考虑拆分也不迟。"
- 主张微服务架构的成员:"现在不上微服务,将来肯定要重构。不如一步到位,每个功能独立部署,方便以后扩展。而且微服务是技术趋势,招聘也有优势。"
核心矛盾:这不仅仅是技术选择,更是对项目风险、团队能力、未来发展的综合判断。让我们通过实际构建来理解这两种选择的真实影响。
2. 单体架构与微服务架构的明确定义
单体架构 (Monolithic Architecture)
定义:将应用程序的所有功能模块(用户界面、业务逻辑、数据访问等)打包在一个单一的部署单元中,作为一个整体进行开发、测试、部署和扩展。
技术特征:
- 单一代码库
- 单一构建产物(如JAR、WAR文件)
- 共享同一个数据库
- 进程内通信
类比 :像一台多功能一体机,打印、复印、扫描功能都集成在一个设备里。功能齐全,但任何一个部件故障都可能影响整体使用。
微服务架构 (Microservices Architecture)
定义:将应用程序构建为一组小型服务的架构风格,每个服务运行在自己的进程中,服务之间通过轻量级机制(通常是HTTP RESTful API)进行通信,每个服务围绕特定业务能力构建,可独立部署。
技术特征:
- 多个独立的代码库
- 多个独立的部署单元
- 每个服务拥有自己的数据存储
- 进程间通信(网络调用)
类比 :像一支专业足球队,前锋、中场、后卫各司其职,通过传球(网络调用)协作。一个球员受伤,可以换人替补,不会导致整个球队停摆。
3. 深入剖析两种架构的全面差异
| 对比维度 | 单体架构 | 微服务架构 | 深度分析 |
|---|---|---|---|
| 开发效率 | 初期效率高,代码集中,调试方便 | 初期效率低,需搭建基础设施,调试复杂 | 单体适合快速验证业务,微服务适合长期演进 |
| 技术栈 | 必须统一技术栈,升级风险大 | 可混合技术栈,选择最优方案 | 微服务在技术多样性上有优势,但增加学习成本 |
| 可扩展性 | 整体扩展,资源利用率低 | 细粒度扩展,资源利用率高 | 微服务在应对不均匀流量时优势明显 |
| 可靠性 | 单点故障影响全局 | 故障可隔离,有熔断降级机制 | 微服务通过分布式设计提高系统韧性 |
| 数据管理 | 单一数据库,事务简单,关联查询方便 | 分布式数据库,事务复杂,关联查询困难 | 数据一致性是微服务的最大挑战 |
| 部署运维 | 部署简单,监控容易 | 部署复杂,需要完整监控体系 | 微服务对运维能力要求极高 |
| 测试复杂度 | 集成测试简单 | 需要契约测试、集成测试、端到端测试 | 微服务测试成本和难度大幅增加 |
| 团队协作 | 适合小团队集中开发 | 适合多团队并行开发 | 微服务架构反映组织架构(康威定律) |
| 系统演进 | 修改需要全量回归测试 | 服务可独立演进和部署 | 微服务支持持续交付和快速迭代 |
4. 技术选型与环境搭建
技术栈版本
bash
# 验证版本
Java: 17.0.10
Spring Boot: 3.2.5
Spring Cloud: 2023.0.1
Spring Cloud Alibaba: 2023.0.1.2
Maven: 3.9.6
MySQL: 8.0.35
Redis: 7.2.3
Nacos: 2.3.0
环境准备脚本
bash
# 创建项目目录
mkdir library-architecture-comparison
cd library-architecture-comparison
# 创建单体项目目录
mkdir library-monolith
cd library-monolith
# 创建微服务项目目录结构
cd ..
mkdir library-microservices
cd library-microservices
mkdir -p library-gateway library-book-service library-user-service library-borrow-service
5. (实战1)构建单体架构的图书馆管理系统
5.1 项目结构
library-monolith/
├── pom.xml
├── src/main/java/com/library/monolith/
│ ├── LibraryMonolithApplication.java
│ ├── controller/
│ │ ├── BookController.java
│ │ ├── UserController.java
│ │ └── BorrowController.java
│ ├── service/
│ ├── mapper/
│ └── entity/
└── src/main/resources/
├── application.yml
└── schema.sql
5.2 完整POM文件
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<groupId>com.library</groupId>
<artifactId>library-monolith</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 依赖版本 -->
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql-connector.version>8.3.0</mysql-connector.version>
<springdoc.version>2.5.0</springdoc.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
5.3 完整配置文件
yaml
server:
port: 8080
servlet:
context-path: /api
spring:
application:
name: library-monolith
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
# Redis配置
data:
redis:
host: localhost
port: 6379
database: 0
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
shutdown-timeout: 100ms
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# SpringDoc配置
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
tags-sorter: alpha
operations-sorter: alpha
# 日志配置
logging:
level:
com.library.monolith: debug
org.springframework.web: info
file:
name: logs/library-monolith.log
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# Actuator配置
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: always
metrics:
export:
prometheus:
enabled: true
5.4 实体类定义
java
package com.library.monolith.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("book")
public class Book {
@TableId(type = IdType.AUTO)
private Long id;
private String isbn;
private String title;
private String author;
private String publisher;
private LocalDateTime publishDate;
private Integer totalCopies; // 总副本数
private Integer availableCopies; // 可用副本数
private Integer status; // 0-可借阅,1-已借出,2-维护中
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
@Data
@TableName("library_user")
public class LibraryUser {
@TableId(type = IdType.AUTO)
private Long id;
private String userNo; // 读者证号
private String username;
private String email;
private String phone;
private Integer userType; // 用户类型:0-学生,1-教师,2-管理员
private Integer maxBorrowCount; // 最大借阅数量
private Integer status; // 0-正常,1-冻结
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
@Data
@TableName("borrow_record")
public class BorrowRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long bookId;
private LocalDateTime borrowTime;
private LocalDateTime dueTime; // 应还时间
private LocalDateTime returnTime; // 实际归还时间
private Integer status; // 0-借阅中,1-已归还,2-逾期
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}
5.5 业务逻辑实现
java
package com.library.monolith.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.library.monolith.entity.Book;
import com.library.monolith.entity.BorrowRecord;
import com.library.monolith.entity.LibraryUser;
import com.library.monolith.mapper.BookMapper;
import com.library.monolith.mapper.BorrowRecordMapper;
import com.library.monolith.mapper.LibraryUserMapper;
import com.library.monolith.service.BorrowService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class BorrowServiceImpl extends ServiceImpl<BorrowRecordMapper, BorrowRecord> implements BorrowService {
private final BookMapper bookMapper;
private final LibraryUserMapper userMapper;
private final BorrowRecordMapper borrowRecordMapper;
/**
* 借阅图书 - 单体架构下的本地事务方法
*/
@Override
@Transactional(rollbackFor = Exception.class)
public BorrowResult borrowBook(Long userId, Long bookId) {
log.info("开始处理借阅请求: userId={}, bookId={}", userId, bookId);
// 1. 验证用户存在且状态正常
LibraryUser user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
if (user.getStatus() != 0) {
throw new BusinessException("用户账户被冻结");
}
// 2. 验证图书存在且可借
Book book = bookMapper.selectById(bookId);
if (book == null) {
throw new BusinessException("图书不存在");
}
if (book.getStatus() != 0) {
throw new BusinessException("图书当前不可借阅");
}
if (book.getAvailableCopies() <= 0) {
throw new BusinessException("图书已全部借出");
}
// 3. 检查用户借阅数量是否超限
LambdaQueryWrapper<BorrowRecord> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(BorrowRecord::getUserId, userId)
.eq(BorrowRecord::getStatus, 0);
long borrowingCount = borrowRecordMapper.selectCount(queryWrapper);
if (borrowingCount >= user.getMaxBorrowCount()) {
throw new BusinessException("已达到最大借阅数量");
}
// 4. 减少图书可用数量
book.setAvailableCopies(book.getAvailableCopies() - 1);
if (book.getAvailableCopies() == 0) {
book.setStatus(1); // 标记为已借出
}
bookMapper.updateById(book);
// 5. 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setUserId(userId);
record.setBookId(bookId);
record.setBorrowTime(LocalDateTime.now());
record.setDueTime(LocalDateTime.now().plusDays(30)); // 30天后应还
record.setStatus(0);
borrowRecordMapper.insert(record);
log.info("借阅成功: userId={}, bookId={}, recordId={}", userId, bookId, record.getId());
return new BorrowResult(true, "借阅成功", record);
}
/**
* 归还图书
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ReturnResult returnBook(Long recordId) {
log.info("开始处理归还请求: recordId={}", recordId);
BorrowRecord record = borrowRecordMapper.selectById(recordId);
if (record == null) {
throw new BusinessException("借阅记录不存在");
}
if (record.getStatus() != 0) {
throw new BusinessException("该记录已处理");
}
// 1. 更新借阅记录状态
record.setReturnTime(LocalDateTime.now());
record.setStatus(1);
borrowRecordMapper.updateById(record);
// 2. 增加图书可用数量
Book book = bookMapper.selectById(record.getBookId());
book.setAvailableCopies(book.getAvailableCopies() + 1);
if (book.getAvailableCopies() > 0) {
book.setStatus(0); // 恢复为可借阅状态
}
bookMapper.updateById(book);
// 3. 检查是否逾期
boolean isOverdue = record.getReturnTime().isAfter(record.getDueTime());
if (isOverdue) {
// 计算逾期天数
long overdueDays = record.getDueTime().until(record.getReturnTime()).toDays();
// 这里可以触发罚款计算逻辑
log.warn("借阅记录逾期: recordId={}, overdueDays={}", recordId, overdueDays);
}
log.info("归还成功: recordId={}", recordId);
return new ReturnResult(true, "归还成功", isOverdue);
}
}
5.6 控制器实现
java
package com.library.monolith.controller;
import com.library.monolith.common.Result;
import com.library.monolith.dto.BorrowRequestDTO;
import com.library.monolith.service.BorrowService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/borrow")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "借阅管理", description = "图书借阅相关接口")
public class BorrowController {
private final BorrowService borrowService;
@Operation(summary = "借阅图书")
@PostMapping("/borrow")
public Result<?> borrowBook(@RequestBody BorrowRequestDTO request) {
log.info("接收到借阅请求: {}", request);
try {
var result = borrowService.borrowBook(request.getUserId(), request.getBookId());
return Result.success(result);
} catch (BusinessException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
log.error("借阅图书异常", e);
return Result.error("系统异常");
}
}
@Operation(summary = "归还图书")
@PostMapping("/return/{recordId}")
public Result<?> returnBook(@PathVariable Long recordId) {
log.info("接收到归还请求: recordId={}", recordId);
try {
var result = borrowService.returnBook(recordId);
return Result.success(result);
} catch (BusinessException e) {
return Result.error(e.getMessage());
} catch (Exception e) {
log.error("归还图书异常", e);
return Result.error("系统异常");
}
}
@Operation(summary = "查询借阅记录")
@GetMapping("/records")
public Result<?> getBorrowRecords(@RequestParam Long userId) {
log.info("查询借阅记录: userId={}", userId);
try {
var records = borrowService.getUserBorrowRecords(userId);
return Result.success(records);
} catch (Exception e) {
log.error("查询借阅记录异常", e);
return Result.error("系统异常");
}
}
}
5.7 数据库初始化脚本
sql
-- 创建数据库
CREATE DATABASE IF NOT EXISTS library_db
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE library_db;
-- 图书表
CREATE TABLE IF NOT EXISTS book (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
isbn VARCHAR(20) NOT NULL UNIQUE COMMENT 'ISBN号',
title VARCHAR(200) NOT NULL COMMENT '书名',
author VARCHAR(100) NOT NULL COMMENT '作者',
publisher VARCHAR(100) COMMENT '出版社',
publish_date DATETIME COMMENT '出版日期',
total_copies INT DEFAULT 0 COMMENT '总副本数',
available_copies INT DEFAULT 0 COMMENT '可用副本数',
status TINYINT DEFAULT 0 COMMENT '状态:0-可借阅,1-已借出,2-维护中',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
INDEX idx_isbn (isbn),
INDEX idx_title (title),
INDEX idx_author (author)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表';
-- 用户表
CREATE TABLE IF NOT EXISTS library_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_no VARCHAR(20) NOT NULL UNIQUE COMMENT '读者证号',
username VARCHAR(50) NOT NULL COMMENT '姓名',
email VARCHAR(100) COMMENT '邮箱',
phone VARCHAR(20) COMMENT '电话',
user_type TINYINT DEFAULT 0 COMMENT '用户类型:0-学生,1-教师,2-管理员',
max_borrow_count INT DEFAULT 5 COMMENT '最大借阅数量',
status TINYINT DEFAULT 0 COMMENT '状态:0-正常,1-冻结',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
INDEX idx_user_no (user_no),
INDEX idx_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书馆用户表';
-- 借阅记录表
CREATE TABLE IF NOT EXISTS borrow_record (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '用户ID',
book_id BIGINT NOT NULL COMMENT '图书ID',
borrow_time DATETIME NOT NULL COMMENT '借阅时间',
due_time DATETIME NOT NULL COMMENT '应还时间',
return_time DATETIME COMMENT '实际归还时间',
status TINYINT DEFAULT 0 COMMENT '状态:0-借阅中,1-已归还,2-逾期',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除:0-未删除,1-已删除',
INDEX idx_user_id (user_id),
INDEX idx_book_id (book_id),
INDEX idx_status (status),
INDEX idx_borrow_time (borrow_time),
FOREIGN KEY (user_id) REFERENCES library_user(id),
FOREIGN KEY (book_id) REFERENCES book(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';
-- 插入测试数据
INSERT INTO library_user (user_no, username, email, phone, user_type, max_borrow_count) VALUES
('2024001', '张三', 'zhangsan@example.com', '13800138001', 0, 5),
('2024002', '李四', 'lisi@example.com', '13800138002', 0, 5),
('T2024001', '王老师', 'wang@example.com', '13800138003', 1, 10);
INSERT INTO book (isbn, title, author, publisher, total_copies, available_copies) VALUES
('978-7-111-12345-6', 'Spring Boot实战', '张三', '机械工业出版社', 10, 10),
('978-7-222-23456-7', '微服务架构设计', '李四', '人民邮电出版社', 5, 5),
('978-7-333-34567-8', '领域驱动设计', '王五', '电子工业出版社', 3, 3);
6. (实战2)构建微服务架构的图书馆管理系统
6.1 微服务项目整体结构
library-microservices/
├── pom.xml (父工程)
├── library-gateway/
├── library-book-service/
├── library-user-service/
├── library-borrow-service/
└── library-common/ (公共模块)
6.2 父工程POM文件
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.library</groupId>
<artifactId>library-microservices</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>library-common</module>
<module>library-gateway</module>
<module>library-book-service</module>
<module>library-user-service</module>
<module>library-borrow-service</module>
</modules>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Spring版本 -->
<spring-boot.version>3.2.5</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>
<!-- 其他依赖版本 -->
<mybatis-plus.version>3.5.6</mybatis-plus.version>
<mysql-connector.version>8.3.0</mysql-connector.version>
<springdoc.version>2.5.0</springdoc.version>
<lombok.version>1.18.30</lombok.version>
</properties>
<!-- 依赖管理 -->
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba BOM -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector.version}</version>
</dependency>
<!-- SpringDoc -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
6.3 公共模块 (library-common)
xml
<!-- library-common/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>library-microservices</artifactId>
<groupId>com.library</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>library-common</artifactId>
<dependencies>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
java
// library-common/src/main/java/com/library/common/Result.java
package com.library.common;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result<T> implements Serializable {
private Integer code;
private String message;
private T data;
private Long timestamp;
public Result() {
this.timestamp = System.currentTimeMillis();
}
public Result(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = System.currentTimeMillis();
}
public static <T> Result<T> success() {
return new Result<>(200, "success", null);
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(200, message, data);
}
public static <T> Result<T> error(String message) {
return new Result<>(500, message, null);
}
public static <T> Result<T> error(Integer code, String message) {
return new Result<>(code, message, null);
}
}
6.4 图书服务 (library-book-service)
xml
<!-- library-book-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>library-microservices</artifactId>
<groupId>com.library</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>library-book-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.library</groupId>
<artifactId>library-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>
yaml
# library-book-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: library-book-service
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/library_book_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
# Nacos配置
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
group: DEFAULT_GROUP
service: ${spring.application.name}
ip: 127.0.0.1
port: ${server.port}
heartbeat-interval: 5000ms
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
management:
endpoints:
web:
exposure:
include: health,info,metrics
java
// 图书服务主启动类
package com.library.book;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.library.book.mapper")
public class BookServiceApplication {
public static void main(String[] args) {
SpringApplication.run(BookServiceApplication.class, args);
System.out.println("图书服务启动成功,端口:8081");
}
}
java
// 图书服务控制器
package com.library.book.controller;
import com.library.book.dto.BookDTO;
import com.library.book.service.BookService;
import com.library.common.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/book")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "图书管理", description = "图书相关接口")
public class BookController {
private final BookService bookService;
@Operation(summary = "获取图书信息")
@GetMapping("/{id}")
public Result<BookDTO> getBook(@PathVariable Long id) {
log.info("获取图书信息: id={}", id);
BookDTO book = bookService.getBookById(id);
if (book == null) {
return Result.error("图书不存在");
}
return Result.success(book);
}
@Operation(summary = "减少库存")
@PutMapping("/{id}/stock/reduce")
public Result<Boolean> reduceStock(@PathVariable Long id, @RequestParam Integer quantity) {
log.info("减少图书库存: id={}, quantity={}", id, quantity);
try {
boolean success = bookService.reduceStock(id, quantity);
if (success) {
return Result.success(true);
} else {
return Result.error("库存不足");
}
} catch (Exception e) {
log.error("减少库存异常", e);
return Result.error("操作失败");
}
}
@Operation(summary = "增加库存")
@PutMapping("/{id}/stock/increase")
public Result<Boolean> increaseStock(@PathVariable Long id, @RequestParam Integer quantity) {
log.info("增加图书库存: id={}, quantity={}", id, quantity);
try {
bookService.increaseStock(id, quantity);
return Result.success(true);
} catch (Exception e) {
log.error("增加库存异常", e);
return Result.error("操作失败");
}
}
}
6.5 用户服务 (library-user-service)
xml
<!-- library-user-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>library-microservices</artifactId>
<groupId>com.library</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>library-user-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.library</groupId>
<artifactId>library-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>
yaml
# library-user-service/src/main/resources/application.yml
server:
port: 8082
spring:
application:
name: library-user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/library_user_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
group: DEFAULT_GROUP
service: ${spring.application.name}
ip: 127.0.0.1
port: ${server.port}
heartbeat-interval: 5000ms
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
java
// 用户服务控制器
package com.library.user.controller;
import com.library.common.Result;
import com.library.user.dto.UserDTO;
import com.library.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "用户管理", description = "用户相关接口")
public class UserController {
private final UserService userService;
@Operation(summary = "获取用户信息")
@GetMapping("/{id}")
public Result<UserDTO> getUser(@PathVariable Long id) {
log.info("获取用户信息: id={}", id);
UserDTO user = userService.getUserById(id);
if (user == null) {
return Result.error("用户不存在");
}
return Result.success(user);
}
@Operation(summary = "验证用户状态")
@GetMapping("/{id}/status")
public Result<Boolean> checkUserStatus(@PathVariable Long id) {
log.info("验证用户状态: id={}", id);
boolean isValid = userService.isUserValid(id);
return Result.success(isValid);
}
@Operation(summary = "获取借阅数量")
@GetMapping("/{id}/borrow-count")
public Result<Integer> getBorrowCount(@PathVariable Long id) {
log.info("获取用户借阅数量: id={}", id);
int count = userService.getCurrentBorrowCount(id);
return Result.success(count);
}
}
6.6 借阅服务 (library-borrow-service) - 核心服务
xml
<!-- library-borrow-service/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>library-microservices</artifactId>
<groupId>com.library</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>library-borrow-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.library</groupId>
<artifactId>library-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 数据访问 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!-- Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
</project>
yaml
# library-borrow-service/src/main/resources/application.yml
server:
port: 8083
spring:
application:
name: library-borrow-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/library_borrow_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root123
cloud:
nacos:
discovery:
server-addr: localhost:8848
namespace: public
group: DEFAULT_GROUP
service: ${spring.application.name}
ip: 127.0.0.1
port: ${server.port}
heartbeat-interval: 5000ms
sentinel:
transport:
dashboard: localhost:8858
eager: true
# Feign配置
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: basic
sentinel:
enabled: true
java
// Feign客户端定义
package com.library.borrow.feign;
import com.library.common.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "library-book-service", fallbackFactory = BookFallbackFactory.class)
public interface BookFeignClient {
@GetMapping("/book/{id}")
Result<?> getBook(@PathVariable Long id);
@PutMapping("/book/{id}/stock/reduce")
Result<Boolean> reduceStock(@PathVariable Long id, @RequestParam Integer quantity);
@PutMapping("/book/{id}/stock/increase")
Result<Boolean> increaseStock(@PathVariable Long id, @RequestParam Integer quantity);
}
@FeignClient(name = "library-user-service", fallbackFactory = UserFallbackFactory.class)
public interface UserFeignClient {
@GetMapping("/user/{id}")
Result<?> getUser(@PathVariable Long id);
@GetMapping("/user/{id}/status")
Result<Boolean> checkUserStatus(@PathVariable Long id);
@GetMapping("/user/{id}/borrow-count")
Result<Integer> getBorrowCount(@PathVariable Long id);
}
java
// 借阅服务核心业务逻辑
package com.library.borrow.service.impl;
import com.library.borrow.dto.BorrowRecordDTO;
import com.library.borrow.entity.BorrowRecord;
import com.library.borrow.feign.BookFeignClient;
import com.library.borrow.feign.UserFeignClient;
import com.library.borrow.mapper.BorrowRecordMapper;
import com.library.borrow.service.BorrowService;
import com.library.common.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class BorrowServiceImpl implements BorrowService {
private final BorrowRecordMapper borrowRecordMapper;
private final UserFeignClient userFeignClient;
private final BookFeignClient bookFeignClient;
/**
* 借阅图书 - 微服务架构下的分布式事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public BorrowResult borrowBook(Long userId, Long bookId) {
log.info("【微服务】开始处理借阅请求: userId={}, bookId={}", userId, bookId);
// 1. 远程调用用户服务验证用户
Result<?> userResult = userFeignClient.getUser(userId);
if (userResult.getCode() != 200 || userResult.getData() == null) {
throw new BusinessException("用户不存在或服务异常");
}
Result<Boolean> statusResult = userFeignClient.checkUserStatus(userId);
if (statusResult.getCode() != 200 || !Boolean.TRUE.equals(statusResult.getData())) {
throw new BusinessException("用户账户异常");
}
// 2. 远程调用获取用户当前借阅数量
Result<Integer> countResult = userFeignClient.getBorrowCount(userId);
if (countResult.getCode() != 200) {
throw new BusinessException("获取借阅数量失败");
}
// 3. 远程调用图书服务获取图书信息
Result<?> bookResult = bookFeignClient.getBook(bookId);
if (bookResult.getCode() != 200 || bookResult.getData() == null) {
throw new BusinessException("图书不存在或服务异常");
}
// 4. 创建借阅记录(本地事务)
BorrowRecord record = new BorrowRecord();
record.setUserId(userId);
record.setBookId(bookId);
record.setBorrowTime(LocalDateTime.now());
record.setDueTime(LocalDateTime.now().plusDays(30));
record.setStatus(0);
borrowRecordMapper.insert(record);
// 5. 远程调用减少库存
Result<Boolean> reduceResult = bookFeignClient.reduceStock(bookId, 1);
if (reduceResult.getCode() != 200 || !Boolean.TRUE.equals(reduceResult.getData())) {
// 库存操作失败,需要回滚
borrowRecordMapper.deleteById(record.getId());
throw new BusinessException("库存不足");
}
log.info("借阅成功: recordId={}", record.getId());
return new BorrowResult(true, "借阅成功", record);
}
/**
* 归还图书
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ReturnResult returnBook(Long recordId) {
log.info("【微服务】开始处理归还请求: recordId={}", recordId);
BorrowRecord record = borrowRecordMapper.selectById(recordId);
if (record == null) {
throw new BusinessException("借阅记录不存在");
}
if (record.getStatus() != 0) {
throw new BusinessException("该记录已处理");
}
// 1. 更新借阅记录
record.setReturnTime(LocalDateTime.now());
record.setStatus(1);
borrowRecordMapper.updateById(record);
// 2. 远程调用增加库存
Result<Boolean> increaseResult = bookFeignClient.increaseStock(record.getBookId(), 1);
if (increaseResult.getCode() != 200 || !Boolean.TRUE.equals(increaseResult.getData())) {
// 库存操作失败,需要回滚
record.setStatus(0);
record.setReturnTime(null);
borrowRecordMapper.updateById(record);
throw new BusinessException("库存操作失败");
}
log.info("归还成功: recordId={}", recordId);
return new ReturnResult(true, "归还成功", false);
}
}
6.7 API网关 (library-gateway)
xml
<!-- library-gateway/pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>library-microservices</artifactId>
<groupId>com.library</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>library-gateway</artifactId>
<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
</project>
yaml
# library-gateway/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: library-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: book-service-route
uri: lb://library-book-service
predicates:
- Path=/api/book/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@pathKeyResolver}"
- id: user-service-route
uri: lb://library-user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- id: borrow-service-route
uri: lb://library-borrow-service
predicates:
- Path=/api/borrow/**
filters:
- StripPrefix=1
7. 两种架构的启动、注册、调用全流程演示
7.1 单体架构运行演示
bash
# 1. 启动单体应用
cd library-monolith
mvn clean package -DskipTests
java -jar target/library-monolith-1.0.0.jar
# 2. 查看启动日志
启动日志输出:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.2.5)
2024-05-27 10:00:00 [main] INFO c.l.m.LibraryMonolithApplication - Starting LibraryMonolithApplication using Java 17.0.10
2024-05-27 10:00:01 [main] INFO c.l.m.LibraryMonolithApplication - No active profile set, falling back to 1 default profile: "default"
2024-05-27 10:00:02 [main] INFO o.s.b.w.e.t.TomcatWebServer - Tomcat initialized with port 8080
2024-05-27 10:00:02 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
2024-05-27 10:00:02 [main] INFO o.a.catalina.core.StandardService - Starting service [Tomcat]
2024-05-27 10:00:02 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet engine: [Apache Tomcat/10.1.20]
2024-05-27 10:00:03 [main] INFO o.a.c.c.C.[Tomcat].[localhost].[/api] - Initializing Spring embedded WebApplicationContext
2024-05-27 10:00:03 [main] INFO o.s.b.w.s.c.ServletWebServerApplicationContext - Root WebApplicationContext: initialization completed in 1500 ms
2024-05-27 10:00:04 [main] INFO c.z.hikari.HikariDataSource - HikariPool-1 - Starting...
2024-05-27 10:00:04 [main] INFO c.z.hikari.HikariDataSource - HikariPool-1 - Start completed.
2024-05-27 10:00:05 [main] INFO o.s.b.a.e.web.EndpointLinksResolver - Exposing 4 endpoint(s) beneath base path '/actuator'
2024-05-27 10:00:05 [main] INFO o.s.b.w.e.t.TomcatWebServer - Tomcat started on port 8080
2024-05-27 10:00:05 [main] INFO c.l.m.LibraryMonolithApplication - Started LibraryMonolithApplication in 5.123 seconds
bash
# 3. 测试借阅接口
curl -X POST "http://localhost:8080/api/borrow/borrow" \
-H "Content-Type: application/json" \
-d '{"userId": 1, "bookId": 1}'
借阅成功响应:
json
{
"code": 200,
"message": "success",
"data": {
"success": true,
"message": "借阅成功",
"record": {
"id": 1,
"userId": 1,
"bookId": 1,
"borrowTime": "2024-05-27T10:00:10",
"dueTime": "2024-06-26T10:00:10",
"returnTime": null,
"status": 0
}
},
"timestamp": 1716789610123
}
7.2 微服务架构运行演示
bash
# 1. 启动Nacos
# 下载Nacos 2.3.0,进入bin目录
startup.cmd -m standalone
# 2. 启动所有微服务
# 在四个服务目录下分别执行
mvn spring-boot:run
# 3. 查看Nacos控制台
# 浏览器访问 http://localhost:8848/nacos
# 默认账号/密码:nacos/nacos
Nacos服务列表截图:
服务名 健康实例数 集群数
library-gateway 1/1 1
library-book-service 1/1 1
library-user-service 1/1 1
library-borrow-service 1/1 1
借阅服务启动日志:
2024-05-27 10:01:00 [main] INFO c.l.b.BorrowServiceApplication - Starting BorrowServiceApplication using Java 17.0.10
2024-05-27 10:01:01 [main] INFO c.l.b.BorrowServiceApplication - The following 1 profile is active: "default"
2024-05-27 10:01:02 [main] INFO o.s.c.a.n.registry.NacosAutoServiceRegistration - Auto service registration finished
2024-05-27 10:01:02 [main] INFO c.a.n.c.naming - [REGISTER-SERVICE] public registering service library-borrow-service
2024-05-27 10:01:03 [main] INFO o.s.c.o.f.FeignClientFactoryBean - Created Feign client 'library-book-service'
2024-05-27 10:01:03 [main] INFO o.s.c.o.f.FeignClientFactoryBean - Created Feign client 'library-user-service'
2024-05-27 10:01:04 [main] INFO o.s.b.w.e.t.TomcatWebServer - Tomcat started on port 8083
2024-05-27 10:01:04 [main] INFO c.l.b.BorrowServiceApplication - Started BorrowServiceApplication in 4.567 seconds
bash
# 4. 通过网关测试借阅接口
curl -X POST "http://localhost:8080/api/borrow/borrow" \
-H "Content-Type: application/json" \
-d '{"userId": 1, "bookId": 1}'
微服务借阅成功响应:
json
{
"code": 200,
"message": "success",
"data": {
"success": true,
"message": "借阅成功",
"record": {
"id": 1,
"userId": 1,
"bookId": 1,
"borrowTime": "2024-05-27T10:02:15",
"dueTime": "2024-06-26T10:02:15",
"returnTime": null,
"status": 0
}
},
"timestamp": 1716789735123
}
借阅服务调用日志:
2024-05-27 10:02:15 [http-nio-8083-exec-1] INFO c.l.b.c.BorrowController - 接收到借阅请求: BorrowRequestDTO(userId=1, bookId=1)
2024-05-27 10:02:15 [http-nio-8083-exec-1] INFO c.l.b.s.i.BorrowServiceImpl - 【微服务】开始处理借阅请求: userId=1, bookId=1
2024-05-27 10:02:16 [http-nio-8083-exec-1] DEBUG c.l.b.f.UserFeignClient - [UserFeignClient#getUser] ---> GET http://library-user-service/user/1 HTTP/1.1
2024-05-27 10:02:16 [http-nio-8083-exec-1] DEBUG c.l.b.f.UserFeignClient - [UserFeignClient#getUser] <--- HTTP/1.1 200 (15ms)
2024-05-27 10:02:16 [http-nio-8083-exec-1] DEBUG c.l.b.f.BookFeignClient - [BookFeignClient#getBook] ---> GET http://library-book-service/book/1 HTTP/1.1
2024-05-27 10:02:16 [http-nio-8083-exec-1] DEBUG c.l.b.f.BookFeignClient - [BookFeignClient#getBook] <--- HTTP/1.1 200 (12ms)
2024-05-27 10:02:16 [http-nio-8083-exec-1] INFO c.l.b.s.i.BorrowServiceImpl - 借阅成功: recordId=1
8. 压力测试与故障模拟对比
8.1 性能压测对比
使用Apache JMeter进行压力测试,配置:100个线程,循环100次,总共10000个请求。
测试结果对比:
| 指标 | 单体架构 | 微服务架构 | 差异分析 |
|---|---|---|---|
| 平均响应时间 | 45ms | 185ms | 微服务慢310%,主要开销在网络通信和序列化 |
| 吞吐量(QPS) | 2200 | 540 | 微服务下降75%,网关和服务间调用是瓶颈 |
| 错误率 | 0.1% | 0.8% | 微服务错误率更高,网络抖动导致超时 |
| CPU使用率 | 85% | 65% | 微服务负载更分散,但总资源消耗更多 |
| 内存使用 | 2GB | 6GB(总计) | 微服务每个JVM有基础开销 |
测试脚本:
bash
# 单体架构压测
jmeter -n -t library-monolith.jmx -l result-monolith.jtl
# 微服务架构压测
jmeter -n -t library-microservices.jmx -l result-microservices.jtl
8.2 故障模拟对比
场景:图书服务故障
- 单体架构故障模拟:
bash
# 模拟数据库连接失败
ALTER TABLE book MODIFY COLUMN available_copies DECIMAL(10,2);
# 再次调用借阅接口
curl -X POST "http://localhost:8080/api/borrow/borrow" \
-d '{"userId": 1, "bookId": 1}'
结果:整个应用不可用,所有接口返回500错误。
- 微服务架构故障模拟:
bash
# 停止图书服务
# 在图书服务控制台按Ctrl+C
# 再次调用借阅接口
curl -X POST "http://localhost:8080/api/borrow/borrow" \
-d '{"userId": 1, "bookId": 1}'
结果:借阅服务触发熔断降级,返回友好提示:
json
{
"code": 500,
"message": "图书服务暂时不可用,请稍后重试",
"data": null,
"timestamp": 1716789835123
}
用户服务和网关仍然正常工作。
9. 从本地调用到远程调用的本质变化
9.1 通信方式对比
单体架构通信:
java
// 本地方法调用,纳秒级
User user = userService.getUserById(1);
// 直接内存访问,无序列化开销
微服务架构通信:
客户端 -> HTTP请求 -> 网络传输 -> 服务端 -> HTTP响应 -> 客户端
序列化 TCP/IP 反序列化 处理 序列化
对象转JSON 三次握手 JSON转对象 业务逻辑 对象转JSON
9.2 事务处理对比
单体架构事务:
java
@Transactional
public void borrowBook() {
// 所有操作在同一个数据库连接中
userService.deduct(); // 本地调用
bookService.reduceStock(); // 本地调用
borrowService.create(); // 本地调用
// 要么全部成功,要么全部回滚
}
微服务架构事务:
java
// 没有全局事务注解
public void borrowBook() {
// 每个调用都是独立的HTTP请求
userFeignClient.deduct(); // 可能成功
bookFeignClient.reduceStock(); // 可能失败
borrowService.create(); // 可能成功
// 问题:如果reduceStock失败,deduct已经执行了
// 解决方案:Saga模式、TCC、本地消息表
}
9.3 数据一致性解决方案
最终一致性模式示例:
java
// 借阅服务 - Saga模式实现
@Service
public class BorrowSagaService {
@Transactional
public void borrowBook(Long userId, Long bookId) {
// 1. 创建本地借阅记录(状态为INIT)
BorrowRecord record = createBorrowRecord(userId, bookId);
// 2. 发送扣减库存命令
sendReduceStockCommand(bookId, record.getId());
// 3. 状态更新为PROCESSING
updateRecordStatus(record.getId(), "PROCESSING");
}
// 库存服务扣减成功后回调
public void onStockReduced(Long recordId) {
// 4. 发送扣减借阅数量命令
sendReduceBorrowCountCommand(recordId);
}
// 用户服务扣减成功后回调
public void onBorrowCountReduced(Long recordId) {
// 5. 更新借阅记录为成功
updateRecordStatus(recordId, "SUCCESS");
}
// 补偿操作
public void compensate(Long recordId) {
// 如果任何一步失败,执行补偿
// 发送增加库存命令
// 发送增加借阅数量命令
// 更新借阅记录为失败
}
}
10. 何时用单体?何时上微服务?
10.1 决策矩阵
| 考虑因素 | 选择单体 | 选择微服务 |
|---|---|---|
| 团队规模 | < 10人 | > 20人,多团队 |
| 项目周期 | < 6个月 | > 1年,长期演进 |
| 技术复杂度 | 技术栈统一 | 需要混合技术栈 |
| 性能要求 | 延迟敏感(<50ms) | 可接受网络开销 |
| 部署频率 | 每周/每月部署 | 每日多次部署 |
| 故障容忍 | 可接受全局故障 | 需要故障隔离 |
| 预算限制 | 有限预算 | 有基础设施投入 |
10.2 分阶段演进策略
阶段1:单体优先(0-6个月)
- 使用模块化单体
- 清晰的包结构划分
- 为未来拆分做准备
阶段2:垂直拆分(6-12个月)
- 识别核心业务边界
- 拆分1-2个核心服务
- 引入基础中间件
阶段3:全面微服务(12个月+)
- 完成所有服务拆分
- 完善监控运维体系
- 建立团队协作规范
10.3 架构决策检查清单
markdown
□ 业务复杂度是否真的需要微服务?
□ 团队是否有微服务开发经验?
□ 是否有完善的DevOps流程?
□ 是否有监控和日志解决方案?
□ 是否有多环境部署能力?
□ 是否有自动化测试体系?
□ 是否有服务治理方案?
□ 是否有数据一致性解决方案?
如果超过5个"否",建议从单体开始。
11. 架构转型中的五个关键陷阱与解决方案
11.1 陷阱一:错误的服务拆分粒度
问题现象:服务拆得过细或过粗,形成"分布式单体"或"大泥球"。
错误示例:
java
// 按技术层次拆分(错误)
user-controller-service // 只有Controller
user-service-service // 只有Service
user-dao-service // 只有DAO
// 按CRUD操作拆分(错误)
user-create-service
user-read-service
user-update-service
user-delete-service
解决方案:
java
// 按业务能力/领域驱动设计拆分(正确)
library-user-service // 用户管理领域
library-book-service // 图书管理领域
library-borrow-service // 借阅管理领域
library-payment-service // 支付领域
// 拆分原则:
// 1. 单一职责原则:一个服务只做一件事
// 2. 共同闭包原则:一起变更的东西放在一起
// 3. 松耦合高内聚:服务间依赖最小化
11.2 陷阱二:共享数据库与数据不一致
问题现象:多个服务直接访问同一个数据库,数据耦合严重。
错误示例:
sql
-- 所有微服务都连接同一个数据库
服务A: jdbc:mysql://localhost:3306/library_db
服务B: jdbc:mysql://localhost:3306/library_db
服务C: jdbc:mysql://localhost:3306/library_db
-- 服务直接修改其他服务的数据表
UPDATE borrow_record SET status = 1 WHERE id = 100; -- 图书服务修改借阅记录
解决方案:
yaml
# 每个服务独立的数据库
服务A: jdbc:mysql://localhost:3306/library_user_db
服务B: jdbc:mysql://localhost:3306/library_book_db
服务C: jdbc:mysql://localhost:3306/library_borrow_db
# 通过API访问其他服务的数据
public class BorrowService {
// 错误:直接SQL跨库查询
// @Select("SELECT * FROM user u JOIN borrow_record b ON u.id = b.user_id")
// 正确:通过Feign调用用户服务API
@Autowired
private UserFeignClient userFeignClient;
public UserDTO getUserWithBorrowInfo(Long userId) {
// 1. 调用用户服务获取用户信息
UserDTO user = userFeignClient.getUser(userId);
// 2. 本地查询借阅记录
List<BorrowRecord> records = borrowMapper.selectByUserId(userId);
// 3. 数据聚合
user.setBorrowRecords(records);
return user;
}
}
11.3 陷阱三:缺乏分布式事务管理
问题现象:跨服务操作数据不一致,出现部分成功部分失败。
错误示例:
java
// 借阅图书方法,没有事务管理
public void borrowBook(Long userId, Long bookId) {
// 1. 调用用户服务扣减借阅额度
userService.reduceBorrowQuota(userId); // 成功
// 2. 调用图书服务扣减库存
bookService.reduceStock(bookId); // 失败,库存不足
// 3. 创建借阅记录
borrowService.createRecord(userId, bookId); // 成功
// 结果:用户额度已扣,但库存没减,数据不一致!
}
解决方案:
java
// 方案1:Saga模式(推荐)
@Service
public class BorrowSagaService {
private final SagaCoordinator sagaCoordinator;
@Transactional
public void borrowBook(Long userId, Long bookId) {
// 创建Saga事务
SagaTransaction saga = sagaCoordinator.createSaga();
try {
// 步骤1:预扣库存(补偿操作:增加库存)
saga.addStep(
() -> bookService.prepareReduceStock(bookId),
() -> bookService.compensateStock(bookId)
);
// 步骤2:预扣借阅额度(补偿操作:恢复额度)
saga.addStep(
() -> userService.prepareReduceQuota(userId),
() -> userService.compensateQuota(userId)
);
// 步骤3:确认借阅(最终操作,无补偿)
saga.addStep(
() -> borrowService.confirmBorrow(userId, bookId),
null
);
// 执行Saga
saga.execute();
} catch (Exception e) {
// 执行补偿操作
saga.compensate();
throw new BusinessException("借阅失败,已回滚");
}
}
}
// 方案2:本地消息表
@Service
public class BorrowMessageService {
@Transactional
public void borrowBook(Long userId, Long bookId) {
// 1. 本地事务:创建借阅记录和消息记录
BorrowRecord record = createBorrowRecord(userId, bookId);
Message message = createMessage("BORROW_BOOK", record.getId());
// 2. 提交事务
// 3. 异步发送消息到MQ
mqProducer.send(message);
}
// 消息消费者处理分布式事务
@RabbitListener(queues = "borrow.queue")
public void processBorrowMessage(Message message) {
try {
// 调用用户服务扣减额度
userService.reduceQuota(message.getUserId());
// 调用图书服务扣减库存
bookService.reduceStock(message.getBookId());
// 更新消息状态为成功
updateMessageStatus(message.getId(), "SUCCESS");
} catch (Exception e) {
// 重试或进入死信队列
log.error("处理借阅消息失败", e);
}
}
}
11.4 陷阱四:忽略服务监控与可观测性
问题现象:系统出问题时无法快速定位,故障排查困难。
错误配置:
yaml
# 没有配置监控
management:
endpoints:
web:
exposure:
include: health # 只暴露健康检查
正确方案:
yaml
# 完整监控配置
management:
endpoints:
web:
exposure:
include: "*"
base-path: /actuator
endpoint:
health:
show-details: always
probes:
enabled: true
metrics:
enabled: true
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
distribution:
percentiles-histogram:
"[http.server.requests]": true
tags:
application: ${spring.application.name}
instance: ${spring.cloud.client.ip-address}:${server.port}
# 分布式链路追踪
spring:
sleuth:
enabled: true
sampler:
probability: 1.0
zipkin:
base-url: http://localhost:9411
java
// 完整的监控体系
/**
* 监控体系四个维度:
* 1. Metrics(指标): Prometheus + Grafana
* 2. Tracing(链路): Sleuth + Zipkin
* 3. Logging(日志): ELK Stack
* 4. Alerting(告警): AlertManager
*/
// 自定义业务指标
@Component
public class BorrowMetrics {
private final MeterRegistry meterRegistry;
private final Counter borrowCounter;
private final Timer borrowTimer;
public BorrowMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.borrowCounter = Counter.builder("library.borrow.total")
.description("借阅总数")
.tag("service", "borrow-service")
.register(meterRegistry);
this.borrowTimer = Timer.builder("library.borrow.duration")
.description("借阅处理耗时")
.tag("service", "borrow-service")
.register(meterRegistry);
}
public void recordBorrow(Long userId, Long bookId, long duration) {
borrowCounter.increment();
borrowTimer.record(duration, TimeUnit.MILLISECONDS);
// 记录自定义标签
meterRegistry.counter("library.borrow.by_user",
"user_id", userId.toString(),
"book_id", bookId.toString()
).increment();
}
}
11.5 陷阱五:API设计不规范
问题现象:服务接口随意变更,版本管理混乱,客户端兼容性问题。
错误示例:
java
// v1.0 接口
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id);
// v1.1 不兼容变更(删除了字段)
@GetMapping("/user/{id}")
public UserV2 getUser(@PathVariable Long id);
// 客户端调用失败:无法解析响应
解决方案:
java
// 方案1:版本控制
@RestController
@RequestMapping("/api/v1/users") // URL路径版本
public class UserControllerV1 {
@GetMapping("/{id}")
public UserV1 getUser(@PathVariable Long id);
}
@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
@GetMapping("/{id}")
public UserV2 getUser(@PathVariable Long id);
}
// 方案2:请求头版本
@GetMapping(value = "/user/{id}", headers = "API-Version=1")
public UserV1 getUserV1(@PathVariable Long id);
@GetMapping(value = "/user/{id}", headers = "API-Version=2")
public UserV2 getUserV2(@PathVariable Long id);
// 方案3:使用OpenAPI规范
@OpenAPIDefinition(
info = @Info(
title = "用户服务API",
version = "1.0.0",
description = "用户管理接口"
),
servers = @Server(url = "http://localhost:8080")
)
@RestController
@RequestMapping("/users")
public class UserController {
@Operation(summary = "获取用户信息", description = "根据ID获取用户详细信息")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "成功"),
@ApiResponse(responseCode = "404", description = "用户不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(
@Parameter(description = "用户ID", required = true, example = "1")
@PathVariable Long id) {
// 实现逻辑
}
// 使用@Deprecated标记过时接口
@Deprecated(forRemoval = true, since = "2.0.0")
@GetMapping("/old/{id}")
public UserOld getOldUser(@PathVariable Long id) {
// 返回兼容数据
return convertToOldFormat(getUser(id));
}
}
12. 企业级微服务体系建设规范
12.1 配置管理规范
分级配置管理:
yaml
# 1. 本地配置文件(开发环境)
# application-dev.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/library_db
username: dev_user
password: dev_pass
# 2. 配置中心配置(生产环境)
# 在Nacos配置中心配置
Data ID: library-service-prod.yml
Group: DEFAULT_GROUP
配置内容:
spring:
datasource:
url: jdbc:mysql://prod-db:3306/library_db
username: prod_user
password: ${DB_PASSWORD} # 使用环境变量
redis:
host: ${REDIS_HOST:localhost}
password: ${REDIS_PASSWORD}
# 3. 应用配置
# bootstrap.yml(优先加载)
spring:
application:
name: library-service
cloud:
nacos:
config:
server-addr: ${NACOS_HOST:localhost}:8848
file-extension: yml
namespace: ${NAMESPACE:dev}
group: ${GROUP:DEFAULT_GROUP}
refresh-enabled: true
extension-configs:
- data-id: common-config.yml
group: COMMON_GROUP
refresh: true
12.2 安全规范
API网关安全配置:
java
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
// 公开接口
.pathMatchers("/auth/login", "/auth/register").permitAll()
// 需要认证的接口
.pathMatchers("/api/**").authenticated()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
.build();
}
// JWT验证转换器
private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>>
jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
// 从JWT中提取权限
List<String> roles = jwt.getClaimAsStringList("roles");
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toList());
});
return new ReactiveJwtAuthenticationConverterAdapter(converter);
}
}
// 服务间认证
@FeignClient(name = "user-service",
configuration = FeignClientConfig.class) // 添加认证配置
public interface UserFeignClient {
@GetMapping("/user/{id}")
UserDTO getUser(@PathVariable Long id);
}
@Configuration
public class FeignClientConfig {
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
// 从安全上下文获取token并传递
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getCredentials() instanceof String) {
String token = (String) authentication.getCredentials();
requestTemplate.header("Authorization", "Bearer " + token);
}
};
}
}
12.3 命名规范
服务命名规范:
yaml
# 服务名称格式:<业务域>-<子域>-<功能>-service
spring:
application:
name: library-user-service # 正确:明确业务域和功能
# name: user-service # 避免:太泛
# name: lib-user-svc # 避免:缩写不清晰
# 数据库命名
数据库名:<服务名>_db
表名:<业务实体>_<操作类型>
示例:
library_user_db
user_basic_info
user_operation_log
# 接口命名
RESTful规范:
GET /users # 获取用户列表
GET /users/{id} # 获取指定用户
POST /users # 创建用户
PUT /users/{id} # 更新用户
DELETE /users/{id} # 删除用户
GET /users/{id}/borrow-records # 获取用户借阅记录
# 包结构规范
com.company.<业务域>.<子域>.<层级>
示例:
com.library.user
├── controller # 控制器层
├── service # 服务层
│ ├── impl # 服务实现
│ └── dto # 数据传输对象
├── repository # 数据访问层
├── entity # 实体类
├── config # 配置类
├── exception # 异常类
└── util # 工具类
12.4 部署与运维规范
Docker容器化:
dockerfile
# Dockerfile
FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
# 设置时区
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 创建非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "/app.jar"]
Kubernetes部署配置:
yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: library-user-service
namespace: library-prod
spec:
replicas: 3
selector:
matchLabels:
app: library-user-service
template:
metadata:
labels:
app: library-user-service
version: v1.2.0
spec:
containers:
- name: user-service
image: registry.example.com/library/user-service:v1.2.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: NACOS_SERVER_ADDR
value: "nacos-server:8848"
- name: JAVA_OPTS
value: "-Xmx512m -Xms256m -XX:+UseG1GC"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: library-user-service
namespace: library-prod
spec:
selector:
app: library-user-service
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: library-ingress
namespace: library-prod
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- library.example.com
secretName: library-tls
rules:
- host: library.example.com
http:
paths:
- path: /api/user
pathType: Prefix
backend:
service:
name: library-user-service
port:
number: 80
- path: /api/book
pathType: Prefix
backend:
service:
name: library-book-service
port:
number: 80
12.5 监控告警规范
Prometheus监控配置:
yaml
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'library-services'
metrics_path: '/actuator/prometheus'
static_configs:
- targets:
- 'library-user-service:8080'
- 'library-book-service:8080'
- 'library-borrow-service:8080'
- 'library-gateway:8080'
relabel_configs:
- source_labels: [__address__]
target_label: instance
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- job_name: 'kubernetes-nodes'
kubernetes_sd_configs:
- role: node
relabel_configs:
- action: labelmap
regex: __meta_kubernetes_node_label_(.+)
Grafana监控看板:
json
{
"dashboard": {
"title": "图书馆微服务监控",
"panels": [
{
"title": "服务QPS",
"targets": [{
"expr": "rate(http_server_requests_seconds_count{application=\"$application\"}[5m])",
"legendFormat": "{{instance}} - {{method}} {{uri}}"
}]
},
{
"title": "服务延迟",
"targets": [{
"expr": "histogram_quantile(0.95, rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m]))",
"legendFormat": "{{instance}} - P95"
}]
},
{
"title": "JVM内存使用",
"targets": [{
"expr": "sum(jvm_memory_used_bytes{application=\"$application\", area=\"heap\"}) by (instance)",
"legendFormat": "{{instance}} - 堆内存"
}]
},
{
"title": "数据库连接池",
"targets": [{
"expr": "hikaricp_connections_active{application=\"$application\"}",
"legendFormat": "{{instance}} - 活跃连接"
}]
}
],
"templating": {
"list": [{
"name": "application",
"query": "label_values(spring_application_name)",
"label": "服务名称"
}]
},
"refresh": "30s"
}
}
告警规则:
yaml
# alert-rules.yml
groups:
- name: library-alerts
rules:
# 服务宕机告警
- alert: ServiceDown
expr: up{job="library-services"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.instance }} 已宕机"
description: "服务 {{ $labels.instance }} 已经超过1分钟无法访问"
# 高延迟告警
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m])) > 1
for: 2m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.instance }} 延迟过高"
description: "服务 {{ $labels.instance }} 95%分位延迟超过1秒"
# 高错误率告警
- alert: HighErrorRate
expr: rate(http_server_requests_seconds_count{status=~\"5..\"}[5m]) / rate(http_server_requests_seconds_count[5m]) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.instance }} 错误率过高"
description: "服务 {{ $labels.instance }} 5xx错误率超过5%"
# JVM内存告警
- alert: HighMemoryUsage
expr: (sum(jvm_memory_used_bytes{area=\"heap\"}) by (instance) / sum(jvm_memory_max_bytes{area=\"heap\"}) by (instance)) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.instance }} 内存使用率过高"
description: "服务 {{ $labels.instance }} 堆内存使用率超过80%"
12.6 日志规范
统一日志格式:
java
@Configuration
public class LoggingConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
// 统一的MDC配置
@Bean
public CorrelationIdFilter correlationIdFilter() {
return new CorrelationIdFilter();
}
}
// 日志切面
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("@within(org.springframework.web.bind.annotation.RestController) || " +
"@within(org.springframework.stereotype.Service)")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
// 记录入参
log.info("[{}::{}] 开始执行,参数: {}", className, methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - startTime;
// 记录出参和执行时间
log.info("[{}::{}] 执行成功,耗时: {}ms,结果: {}",
className, methodName, elapsedTime, result);
return result;
} catch (Exception e) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("[{}::{}] 执行失败,耗时: {}ms,异常: {}",
className, methodName, elapsedTime, e.getMessage(), e);
throw e;
}
}
}
// 日志配置文件
# logback-spring.xml
<configuration>
<property name="LOG_PATH" value="./logs"/>
<property name="APP_NAME" value="${spring.application.name}"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [traceId:%X{traceId}] %msg%n</pattern>
</encoder>
</appender>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [traceId:%X{traceId}] %msg%n</pattern>
</encoder>
</appender>
<!-- 按级别分开的日志文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [traceId:%X{traceId}] %msg%n</pattern>
</encoder>
</appender>
<!-- 异步日志 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<!-- 特定包日志级别 -->
<logger name="com.library" level="DEBUG"/>
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
</configuration>
12.7 测试规范
分层测试策略:
java
// 1. 单元测试
@ExtendWith(MockitoExtension.class)
class BorrowServiceTest {
@Mock
private BorrowRecordMapper borrowRecordMapper;
@Mock
private UserFeignClient userFeignClient;
@Mock
private BookFeignClient bookFeignClient;
@InjectMocks
private BorrowServiceImpl borrowService;
@Test
void testBorrowBook_Success() {
// 准备测试数据
Long userId = 1L;
Long bookId = 100L;
// Mock外部依赖
when(userFeignClient.getUser(userId))
.thenReturn(Result.success(new UserDTO()));
when(bookFeignClient.getBook(bookId))
.thenReturn(Result.success(new BookDTO()));
when(bookFeignClient.reduceStock(bookId, 1))
.thenReturn(Result.success(true));
// 执行测试
BorrowResult result = borrowService.borrowBook(userId, bookId);
// 验证结果
assertTrue(result.isSuccess());
verify(borrowRecordMapper, times(1)).insert(any());
}
}
// 2. 集成测试
@SpringBootTest
@AutoConfigureMockMvc
class BorrowControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserFeignClient userFeignClient;
@Test
void testBorrowBook_Integration() throws Exception {
// 准备测试数据
BorrowRequestDTO request = new BorrowRequestDTO(1L, 100L);
// Mock Feign客户端
when(userFeignClient.getUser(anyLong()))
.thenReturn(Result.success(new UserDTO()));
// 执行HTTP请求
mockMvc.perform(post("/api/borrow/borrow")
.contentType(MediaType.APPLICATION_JSON)
.content(JsonUtils.toJson(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
}
// 3. 契约测试(消费者驱动)
@Pact(consumer = "library-borrow-service", provider = "library-user-service")
public class UserContractTest {
@Pact(consumer = "library-borrow-service", provider = "library-user-service")
public RequestResponsePact getUserPact(PactDslWithProvider builder) {
return builder
.given("用户存在")
.uponReceiving("根据ID获取用户")
.path("/user/1")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.integerType("id", 1)
.stringType("username", "张三")
.stringType("email", "zhangsan@example.com"))
.toPact();
}
@Test
@PactTestFor(pactMethod = "getUserPact")
void testGetUser(MockServer mockServer) {
// 设置Feign客户端指向Mock服务器
UserFeignClient client = Feign.builder()
.target(UserFeignClient.class, mockServer.getUrl());
// 执行测试
Result<UserDTO> result = client.getUser(1L);
// 验证
assertNotNull(result);
assertEquals(200, result.getCode());
assertEquals("张三", result.getData().getUsername());
}
}
// 4. 端到端测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class BorrowE2ETest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Container
static NacosContainer nacos = new NacosContainer("nacos/nacos-server:latest");
@Test
void testCompleteBorrowFlow() {
// 1. 启动所有服务
// 2. 准备测试数据
// 3. 执行完整的借阅流程
// 4. 验证各个系统的状态
// 5. 清理测试数据
}
}
12.8 持续集成与交付
GitLab CI/CD流水线:
yaml
# .gitlab-ci.yml
stages:
- test
- build
- scan
- deploy
variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_REGISTRY: "registry.example.com"
K8S_NAMESPACE: "library-${CI_ENVIRONMENT_NAME}"
cache:
paths:
- .m2/repository/
- target/
# 单元测试阶段
unit-test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
script:
- mvn clean test
artifacts:
reports:
junit: target/surefire-reports/TEST-*.xml
only:
- merge_requests
- main
# 集成测试阶段
integration-test:
stage: test
image: maven:3.9.6-eclipse-temurin-17
services:
- mysql:8.0
- redis:7.2
variables:
MYSQL_DATABASE: test_db
MYSQL_ROOT_PASSWORD: test_pass
script:
- mvn verify -Pintegration-test
needs: ["unit-test"]
only:
- merge_requests
- main
# 代码质量扫描
sonar-scan:
stage: scan
image: sonarsource/sonar-scanner-cli:latest
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0"
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner
-Dsonar.projectKey=library_${CI_PROJECT_NAME}
-Dsonar.sources=src/main/java
-Dsonar.host.url=${SONAR_URL}
-Dsonar.login=${SONAR_TOKEN}
only:
- main
# 构建Docker镜像
docker-build:
stage: build
image: docker:20.10.24
services:
- docker:20.10.24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t ${DOCKER_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA} .
- docker push ${DOCKER_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA}
only:
- main
# 部署到开发环境
deploy-dev:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: dev
url: https://dev.library.example.com
script:
- kubectl config set-cluster k8s-cluster --server=${K8S_SERVER}
- kubectl config set-credentials gitlab-ci --token=${K8S_TOKEN}
- kubectl config set-context k8s-context --cluster=k8s-cluster --user=gitlab-ci
- kubectl config use-context k8s-context
- |
kubectl set image deployment/${CI_PROJECT_NAME} \
${CI_PROJECT_NAME}=${DOCKER_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA} \
-n ${K8S_NAMESPACE}
- kubectl rollout status deployment/${CI_PROJECT_NAME} -n ${K8S_NAMESPACE}
only:
- main
# 部署到生产环境(手动确认)
deploy-prod:
stage: deploy
image: bitnami/kubectl:latest
environment:
name: prod
url: https://library.example.com
script:
- kubectl config set-cluster k8s-cluster --server=${K8S_SERVER_PROD}
- kubectl config set-credentials gitlab-ci --token=${K8S_TOKEN_PROD}
- kubectl config set-context k8s-context --cluster=k8s-cluster --user=gitlab-ci
- kubectl config use-context k8s-context
- |
kubectl set image deployment/${CI_PROJECT_NAME} \
${CI_PROJECT_NAME}=${DOCKER_REGISTRY}/${CI_PROJECT_PATH}:${CI_COMMIT_SHORT_SHA} \
-n ${K8S_NAMESPACE}
- kubectl rollout status deployment/${CI_PROJECT_NAME} -n ${K8S_NAMESPACE}
when: manual
only:
- main
13. 总结
13.1 核心结论回顾
经过全面的对比分析和实践验证,我们可以得出以下核心结论:
选择单体架构的情况:
- 团队规模小(5-10人以下),沟通成本低
- 业务相对简单,功能模块耦合度高
- 开发周期紧张,需要快速上线验证
- 技术团队经验有限,缺乏微服务运维能力
- 性能要求极高,无法接受网络开销
- 资源预算有限,无法支撑复杂的基础设施
选择微服务架构的情况:
- 大型团队(20人以上),需要并行开发
- 复杂业务系统,有明显领域边界
- 需要技术异构,不同服务使用不同技术栈
- 弹性伸缩需求,不同模块有不同负载特征
- 高可用性要求,需要故障隔离和容错
- 长期演进系统,需要独立部署和更新
13.2 架构演进建议
渐进式演进路径:
单体应用
模块化单体
前后端分离
拆分核心服务
全面微服务化
快速验证业务
按业务模块分包
API网关+前端独立
识别核心领域
完善治理体系
13.3 未来发展趋势
1. 服务网格(Service Mesh)
yaml
# Istio配置示例
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: library-user-vs
spec:
hosts:
- library-user-service
http:
- route:
- destination:
host: library-user-service
subset: v1
weight: 90
- destination:
host: library-user-service
subset: v2
weight: 10
timeout: 5s
retries:
attempts: 3
perTryTimeout: 2s
2. Serverless架构
java
// AWS Lambda函数示例
public class BorrowFunction implements RequestHandler<BorrowRequest, BorrowResponse> {
private final BorrowService borrowService;
public BorrowFunction() {
// 初始化依赖
this.borrowService = new BorrowServiceImpl();
}
@Override
public BorrowResponse handleRequest(BorrowRequest request, Context context) {
// 处理借阅请求
return borrowService.borrowBook(request);
}
}
3. 云原生技术栈
- 容器化:Docker作为交付标准
- 编排:Kubernetes作为运行平台
- 服务网格:Istio/Envoy处理服务间通信
- 可观测性:Prometheus+Grafana+Jaeger
- GitOps:ArgoCD进行持续部署
13.4 最终建议
给技术决策者的建议:
- 不要为了微服务而微服务:评估实际需求,避免过度设计
- 从单体开始:除非有明确证据需要微服务,否则从单体开始
- 建立模块化边界:即使在单体中,也要保持良好的模块化设计
- 基础设施先行:在拆分前,先建设好CI/CD、监控、日志等基础设施
- 小步快跑:一次只拆分一个服务,验证后再继续
给开发团队的建议:
- 掌握分布式系统基础:CAP定理、一致性模型、分布式事务
- 学习云原生技术栈:容器、编排、服务网格
- 培养全栈思维:不仅要写代码,还要懂部署、监控、排错
- 注重可观测性:日志、指标、链路追踪是微服务的眼睛
- 编写防御性代码:考虑网络抖动、服务不可用等故障场景
13.5 资源推荐
学习资源:
- 书籍:《微服务架构设计模式》、《领域驱动设计》、《云原生Java》
- 文档:Spring Cloud官方文档、Kubernetes官方文档、Istio官方文档
- 课程:Coursera的"Microservices Architecture"、Udemy的"Spring Boot Microservices"
- 社区:Spring中国社区、Kubernetes中文社区、ServiceMesh中文社区
工具推荐:
- 开发:IntelliJ IDEA Ultimate、VS Code、Docker Desktop
- 测试:Postman、JMeter、Pact
- 部署:Jenkins、GitLab CI、ArgoCD
- 监控:Prometheus、Grafana、ELK Stack、Jaeger
- 容器:Docker、containerd、Podman
- 编排:Kubernetes、Docker Swarm、Nomad
架构选择没有绝对的"对"与"错",只有"合适"与"不合适"。单体架构和微服务架构各有其适用场景,关键是要根据团队情况、业务需求、技术能力和资源约束做出明智的选择。
记住以下原则:
- 简单优于复杂:在能满足需求的前提下,选择最简单的方案
- 演进优于预设:不要试图一开始就设计出完美的架构
- 实用优于潮流:选择被验证过的技术,而不是最热门的技术
- 人优于工具:考虑团队能力和学习成本
你当前正在参与或维护的系统,它属于哪种架构?面临哪些痛点?如果向另一种架构演进,第一步应该做什么?欢迎在评论区分享你的见解。