Flyway 数据库迁移:告别手工执行 SQL 脚本

Flyway 数据库迁移 第1步:目标说明 — 从 38 个手工 SQL 脚本说起 Mall 商城项目的 README 里有一句坦诚的自我检讨: SQL 脚本丢在 sql/ 目录手工执行,没有 Flyway / Liquibase。无法追踪某台机器跑过哪些 DDL,回滚靠猜。 打开 sql/feature_1.0.1/ 目录一看——38 个 SQL 文件,命名靠日期: create_table_2024_01_05.sql create_table_2024_01_29.sql alter_table_2024_02_27.sql alter_table_2024_05_12.sql alter_table_2024_09_26.sql ... 每次上线,开发人员手动连上数据库,挑出"这次要跑的"脚本,逐个执行。脚本里还夹杂了手工更新历史数据的 DML: use mall_db; alter table mall_product add column `cover_url` varchar(200) DEFAULT NULL COMMENT '封面图片url'; -- 更新历史数据 update mall_product p inner join mall_product_photo m on p.id = m.product_id set p.cover_url = m.url where m.type=1 and m.is_del=0; -- 别忘了还有分库 use mall_db_order_0; alter table order_trade_item_0 add column `cover_url` varchar(200) ... alter table order_trade_item_1 add column `cover_url` varchar(200) ... use mall_db_order_1; alter table order_trade_item_0 add column `cover_url` varchar(200) ... alter table order_trade_item_1 add column `cover_url` varchar(200) ... 这种模式下会发生什么,写过的人都懂: ...

一月 12, 2023 · 5 分钟 · 1029 字 · yaomingye

Knife4j 接口文档从配置到上线

Knife4j 接口文档 第1步:目标说明 — 打造可交互的 API 文档 后端写完接口,前端过来问"这个参数什么意思"“返回字段有哪些"“能不能让我直接调一下看看效果”——这种场景写过的都懂。 Swagger 就是来解决这个问题的。它能根据代码里的注解自动生成接口文档页面,前端直接在页面上看字段说明、调接口、看返回,不用再追着后端问。而 Knife4j 是 Swagger 的增强 UI,比原生 Swagger UI 好看得多,还支持离线文档导出、全局参数设置、接口排序等实用功能。 本教程基于 Mall 商城项目的真实配置,从零开始搭建一套 Knife4j + Swagger 接口文档,目标是让读者看完就能在自己的项目里用起来。 最终效果:访问 Knife4j 页面,能看到按模块分组的接口列表,点开任意接口能看到请求参数、响应示例,还能直接在页面上填入 Authorization 请求头,在线调试接口。 第2步:前置条件 开始之前,先确认项目环境满足以下条件。 条件 要求 验证命令 JDK 1.8+ java -version Maven 3.6+ mvn -v Spring Boot 2.x 查看 pom.xml 中 spring-boot-starter-parent 版本 现有 Spring Boot Web 项目 已有 Controller 项目中存在 @RestController 类 ⚠️ 新手提示:Knife4j 3.0.2 基于 Springfox 3.0.0,兼容 Spring Boot 2.x。如果是 Spring Boot 3.x 项目,需要使用 knife4j-openapi3-spring-boot-starter 4.x 版本,注解包名也从 io.swagger.annotations 变为 io.swagger.v3.oas.annotations,差异较大,本教程不涉及。 ...

一月 11, 2023 · 6 分钟 · 1108 字 · yaomingye

一个商城项目的结构化日志改造实录

结构化日志改造实录 第1步:目标说明 — 结构化日志到底解决什么问题 某开发者接手了一个 Spring Boot 商城项目的维护。项目跑得挺稳,直到某天凌晨收到告警——短信发送失败了,但翻遍日志找不到任何记录,因为 catch(Exception e) 的块是空的。 这就是非结构化日志的典型场景:日志看似写了,但关键信息全丢了。 结构化日志(Structured Logging)不是一门新技术,而是一种日志编写规范。它的核心目标只有一句话: 让日志既可以被人快速理解,也可以被机器(ELK、Loki、Splunk)精确检索。 本次教程通过一个真实商城项目的日志审计和改造过程,教会读者: 如何识别团队代码中的日志反模式 如何用 SLF4J 的参数化语法替代字符串拼接 如何配置 logback 实现 dev 控制台 + prod 文件持久化 的双环境策略 如何避免异常栈丢失、日志级别混乱等常见坑 完成本教程后,读者能独立完成一个 Spring Boot 项目的日志规范化改造。 第2步:前置条件 — 需要准备什么 开始之前,确保本地环境满足以下条件。 前置项 版本要求 说明 JDK 1.8+ Spring Boot 2.x 编译和运行 Spring Boot 2.x 自带 spring-boot-starter-logging(Logback + SLF4J) Lombok 1.18+ 提供 @Slf4j 注解,免去手写 Logger 声明 Maven 3.6+ 项目构建工具 验证命令: # 检查 JDK java -version # 检查 Maven mvn -version # 检查 Lombok 依赖(在 IDE 中确认 @Slf4j 可用) 📌 前置知识:读者需要了解 Java 异常体系的基本概念(checked / unchecked exception)、Spring Boot 项目的基本结构(Controller → Service → Mapper),以及日志级别 TRACE / DEBUG / INFO / WARN / ERROR 的含义。 ...

一月 10, 2023 · 11 分钟 · 2305 字 · yaomingye

CAP定理与一致性模型——从Nacos AP/CP双模理解取舍

CAP 定理与一致性模型 上篇讲了两件事:网络不可靠、时钟不可信。结尾留了一句话——这两个不确定性叠加——迫使你在"等精确答案"和"快速给大致答案"之间选边站。 这句话有一个更正式的名字:CAP 定理。 但 CAP 被误解的程度——大概仅次于"TCP 三次握手"——绝大多数文章都把它简化成"一致性、可用性、分区容错性三者选其二"——就像点菜时三选二。 真正的 CAP 远比这复杂——而且它不是一个开关——而是一条光谱。 📌 前置知识:建议先读上篇——理解网络分区和时钟漂移的成因。另外需要有 Nacos 的基本使用经验(知道它可以做注册中心和配置中心即可)。 一、CAP 的经典定义——先搞清楚每个字母到底在说什么 CAP 是 Eric Brewer 在 2000 年提出的——后来由 Gilbert 和 Lynch 在 2002 年给出了形式化证明。注意——CAP 里的"证明"不是实验验证——是数学上严格证明了这三个性质不可能同时满足。 先搞清楚每个字母的精确含义: 字母 全称 经典定义 一句话翻译 C Consistency 每次读操作——都能读到最近一次写操作的结果——所有节点在同一时刻看到的数据完全一致 “你刚写的——马上就能读到” A Availability 每个发给非故障节点的请求——都能在有限时间内得到一个非错误的响应 “请求一定有人接——不会晾着你” P Partition Tolerance 系统在部分节点之间的网络被切断后——仍然能继续对外提供服务 “网线拔了——系统还能撑——不至于完全挂掉” ⚠️ 新手提示:CAP 里的 P(分区容错)不是"系统可以容忍多少台机器宕机"——那叫容错。P 的精确含义是——任意数量的消息丢失或延迟——系统不能进入不可恢复的状态。换句话说——P 不是在问"系统会不会出分区"——分区是客观物理现象——P 是在问"分区发生时——系统还能不能运转"。 现在用一张图看清楚:没有分区时的理想状态 vs 分区发生时的两难。 flowchart TD subgraph nopartition["无网络分区——理想状态"] direction TB c1["客户端写 x=1"]:::startEnd --> n1["节点 A\nx=1"]:::data c1 -.-> n2["节点 B\nx=1\n从 A 同步"]:::data r1["客户端读 x"]:::startEnd --> n2 n2 --> res1["返回 x=1 ✅\nC 和 A 都满足"]:::data end subgraph partition["网络分区发生——A 和 B 互相不可达"] direction TB c2["客户端写 x=2"]:::startEnd --> p1["节点 A\nx=2"]:::data p1 -.->|"❌ 分区——无法同步"| p2["节点 B\nx=1(旧值)"]:::data r2["客户端读 x"]:::startEnd --> p2 p2 --> choice{"节点 B 怎么回复?"}:::condition choice -->|"返回 x=1\n(旧值——保留可用性)"| ap["选了 A——牺牲 C\n❌ 一致性被破坏"]:::reject choice -->|"拒绝响应——\n等网络恢复"| cp["选了 C——牺牲 A\n❌ 可用性被破坏"]:::reject end classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold; classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold; classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; 分区发生时——你只能在"接受不一致"和"拒绝服务"之间二选一。 ...

一月 2, 2023 · 4 分钟 · 759 字 · yaomingye

如果网络会骗你,时钟也会骗你——分布式世界的两个不确定性

如果网络会骗你,时钟也会骗你 单机程序写了几年,什么 bug 都见过——NullPointerException、死循环、线程不安全——但至少有一个信念是牢不可破的:调用一个方法,它要么返回结果,要么抛异常,不会凭空消失。 // 单机世界——确定性 boolean ok = service.deductStock(productId, 5); if (ok) { orderMapper.insert(order); // 扣成功了才下单 } 这段代码在单机上运行了成千上万次——从来没出过问题。if/else 的逻辑像物理定律一样可靠。 然后某天系统拆成了微服务。扣库存从本地方法调用变成了远程 RPC 调用: // 分布式世界——不确定性 boolean ok = rpcService.deductStock(productId, 5); // 这一行代码可能: // - 正常返回 true // - 正常返回 false // - 抛异常 // - 永远不返回——线程一直卡着 // - 扣库存成功——但响应包在网络丢了——你以为失败了 if (ok) { orderMapper.insert(order); } 从那一刻起——之前所有关于"确定性"的直觉——全部失效。 📌 前置知识:本文不需要任何分布式系统经验,但建议有基本的 TCP/HTTP 通信认知(知道"请求-响应"模式即可)。如果写过 Spring Boot 项目,理解 RPC 调用的概念,阅读体验会更好。 一、单机世界 vs 分布式世界——一张图看懂差异 单机程序中——所有事情都发生在一个进程里。方法调用是在同一块内存里跳转指令,操作系统保证要么执行完成、要么异常退出——不存在"不确定有没有执行"这种状态。 ...

一月 1, 2023 · 4 分钟 · 694 字 · yaomingye

分布式事务本质——CAP、BASE 与四大方案

分布式事务本质 一、⚡ @Transactional 在生产中失效——不是代码写错了——是底层就不是一回事 先看一个场景——最经典的"下单扣库存": // 单体应用——一个 @Transactional 搞定 @Service public class OrderService { @Transactional public void createOrder(CreateOrderRequest request) { // ① 创建订单 orderMapper.insert(order); // ② 扣库存 product.setStock(product.getStock() - quantity); productMapper.updateById(product); // ③ 扣余额 account.setBalance(account.getBalance().subtract(totalAmount)); accountMapper.updateById(account); // 这三个操作在同一个数据库中——同一个事务——要么全成功——要么全回滚 } } 拆成微服务后——同样的流程——@Transactional 失效: order-service ──→ 创建订单(自己的数据库) product-service ──→ 扣库存(product 数据库) account-service ──→ 扣余额(account 数据库) 每个服务有独立的数据库——三个 @Transactional 是三个独立的事务 → 订单创建成功——库存扣减成功——但扣余额失败 → 订单已创建——库存已扣——余额没变——钱还在——但东西已经扣了 → 数据不一致——用户赚了——公司亏了 分布式事务的本质问题:多个数据库(或服务)的操作——怎么保证"要么全成功、要么全回滚"? ...

十二月 27, 2022 · 4 分钟 · 706 字 · yaomingye

DDD 重构实战——什么时候该用 DDD?什么时候 MVC 就够了?

DDD vs MVC:如何选择? 📖 前置阅读:本文假设读者已理解 DDD 的核心概念(实体/值对象/聚合根/限界上下文)和战术代码模板(四层架构/Repository/Domain Service)。如果还不熟悉,建议先阅读 DDD 本质 和 DDD 战术落地。 一、⚡ DDD 这么好——是不是所有服务都要重构一遍? 看完前两篇——概念清楚了——代码模板也有了——冲动上来了: "先把所有微服务用 DDD 重构一遍!" ① user-service → DDD ② order-service → DDD ③ product-service → DDD ④ account-service → DDD ⑤ inventory-service → DDD → 加班 2 个月——重构了一堆——代码没更好——反而更复杂了 DDD 不是银弹——不是所有代码都值得用 DDD。这篇的核心就是告诉你:什么该改、什么不改、改到什么程度。 二、🔍 诊断——我们现有的三个服务——各自是什么情况 2.1 user-service——经典 MVC——不改 // user-service——现有结构 controller/ └─ UserController.java @RestController——GET/POST/PUT service/ └─ UserService.java 简单的增删改查 + 缓存操作 mapper/ └─ UserMapper.java MyBatis——selectById/insert/update model/ └─ User.java 15 个字段——getter/setter // UserService 最复杂的方法——也就 20 行 @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RedisTemplate<String, User> redisTemplate; public User getUser(Long userId) { String cacheKey = "user:" + userId; User cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) return cached; User user = userMapper.selectById(userId); if (user != null) redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES); return user; } public void updateUser(User user) { user.setUpdatedAt(LocalDateTime.now()); userMapper.updateById(user); redisTemplate.delete("user:" + user.getId()); // 失效缓存 } } 判断——不需要 DDD: ...

十二月 23, 2022 · 13 分钟 · 2614 字 · yaomingye

DDD 战术落地——代码怎么写

DDD 代码怎么写? 📖 前置阅读:本文假设读者已理解实体、值对象、聚合根、限界上下文、领域事件的核心概念。如果还不熟悉,建议先阅读 DDD 本质——领域驱动设计的核心概念。 一、⚡ 概念都懂了——但代码从哪个 package 开始建? 上一篇搞清楚了实体和值对象的区别、聚合根是"一致性边界"——但回到 IDE 中: 现有项目结构(MVC——三层): controller/ ├─ OrderController.java service/ ├─ OrderService.java (3000 行——上帝类) mapper/ ├─ OrderMapper.java ├─ UserMapper.java ← 跨表调用——OrderMapper 也调 UserMapper ├─ ProductMapper.java ← 跨表调用 model/ ├─ Order.java ← 只有 getter/setter——贫血 ├─ User.java ├─ Product.java 问题——现在要改成 DDD——应该怎么建目录?Repository 放哪?Domain Service 放哪? 这篇就是答案——从目录结构开始——到每一层的代码——完整的落地模板。 二、📂 项目结构——DDD 四层架构 2.1 四层——不是"三层 + 一层" 传统 MVC 三层: Controller → Service → Mapper → Service 层无限膨胀——3000 行——什么都往里塞 DDD 四层: interfaces(接口层) → 接收请求、返回响应——薄薄一层 application(应用层) → 编排业务流程——调 Repository、发事件——没有业务逻辑 domain(领域层) → 业务逻辑——聚合根、值对象、Repository 接口、领域事件 infrastructure(基础设施层)→ 技术实现——Repository 实现、数据库访问、MQ 发送 order-service/ ├── interfaces/ ← ① 接口层 │ ├── rest/ │ │ └── OrderController.java # HTTP 接口——接受请求——转给 application 层 │ ├── dto/ │ │ ├── CreateOrderRequest.java # 入参 DTO │ │ └── OrderResponse.java # 出参 DTO │ └── mq/ │ └── OrderEventListener.java # MQ 消息消费——转到 application 层 │ ├── application/ ← ② 应用层 │ ├── OrderApplicationService.java # 编排——调 Repository + 发事件——不包含业务逻辑 │ ├── command/ │ │ └── CreateOrderCommand.java # 应用层自己的命令对象——DTO 转换后的内部对象 │ └── event/ │ └── OrderEventPublisher.java # 事件发布接口——实现在 infrastructure │ ├── domain/ ← ③ 领域层——核心——不依赖任何外部框架 │ ├── model/ │ │ ├── aggregate/ │ │ │ └── Order.java # 聚合根 │ │ ├── entity/ │ │ │ └── OrderItem.java # 聚合内部实体 │ │ ├── valueobject/ │ │ │ ├── Money.java # 值对象——金额 │ │ │ ├── Address.java # 值对象——地址 │ │ │ └── OrderStatus.java # 枚举——订单状态 │ │ └── event/ │ │ ├── OrderCreatedEvent.java # 领域事件 │ │ └── OrderPaidEvent.java │ ├── repository/ │ │ └── OrderRepository.java # Repository 接口——只有接口——没有实现 │ └── service/ │ ├── OrderDomainService.java # 领域服务——跨聚合的逻辑 │ └── PricingService.java # 领域服务——价格计算策略 │ └── infrastructure/ ← ④ 基础设施层 ├── persistence/ │ ├── OrderRepositoryImpl.java # Repository 实现——调 JPA/MyBatis │ ├── mapper/ │ │ ├── OrderMapper.java # MyBatis Mapper │ │ └── OrderItemMapper.java │ └── converter/ │ └── OrderConverter.java # DO ↔ Domain 对象转换 ├── messaging/ │ └── RocketMQEventPublisher.java # 事件发布实现——发到 RocketMQ └── external/ └── UserServiceAdapter.java # 防腐层——隔离外部 User 服务 依赖方向——只能是单向的: ...

十二月 22, 2022 · 12 分钟 · 2499 字 · yaomingye

DDD 本质——领域驱动设计的核心概念

搞懂 DDD 的核心概念 一、⚡ 一个下单方法 800 行——你知道拆不开是因为什么吗? 先看一段熟悉的代码——我们所有微服务的 Controller/Service 大概都长这样: @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private ProductMapper productMapper; @Autowired private InventoryMapper inventoryMapper; public Order createOrder(CreateOrderRequest request) { // ① 查用户——有没有被封号 User user = userMapper.selectById(request.getUserId()); if (user == null || user.getStatus() == UserStatus.BANNED) { throw new BusinessException("用户不存在或已封号"); } // ② 查商品——库存够不够 BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> items = new ArrayList<>(); for (CreateOrderItemRequest itemReq : request.getItems()) { Product product = productMapper.selectById(itemReq.getProductId()); if (product == null || product.getStatus() != ProductStatus.ON_SALE) { throw new BusinessException("商品 " + itemReq.getProductId() + " 不可售"); } if (product.getStock() < itemReq.getQuantity()) { throw new BusinessException("商品 " + itemReq.getProductId() + " 库存不足"); } // ③ 扣库存——直接在 Service 里 UPDATE product.setStock(product.getStock() - itemReq.getQuantity()); productMapper.updateById(product); totalAmount = totalAmount.add(product.getPrice() .multiply(BigDecimal.valueOf(itemReq.getQuantity()))); items.add(new OrderItem(itemReq.getProductId(), itemReq.getQuantity(), product.getPrice())); } // ④ 扣余额——直接操作 Account 表 Account account = accountMapper.selectByUserId(request.getUserId()); if (account.getBalance().compareTo(totalAmount) < 0) { throw new BusinessException("余额不足"); } account.setBalance(account.getBalance().subtract(totalAmount)); accountMapper.updateById(account); // ⑤ 创建订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAY); order.setItems(items); orderMapper.insert(order); // ⑥ 发通知——MQ rocketMQTemplate.syncSend("order-created", order); return order; } } 问题不是代码长——问题是:你想加一个"首单 9 折"的功能——该加在哪? ...

十二月 21, 2022 · 12 分钟 · 2357 字 · yaomingye

JVM 面试突击

🔬 JVM 面试突击:运行时数据区、类加载、GC 与调优全解析 📌 前置知识:阅读本文需要具备 Java 基础语法知识、对 JVM 有初步概念(知道 JVM 是运行 Java 程序的虚拟机即可)。本文定位为面试突击速查手册,每个考点都按"面试怎么答"组织,命令部分附带完整的操作步骤和输出解读。 各模块面试频率参考 在开始具体考点之前,先了解各模块的面试出现频率,有助于合理分配复习时间: 模块 面试频率 关键程度 运行时数据区 ⭐⭐⭐⭐⭐ 每场必问,入门级考点 类加载机制 ⭐⭐⭐⭐⭐ 双亲委派模型高频出现 垃圾回收机制 ⭐⭐⭐⭐⭐ 区分候选人水平的关键 调优工具与实战 ⭐⭐⭐⭐ 考察实际动手能力 JMM + volatile ⭐⭐⭐⭐⭐ 并发底层原理 经典面试题 ⭐⭐⭐⭐ 综合应用能力 📌 一、JVM 运行时数据区:内存布局与职责 🧠 1.1 JVM 内存布局全景图 这是面试最常考的入门题,必须清楚每个区域的功能、是否为线程共享,以及各自可能抛出的异常。下面先用 Mermaid 展示 JVM 内存区域的整体分类: flowchart LR JVM(["🔷 JVM 运行时数据区"]) JVM --> SHARED["👥 线程共享区"] JVM --> PRIVATE["🔒 线程私有区"] SHARED --> HEAP["📦 Java堆\n对象实例 / 数组\nGC 主要区域"] SHARED --> METHOD["📋 方法区\n类信息 / 常量 / 静态变量\nJDK8+ 元空间实现"] PRIVATE --> PC["📍 程序计数器\n字节码行号指示器\n无OOM"] PRIVATE --> VMSTACK["📚 虚拟机栈\n栈帧: 局部变量表+操作数栈+动态链接\nStackOverflowError / OOM"] PRIVATE --> NATIVE["🔧 本地方法栈\nnative 方法服务\nStackOverflowError / OOM"] 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 private fill:#1e293b,stroke:#0284c7,stroke-width:1.5px,color:#f8fafc; class JVM root class SHARED,PRIVATE branch class HEAP,METHOD leaf class PC,VMSTACK,NATIVE private 下面用 HTML+CSS 布局图精确展示 JVM 内存各区域的相对位置、大小关系和内部结构: ...

十月 18, 2022 · 12 分钟 · 2372 字 · yaomingye
Cat Radio