MyBatis入门:从SQL映射到供应链库存查询

MyBatis 是 Java 后端常用的持久层框架。它不像 JPA 那样尽量屏蔽 SQL,而是把 SQL 显式交给开发者管理,再提供参数绑定、结果映射、动态 SQL、Mapper 接口等能力。对于供应链系统来说,订单、库存、仓储、采购、结算等模块经常需要精细控制 SQL,MyBatis 很适合这类场景。

整体流程

MyBatis SQL 映射流程

MyBatis 解决什么问题

直接使用 JDBC 时,开发者通常要重复处理连接、预编译 SQL、参数绑定、结果集转换和资源关闭。MyBatis 把这些模板代码收敛起来,让开发者把注意力放在 SQL 和对象映射上。

核心能力包括:

  1. Mapper 接口:用 Java 方法表达一次数据库操作。
  2. XML 或注解 SQL:显式管理 SQL,便于优化。
  3. 参数绑定:避免手工拼接 SQL,降低 SQL 注入风险。
  4. 结果映射:把查询结果转换成 Java 对象。
  5. 动态 SQL:根据条件生成不同查询。

供应链例子:库存查询

先定义库存表:

1
2
3
4
5
6
7
8
9
CREATE TABLE scm_inventory (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
warehouse_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
available_qty INT NOT NULL,
locked_qty INT NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_warehouse_sku (warehouse_id, sku_id)
);

Java 对象:

1
2
3
4
5
6
7
8
public class InventoryRecord {
private Long id;
private Long warehouseId;
private Long skuId;
private Integer availableQty;
private Integer lockedQty;
private LocalDateTime updatedAt;
}

Mapper 接口:

1
2
3
4
public interface InventoryMapper {
InventoryRecord findByWarehouseAndSku(@Param("warehouseId") Long warehouseId,
@Param("skuId") Long skuId);
}

XML 映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mapper namespace="com.example.inventory.InventoryMapper">
<resultMap id="InventoryRecordMap" type="com.example.inventory.InventoryRecord">
<id property="id" column="id"/>
<result property="warehouseId" column="warehouse_id"/>
<result property="skuId" column="sku_id"/>
<result property="availableQty" column="available_qty"/>
<result property="lockedQty" column="locked_qty"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>

<select id="findByWarehouseAndSku" resultMap="InventoryRecordMap">
SELECT id, warehouse_id, sku_id, available_qty, locked_qty, updated_at
FROM scm_inventory
WHERE warehouse_id = #{warehouseId}
AND sku_id = #{skuId}
</select>
</mapper>

这里的 #{} 是预编译参数绑定,不是字符串拼接。它会交给 JDBC PreparedStatement 处理,能避免大部分 SQL 注入风险。

动态 SQL

供应链系统经常有多条件查询,例如按仓库、SKU、是否低库存过滤:

1
2
3
4
5
public class InventoryQuery {
private Long warehouseId;
private Long skuId;
private Boolean onlyLowStock;
}

动态 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="search" resultMap="InventoryRecordMap">
SELECT id, warehouse_id, sku_id, available_qty, locked_qty, updated_at
FROM scm_inventory
<where>
<if test="warehouseId != null">
AND warehouse_id = #{warehouseId}
</if>
<if test="skuId != null">
AND sku_id = #{skuId}
</if>
<if test="onlyLowStock != null and onlyLowStock">
AND available_qty &lt; 10
</if>
</where>
ORDER BY updated_at DESC
LIMIT 100
</select>

<where> 会自动处理多余的 AND,比在 Java 里拼接字符串更清晰。

常见误区

第一,#{}${} 不能混用。#{} 是参数绑定,${} 是文本替换。只有表名、排序字段这类无法预编译的位置才可能使用 ${},并且必须做白名单校验。

1
2
private static final Set<String> ALLOWED_SORT_FIELDS =
Set.of("updated_at", "available_qty", "locked_qty");

第二,Mapper 方法不要返回过大的集合。库存流水、订单流水、报表明细必须分页查询,否则容易造成堆内存压力。

第三,SQL 要命中索引。比如 warehouse_id + sku_id 是库存查询的高频条件,就应该建立联合唯一索引。

小结

MyBatis 的价值在于让 SQL 可控,同时减少 JDBC 模板代码。供应链系统的数据一致性和查询性能很依赖 SQL 质量,因此掌握 Mapper、结果映射、动态 SQL、参数绑定和索引配合,是使用 MyBatis 的基础能力。

Spring入门:IoC、DI和供应链服务分层

Spring 的核心价值是把对象创建、依赖管理和横切能力交给容器处理,让业务代码专注于业务规则。对于供应链系统来说,订单、库存、仓储、采购、物流、财务等模块依赖关系复杂,如果所有对象都由代码手动创建,系统会很快变得难以维护。

整体流程

Spring IoC 与服务分层流程

IoC 和 DI

IoC 是控制反转。对象不再自己创建依赖,而是由 Spring 容器创建并注入。

DI 是依赖注入。一个类需要什么依赖,就通过构造器、字段或 setter 交给 Spring 注入。推荐使用构造器注入,因为依赖关系更明确,也更利于测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class OrderCreateService {
private final InventoryService inventoryService;
private final OrderRepository orderRepository;

public OrderCreateService(InventoryService inventoryService,
OrderRepository orderRepository) {
this.inventoryService = inventoryService;
this.orderRepository = orderRepository;
}

public void createOrder(CreateOrderCommand command) {
inventoryService.reserve(command.skuId(), command.quantity());
orderRepository.save(command);
}
}

OrderCreateService 不关心 InventoryServiceOrderRepository 如何创建,Spring 会在启动时完成依赖装配。

Bean 生命周期

Spring 管理的对象称为 Bean。一个 Bean 大致经历:

  1. 扫描或配置定义。
  2. 实例化对象。
  3. 注入依赖。
  4. 初始化。
  5. 进入可用状态。
  6. 容器关闭时销毁。

示例:

1
2
3
4
5
6
7
8
9
@Component
public class WarehouseRouteCache {
private Map<String, String> routeMap;

@PostConstruct
public void init() {
routeMap = loadRouteRules();
}
}

缓存预热可以放在初始化阶段,但要注意失败处理。生产系统不建议在静态代码块里做数据库查询或远程调用。

供应链服务分层

一个典型订单服务可以分为:

1
2
3
4
5
Controller  -> 接收 HTTP 请求
Service -> 编排业务流程和事务
Repository -> 数据访问
Client -> 调用外部系统
Domain -> 承载核心业务规则

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderCreateService orderCreateService;

public OrderController(OrderCreateService orderCreateService) {
this.orderCreateService = orderCreateService;
}

@PostMapping
public void create(@RequestBody CreateOrderCommand command) {
orderCreateService.createOrder(command);
}
}

分层的关键是职责清晰。Controller 不写复杂业务,Repository 不写业务判断,Service 不做大量 SQL 拼接。

AOP 的作用

Spring AOP 适合处理横切关注点,例如事务、日志、监控、权限校验。

最典型的是事务:

1
2
3
4
5
@Transactional(rollbackFor = Exception.class)
public void reserveStock(ReserveStockCommand command) {
inventoryRepository.reserve(command.skuId(), command.quantity());
inventoryLogRepository.insertReserveLog(command);
}

这里的事务不是业务方法手动提交,而是通过 Spring 代理在方法前后开启、提交或回滚。

常见误区

第一,把所有类都加 @Service。注解不是越多越好,只有需要容器管理的对象才应该成为 Bean。

第二,循环依赖。订单服务依赖库存服务,库存服务又依赖订单服务,通常说明边界设计不清晰,需要抽出领域服务或事件机制。

第三,在事务里做远程调用。远程调用会拉长数据库锁持有时间,容易造成锁等待。

第四,忽略测试。构造器注入可以让单元测试直接传入 mock 对象,不必启动完整 Spring 容器。

小结

Spring 入门重点不是背 XML 配置,而是理解容器、依赖注入、Bean 生命周期、AOP 和分层边界。供应链系统模块多、链路长,Spring 的价值是把通用工程能力交给框架,把业务复杂度留在清晰的 Service 和领域模型里。

Java多线程的应用场景和目的

Java 多线程的目的不是把代码写复杂,而是在合适的场景里提升吞吐、降低等待时间,并让 CPU、网络、磁盘和下游服务资源得到更充分的利用。是否应该使用多线程,关键不在于“会不会开线程”,而在于能否判断任务之间有没有依赖、瓶颈在哪里、并发后是否会破坏数据一致性。

整体流程

Java 多线程业务处理流程

多线程解决的核心问题

多线程常见目标有三类:

  1. 提升吞吐量:让系统在单位时间内处理更多任务。
  2. 降低等待时间:把互不依赖的 IO 操作并行执行,缩短整体响应时间。
  3. 提高资源利用率:当一个线程等待数据库、Redis、HTTP 接口时,其他线程可以继续执行。

但多线程不是默认答案。对于 CPU 密集型任务,线程数超过 CPU 核数太多会带来上下文切换;对于数据库密集型任务,线程过多可能把连接池和数据库压垮;对于共享数据修改,如果没有并发控制,会出现脏数据、重复扣减、状态错乱。

供应链场景:订单详情聚合

订单详情页通常需要展示订单基础信息、库存状态、物流轨迹、应收金额。它们来自不同模块,彼此没有强依赖:

1
2
3
4
订单基础信息 -> OMS
库存状态 -> Inventory Service
物流轨迹 -> TMS
应收金额 -> Finance Service

如果串行执行,每个接口耗时都会累加:

1
订单 30ms + 库存 40ms + 物流 80ms + 财务 50ms = 200ms

如果并行查询,整体耗时更接近最慢的那个任务:

1
max(30ms, 40ms, 80ms, 50ms) + 聚合成本

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public OrderDetail queryOrderDetail(long orderId) {
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.get(orderId), bizExecutor);
CompletableFuture<InventoryView> inventoryFuture =
CompletableFuture.supplyAsync(() -> inventoryService.viewByOrder(orderId), bizExecutor);
CompletableFuture<TrackInfo> trackFuture =
CompletableFuture.supplyAsync(() -> trackService.query(orderId), bizExecutor);
CompletableFuture<Receivable> receivableFuture =
CompletableFuture.supplyAsync(() -> financeService.receivable(orderId), bizExecutor);

CompletableFuture.allOf(orderFuture, inventoryFuture, trackFuture, receivableFuture).join();

return new OrderDetail(
orderFuture.join(),
inventoryFuture.join(),
trackFuture.join(),
receivableFuture.join()
);
}

这里的关键点是:这些查询互不依赖,且主要耗时在 IO 等待上,因此适合并行。

不适合并发的情况

下面这种库存扣减逻辑不能简单并行:

1
2
3
4
5
6
7
8
public void reserveStock(String skuCode, int qty) {
int available = inventoryRepository.queryAvailable(skuCode);
if (available < qty) {
throw new BizException("库存不足");
}
inventoryRepository.decreaseAvailable(skuCode, qty);
inventoryRepository.increaseLocked(skuCode, qty);
}

如果多个线程同时读到相同库存,再分别扣减,就可能超卖。库存预占必须依赖数据库行锁、乐观锁、原子更新或 Redis Lua 等机制保护一致性:

1
2
3
4
5
UPDATE scm_inventory
SET available_qty = available_qty - #{qty},
locked_qty = locked_qty + #{qty}
WHERE sku_code = #{skuCode}
AND available_qty >= #{qty};

再通过影响行数判断是否预占成功。

线程池必须受控

生产系统不要为每个请求随意 new Thread()。线程创建和销毁有成本,线程数量失控还会耗尽内存、连接池和下游接口容量。

推荐使用明确的业务线程池:

1
2
3
4
5
6
7
8
9
ThreadPoolExecutor bizExecutor = new ThreadPoolExecutor(
16,
32,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("order-query-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);

线程池配置要结合业务压测结果。队列不能无限大,否则流量高峰时请求会堆积很久,用户看到的是长时间等待而不是快速失败。

判断是否使用多线程的 checklist

使用多线程前可以问几个问题:

  1. 任务之间是否互不依赖。
  2. 瓶颈是 IO 等待还是 CPU 计算。
  3. 并发后是否会修改同一份业务数据。
  4. 线程池、数据库连接池、HTTP 连接池是否有容量上限。
  5. 失败后是否能降级、重试或返回部分结果。
  6. 日志和监控能否定位每个异步任务的耗时和异常。

多线程的收益来自清晰的任务拆分和资源控制。对供应链系统来说,订单聚合查询、采购对账、物流轨迹同步、批量报表生成都可能受益于并发;库存扣减、状态流转、单据审核则必须优先保证一致性。

Java多线程基础:线程生命周期、创建方式和业务边界

Java 多线程基础要掌握三件事:线程处于什么状态、线程任务如何创建、线程之间如何协作。真正写业务代码时,还要判断哪些逻辑适合并发,哪些逻辑必须串行或依赖数据库事务保护。

线程生命周期

Java 线程生命周期

Java 线程常见状态包括:

  1. NEW:线程对象已创建,但还没有调用 start()
  2. RUNNABLE:线程已经可以被 CPU 调度,可能正在运行,也可能等待时间片。
  3. BLOCKED:等待进入 synchronized 临界区。
  4. WAITING:无限期等待其他线程唤醒,例如 Object.wait()Thread.join()
  5. TIMED_WAITING:有超时时间的等待,例如 sleep()、带超时的 wait()
  6. TERMINATED:线程执行结束。

需要注意:调用 start() 后,线程不一定立即执行;调用 run() 只是普通方法调用,不会启动新线程。

创建线程的常见方式

直接继承 Thread

1
2
3
4
5
6
class InventorySyncThread extends Thread {
@Override
public void run() {
syncInventory();
}
}

实现 Runnable

1
2
3
4
5
6
class InventorySyncTask implements Runnable {
@Override
public void run() {
syncInventory();
}
}

使用 Callable 获取返回值:

1
2
3
4
Callable<Integer> task = () -> inventoryService.countLowStockSku();
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();
Integer lowStockCount = futureTask.get();

生产代码中更推荐使用线程池,而不是直接 new Thread()

1
2
ExecutorService executor = Executors.newFixedThreadPool(8);
executor.submit(() -> inventoryService.syncByWarehouse("WH-SH"));

线程池可以控制并发数量、复用线程、设置队列和拒绝策略,更适合服务端应用。

供应链业务例子

库存同步任务可能要同步多个仓库:

1
2
3
4
5
6
7
8
9
10
public void syncAllWarehouses(List<String> warehouseCodes) {
List<CompletableFuture<Void>> futures = warehouseCodes.stream()
.map(code -> CompletableFuture.runAsync(
() -> inventorySyncService.syncWarehouse(code),
inventoryExecutor
))
.toList();

CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}

这个场景适合并发,因为不同仓库之间通常可以独立同步。并发后整体耗时接近最慢的单个仓库,而不是所有仓库耗时相加。

但库存扣减不能只靠多线程提升速度。扣减涉及同一 SKU 的可用量、锁定量和库存流水,必须通过数据库条件更新、行锁或乐观锁保证一致性:

1
2
3
4
5
6
UPDATE scm_inventory
SET available_qty = available_qty - #{qty},
locked_qty = locked_qty + #{qty}
WHERE warehouse_id = #{warehouseId}
AND sku_id = #{skuId}
AND available_qty >= #{qty};

业务代码要根据影响行数判断是否预占成功。

sleep、wait、join 的区别

sleep() 让当前线程暂停一段时间,但不会释放已经持有的对象锁。

wait() 必须在 synchronized 内调用,会释放对象锁,等待其他线程 notify()notifyAll()

join() 用于等待另一个线程执行结束。

示例:

1
2
3
Thread syncThread = new Thread(() -> inventorySyncService.syncWarehouse("WH-BJ"));
syncThread.start();
syncThread.join();

在业务系统里,更常用的是 CountDownLatchCompletableFuture、线程池和消息队列,而不是直接操作 wait/notify

常见风险

第一,线程过多。线程不是越多越快,线程数超过系统承载能力后,会导致上下文切换、内存压力、连接池耗尽。

第二,共享变量不安全。多个线程同时修改 HashMap、普通 int 或业务对象时,可能出现数据错乱。

第三,异常丢失。异步任务里的异常如果没有收集和记录,主流程可能误以为任务成功。

第四,业务一致性被破坏。订单状态、库存扣减、付款确认这类流程必须有明确事务边界。

小结

多线程基础不是背 API,而是理解线程状态、任务提交、协作方式和一致性边界。供应链系统中,仓库同步、物流轨迹拉取、报表计算适合并发;库存扣减、状态流转、财务结算必须优先保证一致性。

GitHub Pages + Hexo:个人技术博客的搭建与维护

个人技术博客的价值不只是“有一个能访问的网站”,更重要的是把学习、项目经验和问题复盘沉淀成可检索的资产。对于 Java 后端开发者来说,博客可以记录数据库锁、JVM、并发、供应链业务建模、AI 编程工具等内容,长期来看比零散笔记更容易形成体系。

这篇文章用当前项目的实际结构重新整理 Hexo + GitHub Pages 的搭建和维护流程。

整体流程

Hexo 博客发布流程

Hexo 适合解决什么问题

Hexo 是一个基于 Node.js 的静态博客框架。它把 Markdown 文章转换成 HTML、CSS、JS 等静态文件,再部署到 GitHub Pages。

这种方案的特点是:

  1. 内容用 Markdown 管理,适合技术文章。
  2. 静态页面访问快,部署成本低。
  3. 源码仓库和发布仓库可以分离,便于维护。
  4. 不依赖数据库,迁移和备份简单。

当前博客就是典型结构:

1
2
hexo_source 仓库:保存 Hexo 源码、文章、主题、配置
guxingxianyue.github.io 仓库:保存生成后的静态页面

平时写文章应该主要维护源码仓库,不要直接手改发布仓库里的 HTML。

本地启动流程

拉取源码仓库后,进入项目目录:

1
cd /Users/chenjinxing/IdeaProjects/mypy/myhexo

安装依赖:

1
npm install

本地启动:

1
npm run server

浏览器访问:

1
http://localhost:4000

如果只是验证静态生成是否正常,可以执行:

1
2
npm run clean
npm run generate

生成后的页面会放在 public/ 目录。

写一篇新文章

可以使用 Hexo 命令创建文章:

1
npm run new "MySQL事务隔离级别和MVCC"

也可以直接在 source/_posts 下创建 Markdown 文件。文章开头必须有标准 front matter:

1
2
3
4
5
---
title: MySQL事务隔离级别和MVCC:订单可见性怎么保证
date: 2025-04-17 09:50:00
tags: [MySQL, MVCC, 事务隔离级别, 供应链系统]
---

建议每篇技术文章都至少包含四部分:

  1. 背景:这个技术点解决什么问题。
  2. 原理:核心概念和工作机制。
  3. Demo:用代码或 SQL 说明。
  4. 场景:结合真实业务说明什么时候用、怎么避坑。

例如供应链系统里的库存预占文章,不应该只解释 SELECT ... FOR UPDATE,还要说明订单创建、库存锁定、扣减、释放和库存流水之间的关系。

内容质量检查清单

技术博客不要只追求“能发布”,更要保证读者能从文章里得到可复用的经验。每次写完文章,可以按下面的清单检查:

  1. 标题是否明确回答一个问题,而不是只写一个技术名词。
  2. 正文是否解释了为什么需要这个技术点。
  3. 是否给出可以运行或接近真实项目的代码、SQL、命令。
  4. 是否说明了适用场景和不适用场景。
  5. 是否把抽象概念落到订单、库存、采购、仓储、对账等业务对象上。
  6. 是否有流程图或结构图帮助读者快速建立整体认识。

例如写 “MySQL 行锁” 时,只说行锁、表锁、间隙锁是不够的。更好的写法是先给出库存预占场景,再说明为什么 WHERE sku_id = ? AND warehouse_id = ? 必须命中索引,最后补上死锁排查方式。这样文章才像工程复盘,而不是概念摘抄。

从旧电脑迁移时要注意什么

如果博客是从旧电脑迁移过来的,优先确认三个东西:

  1. 源码仓库是否完整,包括 source/_posts、主题、package.json_config.yml
  2. Node.js 版本和依赖是否能正常安装。
  3. 部署密钥或 GitHub 登录状态是否可用。

迁移后建议先不要直接部署,先执行:

1
2
3
npm install
npm run clean
npm run generate

如果生成成功,再启动本地服务:

1
npm run server

浏览器确认归档页、文章页、图片路径都正常后,再执行 npm run deploy。这样可以避免把本地缺图、front matter 错误、依赖缺失等问题发布到线上。

推荐的日常发布节奏

日常维护可以按这个顺序执行:

1
2
3
4
5
6
7
8
git pull origin master
npm run new "文章标题"
npm run generate
git status
git add source public db.json
git commit -m "Add xxx post"
git push origin master
npm run deploy

这里要注意,public/ 是生成结果,source/_posts 才是文章源文件。如果只是修改文章内容,先改 Markdown,再重新生成,不要直接改 public/ 里的 HTML。直接改生成结果,下一次 hexo generate 就会被覆盖。

部署到 GitHub Pages

确认本地生成成功后,执行部署:

1
npm run deploy

这个命令会执行:

1
2
3
hexo clean
hexo generate
hexo deploy

部署完成后,访问:

1
https://guxingxianyue.github.io

如果线上没有立即生效,通常是 GitHub Pages 还在构建。可以等待几十秒后刷新,或者查看 GitHub Pages 的 build 状态。

维护建议

博客长期维护时,建议遵守几个规则:

  1. 源码仓库保存文章和配置,发布仓库只保存生成结果。
  2. 每篇文章都写标准 front matter,方便归档、标签和后续迁移。
  3. 技术文尽量补流程图、代码示例和业务场景。
  4. 对过时文章保留发布时间,但可以在正文中说明“当前推荐做法”。
  5. 部署前执行 npm run generate,避免线上发现构建问题。

博客本质上是个人知识库。Hexo 只是工具,真正重要的是持续把项目经验写成结构化内容,并让每篇文章都能回答一个清晰的问题。