工作单元模式:让数据变更像事务一样可控
工作单元模式,说白了就是帮我们管理「一堆要做的数据操作」。
我刚开始做企业级应用时,经常遇到这样的场景:用户填了个表单,点了保存,结果后台要更新订单表、扣库存、写日志、发消息……如果每个操作都单独提交,中间任何一个环节失败,数据就乱套了。
工作单元模式就是来解决这个问题的。它把一组相关的数据变更打包在一起,要么全部成功,要么全部回滚。嗯,你可以把它理解成「内存里的事务管理器」。
核心概念:注册、脏检查、提交与回滚
工作单元模式有四个核心动作,我一个个说。
1. 注册对象
当你从数据库里加载一个实体,或者新建一个实体时,工作单元会把它「注册」到自己的管理列表中。注册时通常会标记对象的状态:新建(New)、脏(Dirty)、干净(Clean)、已删除(Deleted)。
我个人习惯在仓储层(Repository)里自动完成注册。每次调用仓储的查询方法,返回的实体自动进入当前工作单元的管理范围。
2. 脏检查
脏检查是工作单元最核心的机制。它定期扫描所有已注册的对象,看看哪些属性被修改了。
怎么判断脏?最简单的方式是保存对象的原始快照。每次注册时,把对象的字段值复制一份存起来。提交时,拿当前值和快照对比,有变化的就是脏对象。
我在项目中遇到过一个问题:如果实体有几十个字段,每次都全量快照,内存开销不小。后来我们改用「属性变更追踪」——在实体的 setter 方法里记录哪些字段被改过。这样脏检查就变成了 O(1) 的查询,而不是 O(n) 的对比。
3. 提交
提交时,工作单元遍历所有已注册的对象,按照「新建→更新→删除」的顺序生成 SQL 语句,然后在一个数据库事务里执行。
为什么要按这个顺序?因为新建的记录可能产生主键,更新和删除可能依赖这个主键。顺序错了,外键约束就炸了。
4. 回滚
回滚相对简单——清空工作单元内部的所有变更记录,丢弃脏检查的结果。数据库那边的事务回滚由外层控制。
我曾经踩过一个坑:工作单元的回滚只清内存,但数据库连接的事务没回滚,导致数据半提交。后来我强制要求:工作单元的提交/回滚必须和数据库事务的生命周期绑定。
代码示例:Java 实现一个简单的工作单元
下面这个例子,我手写了一个轻量级的工作单元。它支持注册、脏检查、提交和回滚。
// 实体接口
public interface IEntity {
Long getId();
void setId(Long id);
}
// 实体状态
public enum EntityState {
NEW, DIRTY, CLEAN, DELETED
}
// 工作单元
public class UnitOfWork {
private final Map<IEntity, EntityState> entities = new HashMap<>();
private final Map<IEntity, Map<String, Object>> snapshots = new HashMap<>();
// 注册新对象
public void registerNew(IEntity entity) {
entities.put(entity, EntityState.NEW);
takeSnapshot(entity);
}
// 注册脏对象(通常由仓储层调用)
public void registerDirty(IEntity entity) {
if (!entities.containsKey(entity)) {
entities.put(entity, EntityState.DIRTY);
takeSnapshot(entity);
} else {
entities.put(entity, EntityState.DIRTY);
}
}
// 注册已删除对象
public void registerDeleted(IEntity entity) {
entities.put(entity, EntityState.DELETED);
}
// 脏检查:对比快照
public boolean isDirty(IEntity entity) {
Map<String, Object> snapshot = snapshots.get(entity);
if (snapshot == null) return false;
// 这里用反射或字段访问器对比每个字段
// 简化实现:假设实体有 getFields() 方法
Map<String, Object> current = entity.getFields();
return !snapshot.equals(current);
}
// 提交
public void commit() {
// 按顺序处理:NEW → DIRTY → DELETED
for (Map.Entry<IEntity, EntityState> entry : entities.entrySet()) {
IEntity entity = entry.getKey();
EntityState state = entry.getValue();
switch (state) {
case NEW:
// 执行 INSERT
System.out.println("INSERT " + entity);
break;
case DIRTY:
// 执行 UPDATE
System.out.println("UPDATE " + entity);
break;
case DELETED:
// 执行 DELETE
System.out.println("DELETE " + entity);
break;
}
}
// 提交后清空
entities.clear();
snapshots.clear();
}
// 回滚
public void rollback() {
entities.clear();
snapshots.clear();
System.out.println("工作单元已回滚");
}
private void takeSnapshot(IEntity entity) {
// 深拷贝实体的字段
snapshots.put(entity, new HashMap<>(entity.getFields()));
}
}
JPA 中的持久化上下文:工作单元的最佳实践
如果你用过 JPA(Java Persistence API),那你其实每天都在用工作单元模式。JPA 的持久化上下文(Persistence Context)就是工作单元的一个经典实现。
持久化上下文的核心机制是这样的:
- 实体管理(EntityManager):每个 EntityManager 实例维护一个持久化上下文。它负责管理所有被持久化的实体实例。
- 自动脏检查:当事务提交时,EntityManager 会扫描所有受管实体,自动检测哪些属性被修改了。你不需要手动调用 registerDirty。
- 一级缓存:持久化上下文也是 JPA 的一级缓存。同一个 EntityManager 内,多次查询同一个 ID 的实体,只会发一次 SQL。
- 提交时刷新:调用 EntityManager.flush() 时,会把所有变更同步到数据库。但注意,flush 不等于事务提交,它只是把 SQL 发出去。
我举个例子,你感受一下:
@Transactional
public void updateOrder(Long orderId, String newStatus) {
// EntityManager 自动开启持久化上下文
Order order = entityManager.find(Order.class, orderId);
order.setStatus(newStatus); // 这里自动标记为脏
// 方法结束时,事务提交
// EntityManager 自动执行脏检查,生成 UPDATE 语句
// 如果中间抛出异常,事务回滚,持久化上下文也回滚
}
你看,整个过程你只需要修改实体属性,脏检查和提交都是自动的。这就是工作单元模式在 JPA 里的体现。
工作单元模式的核心逻辑图
下面这张图,我画了工作单元模式的完整流程。从注册对象开始,到脏检查,再到提交或回滚。你一看就明白。
工作单元模式的应用场景
不是所有项目都需要工作单元模式。我总结了几种典型场景:
| 场景 | 是否推荐 | 原因 |
|---|---|---|
| 单体应用 + 关系型数据库 | 强烈推荐 | JPA/Hibernate 已经内置,开箱即用 |
| 微服务 + 独立数据库 | 推荐 | 每个服务内部使用,跨服务用 Saga 模式 |
| 纯 NoSQL 场景 | 视情况而定 | 如果 NoSQL 不支持事务,工作单元只能保证内存一致性 |
| 批量数据导入 | 推荐 | 可以显著减少数据库交互次数,提升性能 |
避坑指南
最后分享几个我踩过的坑:
- 不要跨线程共享工作单元:工作单元不是线程安全的。每个线程或每个请求应该有自己的实例。Spring 的 @Transactional 默认绑定到当前线程,这个设计很合理。
- 注意大对象的脏检查性能:如果一个实体有几十个字段,每次提交都全量对比,性能会下降。我建议用属性变更追踪,或者只对比关键字段。
- 回滚时要清理干净:工作单元的回滚不仅要清内存,还要确保数据库连接的事务也回滚了。我曾经因为忘记回滚事务,导致脏数据写入了数据库,排查了一整天。
- 不要滥用延迟提交:工作单元虽然能减少数据库交互,但如果长时间不提交,内存里会堆积大量变更记录。我建议每个业务操作完成后及时提交,不要跨请求持有工作单元。
工作单元模式,说白了就是帮我们把零散的数据操作「攒」起来,一次性处理。它不是什么高深的技术,但用好了,能让你的代码更干净、数据更一致。嗯,希望今天的分享对你有帮助。
公众号:蓝海资料掘金营,微信deep3321