简介: 数字农业库存管理系统在2020年时,部门对产地仓生鲜水果生产加工数字化的背景下应运而生。项目一期的数农WMS中的各类库存操作均为单独编写。而伴随着后续的不断迭代,这些库存操作间慢慢积累了大量的共性逻辑:如参数校验、幂等性控制、操作明细构建、同步任务构建、数据库操作CAS重试、库存动账事件发布等等……大量重复或相似的代码不利于后续维护及高效迭代,因此我们决定借鉴并比较模板方法(Template Method)和回调(Callback)的思路进行重构:我们需要为各类库存操作搭建一个统一的框架,对其中固定不变的共性逻辑进行复用,而对会随场景变化的部分提供灵活扩展的能力支持。
作者 | 在田
来源 | 阿里技术公众号
一 问题背景
数字农业库存管理系统(以下简称数农WMS)是在2020年时,部门对产地仓生鲜水果生产加工数字化的背景下应运而生。项目一期的数农WMS中的各类库存操作(如库存增加、占用、转移等)均为单独编写。而伴随着后续的不断迭代,这些库存操作间慢慢积累了大量的共性逻辑:如参数校验、幂等性控制、操作明细构建、同步任务构建、数据库操作CAS重试、库存动账事件发布等等……大量重复或相似的代码不利于后续维护及高效迭代,因此我们决定借鉴并比较模板方法(Template Method)和回调(Callback)的思路进行重构:我们需要为各类库存操作搭建一个统一的框架,对其中固定不变的共性逻辑进行复用,而对会随场景变化的部分提供灵活扩展的能力支持。
二 模板方法
GoF的《设计模式》一书中对模板方法的定义是:「定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。」 —— 其核心是对算法或业务逻辑骨架的复用,以及其中部分操作的个性化扩展。在正式介绍对数农WMS库存操作的重构工作前,我们先以一个具体案例 —— AbstractQueuedSynchronizer(注1)(以下简称AQS) —— 来了解模板方法设计模式。虽然通过AQS这个相对复杂的例子来介绍模板方法显得有些小题大做,但由于AQS一方面是Java并发包的核心框架,另一方面也是模板方法在JDK中的现实案例,对它的剖析能使我们了解其背后精心的设计思路,同时与下文将介绍的回调的重构方式进行对比,值得我们多花一些时间研究。
《Java并发编程实战》中对AQS的描述是:AQS是一个用于构建锁和同步器的框架,许多同步器都可以通过AQS很容易并且高效地构造出来。不仅ReentrantLock和Semaphore是基于AQS构建的,还包括CountDownLatch、ReentrantReadWriteLock等。AQS解决了在实现同步器时涉及的大量细节问题(例如等待线程采用FIFO队列操作顺序)。在基于AQS构建的同步器类中,最基本的操作包括各种形式的「获取操作」和「释放操作」。在不同的同步器中可以定义一些灵活的标准,来判断某个线程是应该通过还是需要等待。比如当使用锁或信号量时,获取操作的含义就很直观,即「获取的是锁或者许可」。AQS负责管理同步器类中的状态(synchronization state),它管理了一个整数状态信息,用于表示任意状态。例如,ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的可被获取的许可数量。
对照我们在前文中引用的GoF对模板模式的定义,这里提到的「锁和同步器的框架」即对应「算法的骨架」,「灵活的标准」即对应「重定义该算法的某些特定步骤」;而synchronization state(以下简称「同步状态」)可以说是这两者之间交互的桥梁。Doug Lea对AQS框架的「获取操作」和「释放操作」的算法骨架的基本思路描述如下方伪代码所示。可以看到,在获取和释放操作中,对同步状态的判断和更新,是算法骨架中可被各类同步器灵活扩展的部分;而相应的对操作线程的入队、阻塞、唤起和出队操作,则是算法骨架中被各类同步器所复用的部分。
// 「获取操作」伪代码
While(synchronization state does not allow acquire) { // * 骨架扩展点
enqueue current thread if not already queued; // 线程结点入队
possibly block current thread; // 阻塞当前线程
}
dequeue current thread if it was queued; // 线程结点出队
// 「释放操作」伪代码
update synchronization state // * 骨架扩展点
if (state may permit a blocked thread to acquire) { // * 骨架扩展点
unblock one or more queued threads; // 唤起被阻塞的线程
}
下面我们以大家熟悉的ReentrantLock为例具体分析。ReentrantLock实例内部维护了一个AQS的具体实现,用户的lock/unlock请求最终是借助AQS实例的acquire/release方法实现。同时,AQS实例在被构造时有两种选择:非公平性锁实现和公平性锁实现。我们来看下AQS算法骨架部分的代码:
// AQS acquire/release 操作算法骨架代码
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 同步状态 synchronization state
private volatile int state;
// 排他式「获取操作」
public final void acquire(int arg) {
if (!tryAcquire(arg) && // * 骨架扩展点
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 线程结点入队
selfInterrupt();
}
// 针对已入队线程结点的排他式「获取操作」
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // * 骨架扩展点
setHead(node); // 线程结点出队(队列head为哑结点)
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 阻塞当前线程
i