从单体到微服务

🏗️ 从单体到微服务:拆分决策、业务边界分析与中间件选型全指南 🏗️ 一、问题切入:一个电商系统的"临界点" 假设你接手了一个运行了两年的电商单体应用。它使用 Spring Boot + MyBatis + MySQL 开发,所有模块——用户、商品、订单、库存、支付、物流——都在一个 Git 仓库、一个进程、一个数据库里运行。 刚开始 3 个开发,CI/CD 流水线 3 分钟跑完。现在团队扩到了 18 人,一个订单功能的改动要等 UI 模块的测试先跑完才能部署。上周,运营活动模块的内存泄漏导致支付服务也一起挂了——整个系统 40 分钟不可用。 这种场景不是假设,它是大多数高速增长的业务最终都会撞上的临界点。接下来的问题是: 你该不该拆?如果拆,怎么拆?拆完各服务怎么通信?中间件怎么选? 这篇文章回答这四个问题。 flowchart TD %% ========================================== %% 样式定义 %% ========================================== classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;; classDef problem fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;; classDef question fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;; subgraph MONOLITH ["单体架构现状"] M[单个 Spring Boot 进程\n所有模块耦合在一起] --> S1[代码冲突频繁\n18 人改同一仓库] M --> S2[部署互相阻塞\n改订单要等 UI 构建] M --> S3[故障无隔离\n内存泄漏拖垮全站] end subgraph DECISION ["你需要回答四个问题"] Q1([该不该拆?]) --> Q2([怎么拆?]) Q2 --> Q3([怎么通信?]) Q3 --> Q4([中间件选什么?]) end S1 -.->|推动决策| Q1 S2 -.->|推动决策| Q1 S3 -.->|推动决策| Q1 class M startEnd; class S1,S2,S3 problem; class Q1,Q2,Q3,Q4 question; 🏗️ 二、什么时候该拆:六个关键信号 拆分的收益永远伴随着代价——分布式事务、网络延迟、运维复杂度。在讨论"怎么拆"之前,必须先确认"该不该拆"。以下六个信号同时出现 3 个以上时,才值得启动拆分。 ...

十月 3, 2022 · 8 分钟 · 1559 字 · yaomingye

JUC 在中间件中的应用

JUC 在中间件中的应用:线程池与并发集合实战全景 问题切入:道格·李的组件在中间件里是如何落地的 道格·李设计的每一个 JUC 组件都有明确的定位:ThreadPoolExecutor 管理线程资源、ConcurrentHashMap 提供高并发下的安全容器、BlockingQueue 协调生产者与消费者。但这些组件本身只是"积木"——积木搭成什么,看用的人。 Tomcat、Netty、Dubbo、RocketMQ 这些中间件的作者,就是最高水平的积木搭手。他们在道格·李提供的基础上做了大量二次定制:继承 ThreadPoolExecutor 改写拒绝策略、用 ConcurrentHashMap 存储单例对象、用 BlockingQueue 实现异步日志缓冲。 翻开这些中间件的源码,你会发现:标准 JUC 组件很少被直接使用,几乎都被继承或组合包装。这不是因为标准组件不够好,而是因为每个中间件的场景都有自己的约束——Tomcat 的线程池需要在队列满时反过来创建线程(而不是拒绝),Netty 用 NioEventLoopGroup 把线程池拆成了事件循环。 本篇从源码层面逐一拆解道格·李的 JUC 积木如何在中间件中被定制、组合和落地。覆盖的中间件和对应的 JUC 组件如下: flowchart LR classDef root fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; classDef branch fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef leaf fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; ROOT[JUC 在中间件中的应用全景] ROOT --> THREAD["线程池 ThreadPoolExecutor"] THREAD --> T1["Tomcat: 请求处理线程池\n自定义 TaskQueue 配合拒绝策略"] THREAD --> T2["Netty: NioEventLoopGroup\nSingleThreadEventExecutor 模型"] THREAD --> T3["Dubbo: 多种线程池策略\nFixed/Cached/Limited/Eager"] THREAD --> T4["RocketMQ: Broker 线程池组\nSendMessage/PullMessage 等"] ROOT --> MAP["ConcurrentHashMap"] MAP --> M1["Spring IOC: singletonObjects\n所有单例 Bean 的存储容器"] MAP --> M2["Netty: DefaultChannelHandlerContext\nChannel 属性存储"] MAP --> M3["Tomcat: Servlet 映射表\nURL → Servlet 的路由缓存"] ROOT --> QUEUE["BlockingQueue"] QUEUE --> Q1["Logback: AsyncAppender\nArrayBlockingQueue 异步写日志"] QUEUE --> Q2["Tomcat: TaskQueue\n继承 LinkedBlockingQueue"] QUEUE --> Q3["Disruptor: RingBuffer\n虽非JUC但思想同源"] ROOT --> LIST["CopyOnWriteArrayList"] LIST --> L1["Tomcat: Session 监听器列表\n遍历时无需加锁"] LIST --> L2["Spring: ApplicationListener 集合\n事件多播时安全迭代"] class ROOT root; class THREAD,MAP,QUEUE,LIST branch; class T1,T2,T3,T4,M1,M2,M3,Q1,Q2,Q3,L1,L2 leaf; class T1,T2,M1,Q1,L1 highlight; 🏊 线程池在中间件中的应用 🏊 Tomcat:请求处理的线程池引擎 Tomcat 处理 HTTP 请求的核心是一个定制化的 ThreadPoolExecutor 。它没有直接用 JDK 的标准实现,而是继承了 ThreadPoolExecutor 并重写了其中的关键行为。 ...

十月 2, 2022 · 9 分钟 · 1834 字 · yaomingye

MQTT 协议

📡 MQTT 协议:角色体系、Broker 原理与 QoS 分级机制全解析 问题切入:一个智能家居的消息困境 假设你要开发一个智能家居系统,包含以下设备: 10 个温湿度传感器,每 5 秒上报一次数据 5 个智能插座,需要接收开关指令并上报当前功率 1 个手机 App,需要实时看到所有设备的状态,并能下发控制指令 你的第一反应可能是用 HTTP:传感器 POST 数据到服务端,App 轮询拉取最新状态。但很快问题就来了: 传感器数量 × 上报频率 = 10 × (1 / 5s) = 2 QPS 的上报请求 App 轮询最新状态 = 1 × (1 / 2s) = 0.5 QPS 的查询请求 设备控制指令 = App POST 到服务端,服务端再推给设备... HTTP 是请求-响应模式,服务端无法主动向设备推送指令。如果让设备轮询指令,延迟高且浪费带宽。而且温湿度传感器是低功耗设备(电池供电的 ESP8266),HTTP 的 TCP 三次握手 + Header 开销太大。 这就是 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议)解决的问题:它是一个 发布-订阅模式 的轻量级消息协议,专为低带宽、高延迟、不可靠网络下的物联网设备通信而设计。 MQTT 的角色体系 MQTT 协议定义了三种角色。大部分文章对它们的介绍含糊其词,这里逐个讲清楚。 ...

十月 1, 2022 · 14 分钟 · 2882 字 · yaomingye

Spring Boot 日志

Spring Boot 日志:打点位置、框架选型与线上排查全解析 🐛 问题切入:一段没有日志的代码 下面是一个新手开发者写的 Spring Boot 订单服务: @RestController @RequestMapping("/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public Result<Order> createOrder(@RequestBody CreateOrderRequest req) { Order order = orderService.createOrder(req); return Result.success(order); } } @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private InventoryService inventoryService; @Transactional public Order createOrder(CreateOrderRequest req) { // 扣减库存 boolean deducted = inventoryService.deduct(req.getProductId(), req.getQuantity()); if (!deducted) { throw new BusinessException("库存不足"); } // 创建订单 Order order = new Order(); order.setUserId(req.getUserId()); order.setAmount(req.getAmount()); orderMapper.insert(order); return order; } } 某天线上出现了一个问题:用户投诉"我付了钱但订单没创建成功"。后端同学打开服务器,面对空荡荡的日志文件(只有 Spring Boot 默认的启动 banner),完全不知道从哪里下手。 ...

九月 30, 2022 · 13 分钟 · 2638 字 · yaomingye

云服务器选型实战

☁️ 云服务器选型实战:带宽、CPU、内存容量估算方法论 —— 以阿里云 ECS 为例 📌 一、问题切入:新项目上云,ECS 实例怎么选? 小张接到一个新项目——做一个面向 C 端用户的电商小程序后端,预计日均 UV 5 万,高峰期 QPS(每秒请求数)约 500 📊。技术栈是 Spring Boot + MySQL + Redis,全部部署在阿里云 ECS 上。 他打开阿里云 ECS 购买页面,面对几十种实例规格、上百个配置组合: ecs.g7.large 2vCPU 8GB 最高 10Gbps ecs.c7.xlarge 4vCPU 8GB 最高 12.5Gbps ecs.r7.large 2vCPU 16GB 最高 10Gbps ecs.g7.xlarge 4vCPU 16GB 最高 12.5Gbps ... 选低了——大促时服务崩掉 💥,用户投诉;选高了——老板看账单时脸色不好 😤。 服务器选型的本质是对 三个核心维度 的估算: 带宽(网络吞吐) 、 CPU(计算能力) 、 内存(数据缓存空间) 。三个维度相互独立又彼此制约,高估任何一个都是浪费 💸,低估任何一个都是事故 🚨。本文将给出每个维度的 可量化估算公式 ,结合阿里云 ECS 的具体实例规格,形成一套可复用的选型标准。 🔍 二、估算前置:三个维度的关系 选型之前,先明确三个维度分别决定什么: ...

九月 29, 2022 · 10 分钟 · 1926 字 · yaomingye

API 响应封装

API 响应封装:统一返回格式、全局自动包装与异常处理全解析 🤔 1. 问题切入:一个没有封装的 Controller 是怎样的? 在开始讲解之前,先看一段没有做任何统一封装的 Controller 代码: @RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/findById") public ProductEntity findById(Long id) { ProductEntity product = productService.findById(id); if (product == null) { // 直接返回 null,前端收到空响应体,不知道发生了什么 return null; } return product; } @PostMapping("/insert") public String insert(@RequestBody ProductEntity product) { try { productService.insert(product); return "success"; // 字符串硬编码,前后端契约不统一 } catch (Exception e) { return e.getMessage(); // 把异常栈暴露给前端,安全风险 } } } 这段代码暴露了三个问题: ...

九月 26, 2022 · 10 分钟 · 2010 字 · yaomingye

Servlet 网络编程

🌐 Servlet 网络编程:从 HTTP 协议到 RESTful API、过滤器链、监听器与 Tomcat 部署全解析 1. 问题切入:不用 Spring,如何写一个 HTTP API? 假设你要开发一个用户管理的 RESTful API,要求: 支持 JSON 格式的增删改查 对每个请求打印访问日志 校验请求头中的认证 Token 处理跨域请求 如果使用 Spring Boot,一个 @RestController 就解决了。但 Spring MVC 的底层是什么?DispatcherServlet、FilterChain、HandlerInterceptor 这些概念是怎么来的? 这篇博客将用纯 Servlet 实现上述所有需求,让你理解 Spring MVC 底层的每一块砖。 在开始之前,先看最终效果 —— 一个纯 Servlet 实现的用户 API: // GET /api/users → 查询所有用户(JSON) // GET /api/users/1 → 查询单个用户(JSON) // POST /api/users → 创建用户(JSON请求体) // PUT /api/users/1 → 更新用户(JSON请求体) // DELETE /api/users/1 → 删除用户 2. Servlet 是什么 Servlet(Server Applet,服务端小程序)是 Java EE 规范中定义的一套 服务器端 HTTP 处理接口。它不是独立运行的程序,而是运行在 Servlet 容器(如 Tomcat)中,由容器管理其生命周期,并调用其方法来处理 HTTP 请求。 ...

九月 25, 2022 · 12 分钟 · 2428 字 · yaomingye

Long类型ID前端精度丢失

Long类型ID前端精度丢失:从IEEE 754根因到Jackson全局序列化方案 🤔 一、问题切入:一个"找不着"的订单 某天业务反馈:用户在订单详情页点进去一片空白,后台日志里看到查的是 ID 1857353925587607500,但数据库里根本没有这条记录。翻看上游接口的原始响应体,后端明明返回的是 1857353925587607552。 差了多少?不多,就差了 52:...552 变成了 ...500。但这 52 的差距足以让一条订单从数据库里彻底"消失"。 写个最简单的演示: // 后端:Java Long 值 long orderId = 1857353925587607552L; System.out.println(orderId); // 输出: 1857353925587607552 ✓ 后端没问题。再看前端: // 前端:直接解析后端返回的 JSON const json = '{"orderId": 1857353925587607552}'; const obj = JSON.parse(json); console.log(obj.orderId); // 输出: 1857353925587607500 ✗ 同一个数字,跨了一道 HTTP 就被"阉割"了最后两位精度。这不是哪家框架的 bug,也不是谁写错了代码——根因在 JavaScript Number 的底层存储格式。 flowchart TD classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; JAVA[Java Long\n1857353925587607552] JSON[JSON 数字\n1857353925587607552] PARSE[JavaScript JSON.parse] NUM[JS Number\n1857353925587607500] QUERY[用错误ID查数据库] MISS[查不到数据] JAVA -->|Jackson序列化| JSON JSON -->|HTTP响应| PARSE PARSE -->|IEEE 754精度丢失| NUM NUM --> QUERY QUERY --> MISS class JAVA,JSON data; class NUM,MISS reject; class PARSE,QUERY process; 这个问题的触发条件很具体:后端 Long 值超过 9007199254740991(即 2^53 ~ 1,约 16 位十进制数)时,前端 JSON.parse() 解析出的数字就会丢失精度。雪花算法生成的 ID 通常 17 ~ 19 位,正好踩在坑里。 ...

九月 25, 2022 · 5 分钟 · 956 字 · yaomingye

Spring MVC 常用注解

Spring MVC 常用注解:企业级全场景用法与实战指南 🤔 1. 问题切入:一个订单查询接口 假设你在开发一个电商系统的订单查询接口,需要实现以下需求: 通过订单 ID 查询订单详情 支持按状态、时间范围过滤订单列表 接收 JSON 请求体来创建订单 处理参数校验失败时的错误返回 统一处理各类异常 以下是一个典型的 Spring MVC Controller 初版实现: @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping("/{id}") public Result<Order> getOrder(@PathVariable Long id) { // 查询订单 } @GetMapping public Result<Page<Order>> listOrders( @RequestParam(required = false) String status, @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate startDate, @RequestParam(required = false) @DateTimeFormat(iso = DATE) LocalDate endDate, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { // 分页查询 } @PostMapping @ResponseStatus(HttpStatus.CREATED) public Result<Order> createOrder(@Validated @RequestBody CreateOrderRequest request) { // 创建订单 } } 短短几行代码用到了 10+ 个注解。这些注解各自承担什么职责?组合使用时有什么坑?在企业级项目中应该如何规范使用?这篇博客将系统性地回答这些问题。 ...

九月 24, 2022 · 12 分钟 · 2520 字 · yaomingye

OAuth 2.0 + JWT 单点登录

OAuth 2.0 + JWT 单点登录实战 一、从一个登录按钮说起 你在商家后台(merchant.shop.com)点了"登录",页面跳转到了一个统一的登录页,你输入账号密码,然后又跳回了商家后台——你已经在系统里了。然后你打开运营后台(ops.shop.com),不用再输密码,直接进去了。 这就是单点登录(SSO)。背后有三个角色在协作: sequenceDiagram participant Browser as 浏览器 participant Biz as 业务系统\nmerchant.shop.com participant SSO as 认证中心\nsso.company.com Browser->>Biz: 1. 访问商家后台 Biz-->>Browser: 2. 302: 去认证中心登录 Browser->>SSO: 3. 跳转到登录页 SSO-->>Browser: 4. 返回登录页面 Browser->>SSO: 5. 提交用户名密码 SSO-->>Browser: 6. 302: 登录成功,回业务系统 Browser->>Biz: 7. 带着凭证回商家后台 Biz->>SSO: 8. 后端验证凭证 SSO-->>Biz: 9. 返回用户身份 Biz-->>Browser: 10. 登录成功,进入系统 这里面有两个关键问题: 第 5 步中,用户的密码交给了谁? 答案:只交给了认证中心。业务系统从头到尾都没见过用户的密码。 第 7 步中,浏览器带回的"凭证"是什么? 答案:是一个一次性的授权码(code),不是用户名密码,也不是最终的身份令牌。 这就是 OAuth 2.0 授权码模式的核心思路:用户密码只给认证中心,业务系统通过一个间接的"授权码"来确认用户身份。 二、逐帧拆解:一次登录的完整交互 下面以一个真实场景走一遍完整流程。三个参与者: 参与者 对应系统 职责 浏览器 用户正在用的 Chrome / Edge 用户操作的入口,负责跳转和提交凭据 业务系统 CRM 应用(crm.company.com:8080) 用户真正想用的系统,需要确认"你是谁" 认证中心 SSO 服务器(sso.company.com:9000) 唯一能验证用户名密码的地方,签发身份令牌 下面是完整的交互流程——请重点关注每个角色在每一步做了什么: ...

九月 22, 2022 · 13 分钟 · 2617 字 · yaomingye
Cat Radio