第二章:开宗立派 · 分布式修真
单机已无法承载,需开辟洞府、建立宗门,在分布式天劫中求存
楔子:单机的极限
突破筑基期后,韩立在源界中游历了数年。他凭借着Docker容器化和扎实的运维功底,在各种服务器秘境中都能稳定运行,处理了无数业务请求。
然而,随着业务的发展,韩立发现自己的单机服务已经达到了极限。
那日,系统突然收到一个超级大客户的订单,需要处理百万级别的数据。韩立的CPU瞬间飙升至100%,内存也被耗尽,整个服务陷入了僵死状态。
“单机…已经无法承载了。“韩立看着监控面板上的一片红色,心中涌起一股无力感。
在源界中,单机服务有着天然的瓶颈:
- CPU限制:单核或多核CPU的处理能力有限
- 内存限制:物理内存无法无限扩展
- 网络限制:单机的网络带宽有限
- 存储限制:单机的磁盘IO能力有限
当业务规模超过单机的承载能力时,就必须走向分布式——将服务拆分,部署到多台服务器上,通过协作来完成复杂的业务。
这就是"开宗立派"的开始。
第一节:开辟洞府——服务拆分
韩立知道,要突破单机的限制,必须将自己的服务拆分。这就像修士要建立宗门,必须先将自己的功法拆分为不同的传承,让不同的弟子(服务)去修炼。
但如何拆分,却是一门大学问。
韩立想起了源界中流传的"领域驱动设计”(DDD)理论。这个理论说,应该按照业务领域来拆分服务,而不是按照技术层次。
他仔细分析自己的业务:
- 用户服务:管理用户信息、登录认证
- 订单服务:处理订单创建、支付、退款
- 商品服务:管理商品信息、库存
- 支付服务:处理支付逻辑、对账
每个服务都有自己独立的数据库,这就是"数据自治”——每个服务只管理自己的数据,不直接访问其他服务的数据。
韩立开始动手拆分。他先创建了用户服务:
// 用户服务 - user-service
class UserService {
async getUserById(userId) {
// 查询自己的数据库
return await db.users.findById(userId);
}
async createUser(userData) {
// 创建用户,只管理用户相关数据
return await db.users.create(userData);
}
}
然后是订单服务:
// 订单服务 - order-service
class OrderService {
async createOrder(orderData) {
// 创建订单,但需要调用用户服务验证用户
const user = await userService.getUserById(orderData.userId);
if (!user) throw new Error('User not found');
// 调用商品服务检查库存
const product = await productService.getProductById(orderData.productId);
if (product.stock < orderData.quantity) {
throw new Error('Insufficient stock');
}
// 创建订单
return await db.orders.create(orderData);
}
}
拆分完成后,韩立将每个服务都封装成Docker镜像,部署到了不同的服务器上。这就是"开辟洞府"——每个服务都有自己的运行环境,互不干扰。
第二节:升仙大会——服务注册与发现
服务拆分后,韩立遇到了一个新问题:订单服务需要调用用户服务,但它怎么知道用户服务在哪里?
在单体应用中,服务调用是直接的函数调用。但在分布式系统中,服务运行在不同的服务器上,需要通过网络来通信。
这就需要一个"升仙大会"——所有服务都在这里登记自己的信息,其他服务可以通过这里找到它。
在源界中,这个"升仙大会"就是服务注册中心(Service Registry),比如Eureka、Nacos、Consul等。
韩立选择了Nacos作为注册中心。他让每个服务启动时,都向Nacos注册自己的信息:
// 服务注册
const nacos = require('nacos');
const client = new nacos.NacosNamingClient({
serverList: 'nacos-server:8848',
namespace: 'public'
});
// 注册服务
await client.registerInstance('user-service', {
ip: '192.168.1.100',
port: 8080,
metadata: {
version: '1.0.0',
region: 'beijing'
}
});
当订单服务需要调用用户服务时,它先向Nacos查询:
// 服务发现
const instances = await client.selectInstances('user-service', true);
// 返回所有可用的用户服务实例列表
// [{ip: '192.168.1.100', port: 8080}, {ip: '192.168.1.101', port: 8080}]
然后选择一个实例进行调用(通常使用负载均衡算法,如轮询、随机、加权等)。
这就是"升仙大会"的作用——让所有服务都能找到彼此,组成一个协作的网络。
第三节:天道法则碑——配置中心
服务拆分后,韩立又遇到了一个问题:每个服务都有自己的配置文件,当需要修改配置时,需要重新部署每个服务,非常麻烦。
比如,当需要修改数据库连接池大小、缓存过期时间、限流阈值等参数时,如果每个服务都要重新部署,那工作量就太大了。
韩立想起了源界中的"天道法则碑"——配置中心(Configuration Center),比如Apollo、Nacos Config、Spring Cloud Config等。
配置中心统一管理所有服务的配置,支持动态更新。当配置修改后,服务可以自动感知并应用新配置,无需重启。
韩立选择了Apollo作为配置中心。他将所有服务的配置都上传到Apollo:
# user-service配置
datasource:
maxPoolSize: 20
minPoolSize: 5
cache:
expireTime: 3600
rateLimit:
qps: 10000
当需要修改配置时,韩立只需要在Apollo的管理界面中修改,服务会自动拉取新配置并应用。
这就是"天道法则碑"的威力——改天换地,只在一念之间。
第四节:界域传送阵——消息队列
随着业务的发展,韩立发现有些操作不需要立即返回结果,可以异步处理。比如发送邮件、生成报表、更新统计数据等。
如果这些操作都同步处理,会阻塞主流程,影响用户体验。
韩立想起了源界中的"界域传送阵"——消息队列(Message Queue),比如Kafka、RabbitMQ、RocketMQ等。
消息队列可以实现异步通信:服务A发送消息到队列,服务B从队列中消费消息,两者不需要直接通信,实现了"隔空传音"。
韩立选择了Kafka作为消息队列。当订单创建成功后,他发送一个消息到Kafka:
// 发送消息
const kafka = require('kafka-node');
const producer = new kafka.Producer(new kafka.KafkaClient());
producer.send([{
topic: 'order-created',
messages: JSON.stringify({
orderId: '12345',
userId: '67890',
amount: 999.99
})
}], (err, data) => {
if (err) console.error(err);
});
然后,邮件服务、统计服务等都可以订阅这个主题,异步处理订单创建后的相关操作:
// 消费消息
const consumer = new kafka.Consumer(new kafka.KafkaClient(),
[{ topic: 'order-created' }]);
consumer.on('message', async (message) => {
const order = JSON.parse(message.value);
// 发送邮件通知
await emailService.sendOrderConfirmation(order);
// 更新统计数据
await statsService.updateOrderStats(order);
});
消息队列还有一个重要作用——削峰填谷。当流量洪峰来临时,消息可以暂存在队列中,后端服务按照自己的处理能力慢慢消费,避免了系统被冲垮。
这就是"界域传送阵"的妙用——连接不同的服务模块,实现异步通信和流量控制。
第五节:藏经阁与储物戒指——数据库与缓存
在分布式系统中,数据存储是一个核心问题。韩立选择了两种不同的存储方式:
关系型数据库(MySQL)——藏经阁
MySQL就像源界中的"藏经阁",存储着结构化的数据,分门别类,关系严谨。它支持事务,保证数据的一致性,就像功法传承有序,不容有误。
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(100),
created_at TIMESTAMP
);
-- 订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
amount DECIMAL(10,2),
status VARCHAR(20),
FOREIGN KEY (user_id) REFERENCES users(id)
);
非关系型数据库(Redis)——储物戒指
Redis就像源界中的"储物戒指",存储热点数据,访问极快。它支持多种数据结构(字符串、列表、集合、哈希、有序集合),就像储物戒指中可以存放各种类型的法宝。
// 缓存用户信息
await redis.setex(`user:${userId}`, 3600, JSON.stringify(userData));
// 获取用户信息
const userData = await redis.get(`user:${userId}`);
Redis的特点是"神念一动,即刻取用"——访问速度极快,但数据可能过期(道韵消散)。所以它通常用来缓存热点数据,减少对MySQL的访问。
韩立还使用了Redis实现分布式锁,解决并发问题:
// 获取分布式锁
const lockKey = `lock:order:${orderId}`;
const lockValue = Date.now();
const acquired = await redis.set(lockKey, lockValue, 'EX', 30, 'NX');
if (acquired) {
try {
// 执行业务逻辑
await processOrder(orderId);
} finally {
// 释放锁
await redis.del(lockKey);
}
}
这就是"藏经阁"和"储物戒指"的配合——MySQL存储持久化数据,Redis缓存热点数据,两者结合,既保证了数据的一致性,又提升了访问速度。
第六节:分布式天劫——服务雪崩
就在韩立以为可以高枕无忧时,真正的考验来了——分布式天劫。
那日,系统突然收到大量请求,用户服务因为数据库连接池耗尽而崩溃。订单服务调用用户服务失败,也开始崩溃。商品服务、支付服务也相继崩溃…
这就是服务雪崩——一个服务的崩溃,导致依赖它的所有服务都崩溃,就像雪崩一样,一发不可收拾。
“这是…道统覆灭!“韩立看着监控面板上的一片红色,心中涌起一股绝望。
服务雪崩的根本原因是服务之间没有做好隔离和容错。当一个服务出现问题时,问题会迅速传播到整个系统。
韩立立刻施展"熔断术”——熔断器模式(Circuit Breaker):
// 熔断器实现
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold; // 失败阈值
this.timeout = timeout; // 超时时间
this.state = 'CLOSED'; // 状态:CLOSED(关闭)、OPEN(打开)、HALF_OPEN(半开)
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error('Circuit breaker is OPEN');
}
// 尝试恢复
this.state = 'HALF_OPEN';
}
try {
const result = await fn();
// 成功,重置计数器
this.failureCount = 0;
this.state = 'CLOSED';
return result;
} catch (error) {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.timeout;
}
throw error;
}
}
}
// 使用熔断器
const breaker = new CircuitBreaker();
try {
const user = await breaker.execute(() => userService.getUserById(userId));
} catch (error) {
// 熔断器打开,使用降级方案
const user = await getCachedUser(userId);
}
熔断器有三种状态:
- CLOSED(关闭):正常状态,请求正常通过
- OPEN(打开):失败次数达到阈值,拒绝所有请求,直接返回降级结果
- HALF_OPEN(半开):尝试恢复,允许少量请求通过,如果成功则关闭,如果失败则重新打开
这样,当一个服务崩溃时,依赖它的服务会立即熔断,不再调用它,而是使用降级方案(如返回缓存数据、返回默认值等),避免了服务雪崩。
第七节:限流诀与降级术
除了熔断,韩立还掌握了"限流诀"和"降级术”。
限流诀:使用Sentinel等限流框架,对服务进行流量控制。
// 使用Sentinel限流
const Sentinel = require('@sentinel/node');
// 定义限流规则:每秒最多1000个请求
Sentinel.flow({
resource: 'user-service',
count: 1000,
grade: 1 // QPS模式
});
// 在服务调用处进行限流
const entry = await Sentinel.entry('user-service');
try {
const user = await userService.getUserById(userId);
} finally {
entry.exit();
}
降级术:当系统压力过大时,自动关闭非核心功能,只保留核心业务。
// 降级策略
class DegradationStrategy {
async getUserInfo(userId) {
// 核心功能:获取用户基本信息
const user = await userService.getUserById(userId);
// 非核心功能:获取用户详细信息(可降级)
let userDetail = null;
if (!this.isDegraded('user-detail')) {
try {
userDetail = await userService.getUserDetail(userId);
} catch (error) {
// 失败也不影响主流程
console.error('Get user detail failed:', error);
}
}
return { ...user, detail: userDetail };
}
isDegraded(feature) {
// 根据系统负载决定是否降级
const cpuUsage = this.getCpuUsage();
const memoryUsage = this.getMemoryUsage();
return cpuUsage > 80 || memoryUsage > 80;
}
}
通过限流和降级,韩立成功抵御了分布式天劫,系统在高压下依然能够稳定运行。
第八节:分布式事务法——Seata
在分布式系统中,还有一个难题——分布式事务。
比如,创建订单的流程:
- 订单服务:创建订单记录
- 商品服务:扣减库存
- 支付服务:扣减用户余额
这三个操作需要在不同的服务中完成,但如果其中任何一个失败,都需要回滚所有操作。这就是分布式事务的挑战。
韩立想起了源界中的"分布式事务法"——Seata(Simple Extensible Autonomous Transaction Architecture)。
Seata支持多种事务模式:
- AT模式:自动补偿,通过解析SQL自动生成回滚日志
- TCC模式:Try-Confirm-Cancel,需要业务代码实现三个阶段
- Saga模式:长事务,通过补偿操作来回滚
韩立选择了AT模式,因为它对业务代码侵入最小:
// 使用Seata AT模式
const { GlobalTransaction } = require('@seata/rm-datasource');
// 开启全局事务
const tx = GlobalTransaction.begin('create-order', 60000);
try {
// 订单服务:创建订单
await orderService.createOrder(orderData);
// 商品服务:扣减库存
await productService.deductStock(productId, quantity);
// 支付服务:扣减余额
await paymentService.deductBalance(userId, amount);
// 提交事务
await tx.commit();
} catch (error) {
// 回滚事务
await tx.rollback();
throw error;
}
Seata的工作原理:
- 事务协调者(TC):Seata Server,负责协调全局事务
- 事务管理器(TM):开启全局事务的服务
- 资源管理器(RM):参与事务的各个服务
当TM开启全局事务后,RM会向TC注册分支事务。如果所有分支事务都成功,TC会通知所有RM提交;如果任何一个分支事务失败,TC会通知所有RM回滚。
这就是"分布式事务法"的威力——在分布式系统中,也能保证数据的一致性。
第九节:建立韩门
经过一系列的战斗和修炼,韩立成功建立了自己的"韩门"——一个由多个微服务组成的分布式系统。
韩门的架构:
- 用户服务:管理用户信息
- 订单服务:处理订单业务
- 商品服务:管理商品和库存
- 支付服务:处理支付逻辑
- 通知服务:发送邮件、短信等通知
- 统计服务:收集和分析业务数据
所有服务都注册到Nacos,配置统一管理在Apollo,异步通信通过Kafka,数据存储在MySQL和Redis。
韩立还建立了完善的监控体系:
- Prometheus:收集指标数据
- Grafana:可视化监控面板
- ELK:日志收集和分析
- Zipkin:分布式链路追踪
通过监控,韩立可以实时了解系统的运行状态,及时发现和解决问题。
“韩门,终于建立起来了!“韩立看着监控面板上稳定运行的各个服务,心中涌起一股成就感。
但他知道,这只是开始。在源界中,还有更强大的存在——云原生架构、服务网格、智能运维…这些都需要更高的境界才能掌握。
尾声:新的挑战
建立韩门后,韩立发现了一个新问题:虽然服务已经拆分,但部署和管理依然很麻烦。每个服务都需要单独部署、监控、扩缩容,工作量巨大。
而且,当流量增加时,需要手动增加服务器实例,响应速度慢。当流量减少时,服务器资源闲置,造成浪费。
韩立听说,在源界的高层,有一种叫做"云原生"的修炼方式,可以将服务运行在容器中,由Kubernetes统一调度,实现自动扩缩容、自愈等能力。
“云原生…我一定要掌握它!“韩立眼中闪烁着坚定的光芒。
下一章,韩立将"飞升上界”,学习Kubernetes和云原生架构,开启新的修炼之路。
本章要点总结
- 服务拆分:按照业务领域拆分服务,实现数据自治
- 服务注册与发现:使用Nacos等注册中心,实现服务间的相互发现
- 配置中心:使用Apollo统一管理配置,支持动态更新
- 消息队列:使用Kafka实现异步通信和削峰填谷
- 数据库与缓存:MySQL存储持久化数据,Redis缓存热点数据
- 服务治理:使用熔断、限流、降级等手段,防止服务雪崩
- 分布式事务:使用Seata保证分布式系统中的数据一致性
下一章,韩立将学习Kubernetes和云原生架构,实现服务的自动调度和管理。
