Java 后端每日学习 · 2026-09-11

Day 18

Map 阶段闯关变式:多仓库存预占过期批处理索引的契约、聚合、编码与源码解释。

请通过本地启动器或已部署的学习地址访问;页面加载后即使服务暂时中断,浏览器草稿与下载备份仍可使用。

Java 后端每日学习 · Day 18 · 2026-09-11

打开今日互动学习页:汇总五个课程章节,支持代码复制、复盘作答、浏览器草稿和本地 Markdown 保存。网页作答优先、聊天提交补充。

今日主题

Map 阶段闯关变式:多仓库存预占过期批处理索引的契约、聚合、编码与源码解释。

方向元数据

字段
directionId java-data-structures
directionSession 16
curriculumItemId challenge.map
lessonMode checkpoint
节点进入前状态 covered_unverified
完成本课后的预期状态 covered_unverified;生成第六个变式不等于闯关通过
当前方向状态 active

查看全局 Java 后端知识地图。固定同步助手对最近完整课程 Day 17 的拉取结果为 remote_missing,服务器与本地均没有 2026-09-10/04-复盘作答.md,当前任务也没有能明确映射到 Day 17 题号和问题源哈希的答案。因此答案来源为 none,不评分、不推断薄弱项或红线;challenge.map 保持 covered_unverified,今天继续在 Map 模块完成新的综合变式。

今天把重心从“排序字段变化后的重排”转到“有界批量状态迁移与聚合扣减”:库存预占事实不能因缓存淘汰或到期索引删除而消失;确认和过期必须同步处理到期树、每仓 SKU 预占量、状态计数与最近访问缓存,并在聚合量归零时删除键。批处理上限、严格时间边界和失败恢复要求大大把单个 Map API 与完整业务原子性分开。

可验证目标

  1. 能从仓库、订单、行号组成稳定事实身份,并区分库存事实、到期顺序、SKU 聚合、状态计数与诊断缓存的职责。
  2. 能逐步推演确认、严格上界过期批次、批量上限、聚合扣减至零删键、缓存重新进入以及旧快照保持不变。
  3. 能完成 Java 21 必做编码,编译并产生规定输出,同时守住复合键相等性、TreeMap 全序、半开范围和多索引一致性。
  4. 能沿 OpenJDK jdk-21+35 的入口、字段和主调用链解释 HashMap 条件更新与聚合、TreeMap 导航删除、LinkedHashMap 访问顺序和 EnumMap 槽位。
  5. 能说明批量循环、fail-fast、单 Map 条件 API 与覆盖全部事实及投影的事务或恢复边界之间的区别。

60~75 分钟学习顺序

  1. 昨日复盘:Day 17 五题完整参考答案与限流策略版本切换索引完整实现(约 12~14 分钟)。
  2. 核心讲解:用库存预占到期批次串联事实、排序、聚合、缓存和一致性边界(约 31~32 分钟)。
  3. 编码练习:完成到期快照、确认与带上限的严格过期批处理;这是闯关通过的必交编码(约 18~20 分钟)。
  4. 复盘问题:按四个固定维度提交 Map 阶段闯关答案(约 8~9 分钟)。

总预计用时约 69~75 分钟。今天只验证 Map 模块,不提前进入 Set,也不把昨日参考答案算成大大的作答证据。

方向进度

  • 课前与课程完成后的标准课覆盖均为 10/3528.57%);今天是同一稳定闯关项的第六个变式,不重复计数。
  • 知识点覆盖保持 9/27;已评估 0、已掌握 0、待补强 0
  • 源码型知识点覆盖保持 4/9,已掌握仍为 0/9
  • Map 闯关保持“已出题待验证”:闯关已开始 1/6、通过 0/6;综合项目 0/1、方向总测 0/1
  • 无作答只表示尚未验证,不等于失败;也不能据此放行到 Set。

课程完成标准

  • 完成四部分学习,并能说明预占事实为何不随缓存或到期投影删除,以及 SKU 聚合归零为何要清理键。
  • 将编码练习的 3 个 TODO 补全,以 javac --release 21 编译并运行,保存完整代码与规定输出。
  • 回答复盘页全部闯关题;答案必须映射到当天题号和问题源,不能用本文或昨日参考答案代替。
  • 不依赖 HashMap 顺序,不破坏 equals/hashCodeComparator 契约,不混淆活视图、只读包装与快照,不把 fail-fast 或单 Map 原子 API 当成跨 Map 事务。

闯关放行条件

  • 总分至少 80/100;固定维度为:契约与选型 25、复杂度与状态推演 25、编码 30、源码讲解 20。
  • Java 21 编码必须成功编译、运行且产生预期结果;只写思路、缺少完整代码或输出不符均不能通过。
  • 六类红线均未命中。达到分数但编码失败或命中红线,仍视为未通过。
  • 今日课程生成后,challenge.map 仍只记 covered_unverified。只有后续取得完整、有效且满足全部条件的作答证据,才可记为 mastered 并进入 Set。

固定版本与官方资料

条件式下节预告

  • 若仍没有完整有效的闯关答案:challenge.map 保持未通过,下一完整学习日继续用新数据和约束完成 Map 综合练习或闯关变式,绝不进入 Set。
  • 若出现部分有效答案且未暴露明确错误:逐题点评,challenge.map 记为 practicing,保留验证债务并继续 Map 闯关,不虚构总分。
  • 若完整答案低于 80、Java 21 编码不通过,或任何有效答案暴露明确错误或红线:challenge.map 与明确薄弱的 1~3 个知识点记为 needs_review,方向记为 needs_review;下一课优先更换数据、约束和场景补强,再重闯。
  • 只有完整答案达到至少 80、编码编译运行符合预期且无红线:challenge.map 才记为 mastered,并依据充分证据更新相关 Map 知识点;下一课才可进入 ds.set.contract-algebra

返回今日索引

昨日复盘:多租户限流策略版本切换 Map 闯关核对

复盘对象:Day 17(2026-09-10),评估项 challenge.map。建议用时:12~14 分钟。先用 1~2 分钟确认第 1 节证据与推进边界,遮住答案用 5~6 分钟口述第 2 节,再用约 6 分钟核对第 3 节三个 TODO、规定输出和复杂度。下文全部是教学参考答案,不构成大大的作答、分数、闯关通过或掌握证据

1. 作答证据、逐题评分与推进结论

  • 固定同步助手拉取 Day 17 作答的结果为 remote_missing,服务器上没有 2026-09-10/04-复盘作答.md;本地也没有该文件。
  • Day 17 问题源哈希:sha256:3f2bd372377f603188f5b167aa98c9c33c03df8a0f4c14464794e74cde9e0476
  • 当前任务中没有能明确映射到 Day 17、题号和上述问题源哈希的聊天答案。
  • 答案来源:none
  • 五题均为未作答、未评分;没有有效证据可给分,缺答也不能直接记为 0 分。
  • 总分:不足以评分
  • 红线:未知。没有证据证明命中,也没有证据证明已避开六类红线。
  • 薄弱知识 ID:无法从无作答证据中确定,因此不虚构错题或针对性薄弱项。
  • 状态变化:challenge.map 保持 covered_unverified,Map 闯关仍为未通过;方向保持 active
  • 推进边界:今天继续 challenge.map 的第六个变式“多仓库存预占过期批处理索引”,不得进入 Set。阅读、复制或运行下方参考答案均不会改变状态。
题号 固定维度 分值 大大的有效答案 点评与得分
1 契约与选型 25 无法核对版本身份、排序全序、缓存淘汰、重排投影与快照所有权,未评分
2 复杂度与状态推演 12 无法核对重排成本、HashMap 退化、LinkedList 对比与 fail-fast 边界,未评分
3 复杂度与状态推演 13 无法核对重排、严格上界、缓存重新进入、活视图与稳定快照,未评分
4 编码 30 未提交完整 Java 21 代码、编译证据和规定输出,编码维度未评分,闯关不可放行
5 源码讲解 20 无法核对固定版本入口、字段、主路径和可观察合同,未评分
合计 100 证据不足 不足以评分

无作答时可先独立重做第 1~3 题,再在不看参考实现的情况下完成三个 TODO,最后用第 5 题的源码路径反向解释运行结果。这只是通用复习路线,不表示已经发现大大的确定错误。

2. Day 17 五道闯关题完整参考答案

第 1 题:稳定版本身份、排序投影与所有权

  1. 事实主表使用 HashMap<PolicyKey, RatePolicy>;不可变 record PolicyKey(tenantId,apiCode,version) 的三个组件共同参与 equals/hashCode。键和值都拒绝 null,租户和 API 还拒绝空白、版本必须为正,因此本题 get()==null 可以无歧义地表示缺失。
  2. activateMinuteprioritypermitsPerMinutestate 会随调度或激活而改变,只能放在不可变 value 的新版本中;version 是策略身份的一部分,不能因重排而改变事实键。
  3. 调度索引使用 TreeMap<ActivationKey,PolicyKey>,依次按分钟升序、优先级降序、租户、API、版本升序比较;合法键上只有五个组件全相等才 compareTo==0,不同策略不会在树中互相覆盖。
  4. 状态计数使用 EnumMap<PolicyState,Integer>;封闭枚举键域紧凑且按枚举声明顺序遇见,计数降为零时删除槽位。
  5. 预览缓存使用容量为 3 的 access-order LinkedHashMap;命中、已有键更新和插入都会改变热度,超限只淘汰最老缓存项,不删除事实或调度项。
  6. 重排先保存并条件删除旧 ActivationKey,再以新不可变 value 条件替换事实、插入新排序键并更新缓存;状态未变,所以计数不变。四张普通 Map 的连续写入仍需外层共同锁、单线程事件所有者、不可变聚合交换或事务保护。
  7. subMap/headMap 是背靠根 TreeMap 的活视图,删除会写穿;对外结果应立即投影为独立、不可修改的字符串快照。只读包装会随底层容器变化,不等于快照。
  8. 激活业务顺序只由 ActivationKey 的比较合同决定,不能借用 HashMap/HashSet 的偶然遇见顺序;WeakHashMap 会让事实寿命受键可达性和 GC 影响,IdentityHashMap== 区分反序列化得到的等值键,都不适合持久策略事实。

byKey 是唯一事实来源;调度树、状态计数和预览缓存都是可核对、可从事实重建的派生投影。缓存未命中只说明当前不热,绝不表示策略事实不存在。

第 2 题:重排包含两次树修改,复杂度不是一句 O(1)

设事实表有 n 个策略版本,时间窗命中 k 个:

  • byKey 的精确查询、条件新增和条件替换的期望时间为 O(1),但这不是每次操作的严格保证。
  • 提交包含 HashMap 条件新增、TreeMap 插入、EnumMap 计数和缓存写入,由树插入主导为 O(log n)。
  • 重排包含旧排序键删除和新排序键插入,两项各为 O(log n),再加事实替换及缓存写入的期望 O(1),合并后仍为 O(log n),而不是只做一次 HashMap value 替换。
  • 首项激活需要边界导航和 TreeMap 删除,再更新事实、计数与缓存,成功分支为 O(log n)。
  • 半开窗口的边界定位加 k 项投影为 O(log n + k),返回稳定字符串快照额外占用 O(k) 空间。
  • EnumMap 键域固定,单次计数读写按 O(1) 理解;容量固定的预览缓存命中、移动、插入和一次最老项淘汰为常数或期望 O(1),空间上限为 O(1)。事实表与调度树的总空间为 O(n)。

在固定 OpenJDK jdk-21+35 中,HashMap 新增超过负载阈值时会执行 resize 并处理旧 table 中的节点;发生碰撞时还可能沿链或树继续定位。碰撞桶达到树化条件时,table 容量小于 MIN_TREEIFY_CAPACITY=64 会优先扩容而非树化,容量足够才可能转为树,因此“正常分布下期望 O(1)”不能说成“每次严格 O(1)”。

若改用 LinkedList 且事先不知道候选节点位置,必须先线性查找 O(n);拿到节点后链接删除即便是 O(1),完整的“查找并删除”仍为 O(n)。TreeMap 则利用比较树以 O(log n) 导航和删除。迭代器 fail-fast 只尽力检测部分结构修改,不提供互斥、内存可见性、复合操作原子性或一致快照,所以不能被当作线程安全机制。

第 3 题:重排会改变树位置和缓存热度,但不会改变事实身份

用 A、B、C、D 表示:

  • A:tenant-a/orders/v1@640#P2
  • B:tenant-b/orders/v1@620#P1
  • C:tenant-a/pay/v2@620#P4
  • D:tenant-c/report/v1@660#P3

预览缓存容量为 2,调度树和缓存逐步变化如下:

动作 调度树顺序 access-order 缓存(左旧右新)
提交 A [A@640#P2] [A]
提交 B [B@620#P1, A@640#P2] [A,B]
提交 C [C@620#P4, B@620#P1, A@640#P2] [B,C],A 被淘汰
A 重排为 630#P5 [C@620#P4, B@620#P1, A@630#P5] [C,A],A 重新进入并淘汰 B
提交 D [C@620#P4, B@620#P1, A@630#P5, D@660#P3] [A,D],淘汰 C
activateNext(630) [B@620#P1, A@630#P5, D@660#P3] [D,C],C 重新进入并淘汰 A

同一分钟 620 内优先级降序,所以 P4 的 C 排在 P1 的 B 前。严格上界 630 只接纳分钟 <630 的候选,重排后恰在 630 的 A 被排除,因此激活的是 C。最终计数为 SCHEDULED=3ACTIVE=1;C 的事实仍在 byKey 中,但 value 已换成 ACTIVE

A 的事实键仍是 tenant-a/orders/v1,新 value 保留原限额并变为 activateMinute=630priority=5state=SCHEDULED;旧 A@640#P2 排序键已经删除。先取得的 [620,641) 活视图在激活后当前内容为 [B@620#P1, A@630#P5],因为删除 C 会写穿根树;此前固化的不可变字符串快照仍为 [C@620#P4:SCHEDULED, B@620#P1:SCHEDULED, A@630#P5:SCHEDULED],不会被后续事实替换改写。

“删旧排序键、换事实、建新排序键、更新缓存”横跨多张 Map。题设单线程只避免线程交错,不能抵御进程在中途退出;单个 replaceremove(key,value) 或 ConcurrentHashMap 的单键原子 API 也不会自动把其他 Map 纳入同一事务。

第 4 题:三个 TODO 的最小完整实现策略

  • 重排在任何写入前校验新分钟和优先级;缺失或已激活直接返回 false。成功路径保存并条件删除旧排序键,用新 record 条件替换事实,再插入新键并让缓存项重新进入或升温,状态计数不变。
  • 快照使用两个边界键和四参数 subMap 表达 [fromInclusive,toExclusive),按活视图顺序核对事实状态及完整排序键,最后以 Stream.toList() 固化为不可修改的字符串列表。
  • 激活使用严格上界 headMap 选首项,核对事实与投影后以 ACTIVE record 条件替换事实,从活视图删除旧调度项,再迁移计数和更新缓存。
  • 重复提交由事实表的 putIfAbsent 立即截断,不能修改调度、计数或缓存热度。
  • 被缓存淘汰的策略仍在事实表中,重排或激活写入最新 value 时可以重新进入并触发另一次缓存淘汰。
  • 所有成功步骤仍依赖题设的单线程顺序;生产环境必须给整段状态转换提供共同原子或可恢复边界。

第 3 节给出与 Day 17 起始代码一致的完整 Java 21 参考实现和规定输出。

第 5 题:固定源码路径要落回本题的可观察结果

固定版本为 OpenJDK jdk-21+35

问题 公开入口 关键字段 最多四个箭头节点的主路径 可观察结果或扩展点
HashMap 精确查询、新键写入、同键 value 替换与删除 getputreplaceremove tablesizemodCount get→getNode→桶首/链/树匹配put→putVal→替换或新增remove→removeNode→摘链/树删除 等值键替换 value 不增加 size;新节点和结构删除影响结构;遇见顺序不属于合同
阈值、扩容拆分、树化容量条件和迭代检测 put、集合视图 iterator thresholdloadFactortablemodCountexpectedModCount putVal→超阈值→resize→高低链拆分treeifyBin→容量判断→扩容或树化nextNode→比较 modCount table 容量不足 64 时长桶优先扩容;fail-fast 仅尽力发现结构修改,不提供线程安全
LinkedHashMap access-order 命中、已有键更新与最老项钩子 getput headtailaccessOrder get→afterNodeAccess→节点移尾putVal→afterNodeAccess/afterNodeInsertion→removeEldestEntry 命中和已有键更新改变热度;插入后子类可淘汰最老缓存项,但不会删除另一张事实表
TreeMap 比较定位、删除旧键、插入新键及范围视图 putremovesubMapheadMap rootcomparatorsizemodCount put→逐节点 compare→替换或插入平衡remove→getEntry→deleteEntrysubMap/headMap→NavigableSubMap→边界导航/写穿 compare==0 即同一排序键;旧键删除后新键按完整全序换位,视图按范围遇见并写穿根树
EnumMap 枚举槽位与计数迁移 putgetmerge keyTypekeyUniversevalssize put→typeCheck→key.ordinal→写入 vals 槽位 状态按枚举声明顺序遇见;同一常量定位同一槽位,计数降零后由业务代码删键

源码实现只能解释固定版本现象,不能升级为公共业务合同:不能猜红黑树具体形状,不能把内部阈值写成 Map API 的永久保证,也不能把 modCount、fail-fast、普通 Map 或 ConcurrentHashMap 的单键能力扩张成业务排序、线程安全或跨键、跨 Map 事务。可核对 HashMap.javaLinkedHashMap.javaTreeMap.javaEnumMap.java

3. Day 17 主练习完整可运行参考实现

下面只补全原起始代码的三个 TODO,不改变测试数据,不添加第三方依赖、日志、无恢复意义的 try/catch 或多余公开 API。嵌套类型和方法保持最小可见性;关键注释只解释事实与派生投影的一致性边界。

import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;

public final class RatePolicySwitchChallenge {
    private enum PolicyState {
        SCHEDULED,
        ACTIVE
    }

    private record PolicyKey(
            String tenantId,
            String apiCode,
            long version) {
        PolicyKey {
            Objects.requireNonNull(tenantId, "tenantId");
            Objects.requireNonNull(apiCode, "apiCode");
            if (tenantId.isBlank() || apiCode.isBlank() || version <= 0) {
                throw new IllegalArgumentException("invalid policy key");
            }
        }

        String label() {
            return tenantId + "/" + apiCode + "/v" + version;
        }
    }

    private record RatePolicy(
            PolicyKey key,
            long activateMinute,
            int priority,
            int permitsPerMinute,
            PolicyState state) {
        RatePolicy {
            Objects.requireNonNull(key, "key");
            Objects.requireNonNull(state, "state");
            if (activateMinute < 0
                    || priority < 1
                    || priority > 9
                    || permitsPerMinute <= 0) {
                throw new IllegalArgumentException("invalid rate policy");
            }
        }

        RatePolicy rescheduled(long newMinute, int newPriority) {
            return new RatePolicy(
                    key,
                    newMinute,
                    newPriority,
                    permitsPerMinute,
                    state);
        }

        RatePolicy activated() {
            return new RatePolicy(
                    key,
                    activateMinute,
                    priority,
                    permitsPerMinute,
                    PolicyState.ACTIVE);
        }

        String snapshotLine() {
            return key.label() + "@" + activateMinute
                    + "#P" + priority + ":" + state
                    + ":L" + permitsPerMinute;
        }
    }

    private record ActivationKey(
            long activateMinute,
            int priority,
            String tenantId,
            String apiCode,
            long version) implements Comparable<ActivationKey> {
        static ActivationKey from(RatePolicy policy) {
            return new ActivationKey(
                    policy.activateMinute(),
                    policy.priority(),
                    policy.key().tenantId(),
                    policy.key().apiCode(),
                    policy.key().version());
        }

        static ActivationKey boundary(long minute) {
            return new ActivationKey(
                    minute,
                    Integer.MAX_VALUE,
                    "",
                    "",
                    Long.MIN_VALUE);
        }

        @Override
        public int compareTo(ActivationKey other) {
            int byMinute = Long.compare(activateMinute, other.activateMinute);
            if (byMinute != 0) {
                return byMinute;
            }
            int byPriority = Integer.compare(other.priority, priority);
            if (byPriority != 0) {
                return byPriority;
            }
            int byTenant = tenantId.compareTo(other.tenantId);
            if (byTenant != 0) {
                return byTenant;
            }
            int byApi = apiCode.compareTo(other.apiCode);
            return byApi != 0
                    ? byApi
                    : Long.compare(version, other.version);
        }
    }

    private static final class PolicyIndex {
        private static final int PREVIEW_LIMIT = 3;

        private final Map<PolicyKey, RatePolicy> byKey = new HashMap<>();
        private final NavigableMap<ActivationKey, PolicyKey>
                activationSchedule = new TreeMap<>();
        private final EnumMap<PolicyState, Integer> stateCounts =
                new EnumMap<>(PolicyState.class);
        private final LinkedHashMap<PolicyKey, RatePolicy> previewCache =
                new LinkedHashMap<>(4, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(
                            Map.Entry<PolicyKey, RatePolicy> eldest) {
                        return size() > PREVIEW_LIMIT;
                    }
                };

        private boolean submit(RatePolicy policy) {
            Objects.requireNonNull(policy, "policy");
            if (policy.state() != PolicyState.SCHEDULED) {
                throw new IllegalArgumentException(
                        "new policy must be scheduled");
            }
            if (byKey.putIfAbsent(policy.key(), policy) != null) {
                return false;
            }

            // 事实新增后才建立派生投影;生产环境仍需外层一致性边界。
            activationSchedule.put(ActivationKey.from(policy), policy.key());
            stateCounts.merge(PolicyState.SCHEDULED, 1, Integer::sum);
            previewCache.put(policy.key(), policy);
            return true;
        }

        private boolean reschedule(
                PolicyKey key,
                long newMinute,
                int newPriority) {
            Objects.requireNonNull(key, "key");
            if (newMinute < 0 || newPriority < 1 || newPriority > 9) {
                throw new IllegalArgumentException("invalid new schedule");
            }
            RatePolicy current = byKey.get(key);
            if (current == null || current.state() != PolicyState.SCHEDULED) {
                return false;
            }

            ActivationKey oldKey = ActivationKey.from(current);
            if (!activationSchedule.remove(oldKey, key)) {
                throw new IllegalStateException(
                        "activation schedule is inconsistent with fact map");
            }

            RatePolicy updated = current.rescheduled(newMinute, newPriority);
            if (!byKey.replace(key, current, updated)) {
                throw new IllegalStateException(
                        "fact changed while rescheduling policy");
            }
            // 旧投影已清理;新事实、新排序键和缓存须由同一外层边界保护。
            activationSchedule.put(ActivationKey.from(updated), key);
            previewCache.put(key, updated);
            return true;
        }

        private List<String> activationSnapshot(
                long fromInclusive,
                long toExclusive) {
            if (fromInclusive > toExclusive) {
                throw new IllegalArgumentException(
                        "fromInclusive must be <= toExclusive");
            }
            NavigableMap<ActivationKey, PolicyKey> window =
                    activationSchedule.subMap(
                            ActivationKey.boundary(fromInclusive), true,
                            ActivationKey.boundary(toExclusive), false);
            return window.entrySet().stream()
                    .map(entry -> {
                        RatePolicy policy = byKey.get(entry.getValue());
                        if (policy == null
                                || policy.state() != PolicyState.SCHEDULED
                                || !ActivationKey.from(policy)
                                        .equals(entry.getKey())) {
                            throw new IllegalStateException(
                                    "activation schedule is inconsistent "
                                            + "with fact map");
                        }
                        return policy.snapshotLine();
                    })
                    .toList();
        }

        private String activateNext(long beforeExclusive) {
            if (beforeExclusive < 0) {
                throw new IllegalArgumentException(
                        "beforeExclusive must be >= 0");
            }
            NavigableMap<ActivationKey, PolicyKey> candidates =
                    activationSchedule.headMap(
                            ActivationKey.boundary(beforeExclusive), false);
            Map.Entry<ActivationKey, PolicyKey> first =
                    candidates.firstEntry();
            if (first == null) {
                return "NONE";
            }

            PolicyKey key = first.getValue();
            RatePolicy current = byKey.get(key);
            Integer scheduledCount = stateCounts.get(PolicyState.SCHEDULED);
            if (current == null
                    || current.state() != PolicyState.SCHEDULED
                    || !ActivationKey.from(current).equals(first.getKey())
                    || scheduledCount == null
                    || scheduledCount <= 0) {
                throw new IllegalStateException(
                        "activation schedule is inconsistent with fact map");
            }

            RatePolicy activated = current.activated();
            if (!byKey.replace(key, current, activated)) {
                throw new IllegalStateException(
                        "fact changed while activating policy");
            }
            // 子视图删除写穿根树,随后同步计数与最新缓存 value。
            if (!candidates.remove(first.getKey(), key)) {
                throw new IllegalStateException(
                        "activation schedule changed while activating policy");
            }
            int remaining = stateCounts.merge(
                    PolicyState.SCHEDULED, -1, Integer::sum);
            if (remaining == 0) {
                stateCounts.remove(PolicyState.SCHEDULED);
            }
            stateCounts.merge(PolicyState.ACTIVE, 1, Integer::sum);
            previewCache.put(key, activated);
            return key.label();
        }

        private String policyLine(PolicyKey key) {
            RatePolicy policy = byKey.get(Objects.requireNonNull(key, "key"));
            return policy == null ? "MISSING" : policy.snapshotLine();
        }

        private String previewState(PolicyKey key) {
            RatePolicy policy = previewCache.get(
                    Objects.requireNonNull(key, "key"));
            return policy == null ? "MISS" : policy.state().name();
        }

        private List<String> scheduleOrder() {
            return activationSchedule.entrySet().stream()
                    .map(entry -> entry.getValue().label()
                            + "@" + entry.getKey().activateMinute()
                            + "#P" + entry.getKey().priority())
                    .toList();
        }

        private List<String> previewOrder() {
            return previewCache.keySet().stream()
                    .map(PolicyKey::label)
                    .toList();
        }

        private List<String> countSummary() {
            return stateCounts.entrySet().stream()
                    .map(entry -> entry.getKey() + "=" + entry.getValue())
                    .toList();
        }
    }

    public static void main(String[] args) {
        PolicyKey aKey = new PolicyKey("tenant-a", "search", 1);
        PolicyKey bKey = new PolicyKey("tenant-b", "search", 1);
        PolicyKey cKey = new PolicyKey("tenant-a", "pay", 2);
        PolicyKey dKey = new PolicyKey("tenant-c", "report", 1);

        RatePolicy a = new RatePolicy(
                aKey, 520, 2, 100, PolicyState.SCHEDULED);
        RatePolicy b = new RatePolicy(
                bKey, 500, 1, 80, PolicyState.SCHEDULED);
        RatePolicy c = new RatePolicy(
                cKey, 500, 5, 40, PolicyState.SCHEDULED);
        RatePolicy d = new RatePolicy(
                dKey, 540, 3, 200, PolicyState.SCHEDULED);

        PolicyIndex index = new PolicyIndex();
        System.out.println("SUBMIT A=" + index.submit(a));
        System.out.println("SUBMIT B=" + index.submit(b));
        System.out.println("SUBMIT C=" + index.submit(c));
        System.out.println("SUBMIT D=" + index.submit(d));
        System.out.println("SUBMIT duplicate-A=" + index.submit(a));
        System.out.println("PREVIEW before-reschedule="
                + index.previewOrder());

        System.out.println("RESCHEDULE A="
                + index.reschedule(aKey, 510, 4));
        System.out.println("POLICY A=" + index.policyLine(aKey));
        System.out.println("PREVIEW after-reschedule="
                + index.previewOrder());

        List<String> beforeActivation = index.activationSnapshot(500, 521);
        System.out.println("WINDOW before=" + beforeActivation);
        System.out.println("COUNTS before=" + index.countSummary());
        System.out.println("ACTIVATE before-500="
                + index.activateNext(500));
        System.out.println("ACTIVATE before-510="
                + index.activateNext(510));
        System.out.println("COUNTS after=" + index.countSummary());
        System.out.println("SCHEDULE after=" + index.scheduleOrder());
        System.out.println("PREVIEW after=" + index.previewOrder());
        System.out.println("SNAPSHOT unchanged=" + beforeActivation);
    }
}

三个待补位置的关键步骤

  1. reschedule 先校验全部外部输入和当前状态,再从旧事实生成旧 ActivationKey;只有“旧排序键 + 事实键”条件删除成功才构造新 record、条件替换事实并建立新投影。重排没有状态迁移,不能改 SCHEDULED 计数。
  2. activationSnapshot 用排在同分钟所有合法业务键之前的边界键表达半开区间。下界包含边界会收进下界整分钟,上界排除边界会排除上界整分钟;逐项核对事实和排序键后生成字符串,toList() 返回不可修改列表。
  3. activateNext 用严格上界活视图取得第一项,先核对事实状态、完整排序键和计数,再以新 ACTIVE record 条件替换事实。对子视图的删除写穿根树,随后把计数从 SCHEDULED 迁移到 ACTIVE,并用新 value 更新缓存。

精确编译与规定输出

javac --release 21 RatePolicySwitchChallenge.java
java RatePolicySwitchChallenge

运行输出必须为:

SUBMIT A=true
SUBMIT B=true
SUBMIT C=true
SUBMIT D=true
SUBMIT duplicate-A=false
PREVIEW before-reschedule=[tenant-b/search/v1, tenant-a/pay/v2, tenant-c/report/v1]
RESCHEDULE A=true
POLICY A=tenant-a/search/v1@510#P4:SCHEDULED:L100
PREVIEW after-reschedule=[tenant-a/pay/v2, tenant-c/report/v1, tenant-a/search/v1]
WINDOW before=[tenant-a/pay/v2@500#P5:SCHEDULED:L40, tenant-b/search/v1@500#P1:SCHEDULED:L80, tenant-a/search/v1@510#P4:SCHEDULED:L100]
COUNTS before=[SCHEDULED=4]
ACTIVATE before-500=NONE
ACTIVATE before-510=tenant-a/pay/v2
COUNTS after=[SCHEDULED=3, ACTIVE=1]
SCHEDULE after=[tenant-b/search/v1@500#P1, tenant-a/search/v1@510#P4, tenant-c/report/v1@540#P3]
PREVIEW after=[tenant-c/report/v1, tenant-a/search/v1, tenant-a/pay/v2]
SNAPSHOT unchanged=[tenant-a/pay/v2@500#P5:SCHEDULED:L40, tenant-b/search/v1@500#P1:SCHEDULED:L80, tenant-a/search/v1@510#P4:SCHEDULED:L100]

第四次提交只让容量为 3 的缓存淘汰 A,事实表和调度树仍保留 A;重复提交在 putIfAbsent 失败后立即返回,不能让 A 回到缓存。重排把 A 的旧 520#P2 投影删除并建立 510#P4 新投影,状态没变所以计数仍为 4。同为 500 时 P5 的 C 排在 P1 的 B 前;严格上界 510 排除分钟恰为 510 的 A,因而激活 C。旧快照已经复制为字符串,即使 C 的事实变为 ACTIVE,仍保留复制时的 SCHEDULED

时间与空间复杂度

  • submit 的事实表条件写入期望 O(1),调度树插入 O(log n),计数与缓存更新为常数或期望 O(1),整次为 O(log n)。
  • reschedule 对调度树删除一次、插入一次,事实条件替换和缓存更新为期望 O(1),整次为 O(log n);它不会因两次树操作变成 O(2 log n) 之外的新渐进阶。
  • activationSnapshot 的边界定位与 k 项核对、投影为 O(log n + k),稳定字符串快照额外空间为 O(k)。
  • activateNext 的边界导航与树删除为 O(log n),事实替换、计数迁移及缓存更新为常数或期望 O(1),整次为 O(log n)。
  • policyLine 和预览命中的期望时间为 O(1);scheduleOrder 遍历剩余候选为 O(n);固定状态域和固定容量缓存按 O(1) 空间理解。
  • 事实表与调度树总空间为 O(n),状态表和有界缓存为 O(1)。这些复杂度结论不代表最坏碰撞、并发安全或跨 Map 事务保证。

边界复核

  • 同租户同 API 的不同版本、不同租户的同 API/版本可以并存;重排只产生新 value,不改变事实键。
  • 完全相同复合键重复提交返回 false,且调度、计数和缓存热度都不改变。
  • key==null、负分钟、优先级越界必须在任何 Map 修改前失败;缺失或 ACTIVE 策略重排返回 false 且无副作用。
  • 重排成功后旧排序键不存在,新排序键按完整五字段全序定位,SCHEDULED 计数不变。
  • [510,510) 返回空快照;下界大于上界在读取业务状态前失败;快照不可增删,后续事实替换不改旧字符串。
  • activateNext(500) 返回 NONE;严格上界 510 排除重排后的 A,只允许更早的 C/B 参与比较。
  • 缓存淘汰不删除事实或候选;重排或激活已淘汰项会让它重新进入并可能淘汰当前最老项。
  • 生产环境必须让共同锁、单线程事件所有权、不可变聚合交换或事务覆盖完整多索引转换,并准备从事实重建派生投影。

4. 今日变式前的最短自检

  • 能否说明版本为何属于稳定事实身份,而分钟、优先级、限额和状态为何属于可替换 value?
  • 能否从分钟、优先级、租户、API、版本推出全序,不依赖 HashMap 或 HashSet 的遇见顺序?
  • 能否逐步推演容量为 2 的 access-order 缓存,包括 A 和 C 被淘汰后重新进入的两次变化?
  • 能否区分严格上界 630、[620,641) 活视图与固化字符串快照在激活后的内容?
  • 能否解释重排为何要删旧 TreeMap 键、插新键,却仍保持 O(log n) 且不迁移状态计数?
  • 能否明确承认没有有效作答,所以 challenge.map 仍是 covered_unverified,今天必须继续 Map 而不能进入 Set?

如有一项含糊,先换一组仓库、订单行、预占到期时间与批处理上限重新推演,再开始今天的库存预占变式。这里的完整参考答案始终只用于教学核对,不会被记录为大大的答案证据。

返回今日索引

返回今日索引

核心讲解:多仓库存预占过期批处理中的 Map 合同与聚合

Day 18|directionSession 16|challenge.mapcheckpoint。建议用时 31~32 分钟。Day 17 的远端拉取结果为 remote_missing,本地与当前聊天也没有有效作答,因此 challenge.map 保持 covered_unverified,方向保持 active;本课继续取证,阅读参考过程本身不构成闯关通过。

1. 为什么需要:一笔预占结束,会同时影响事实、日历和汇总量

电商库存服务通常先“预占”再确认扣减:订单在支付窗口内占住某仓某 SKU 的数量,超时则释放。平台需要按预占身份精确查询,按到期时间批量释放,快速回答“某仓某 SKU 当前共被占多少”,维护最近访问缓存和有限状态计数。单条释放已经跨越多张 Map;批量释放还增加两个不同边界:只处理 expireMinute < beforeExclusive 的严格时间上界,同时最多处理 limit 条,避免一次任务独占线程。

昨日主轴是可变排序字段重排;今天不修改到期时间,而是研究多个事实共同贡献一个聚合键时怎样成批退出。若只把事实改为 EXPIRED,却忘了删 TreeMap 候选或扣减聚合量,查询会互相矛盾;若数量减到零仍保留 sku -> 0,那么“缺键表示没有预占”的合同也被破坏。真正目标是从业务不变量推出每一步,而不是背三行 Map 调用。

Map 闯关仍没有满足“总分至少 80、Java 21 编码通过、无红线”的有效证据,所以本课不能越过门禁进入 Set。

2. 前置知识:九项 Map 能力各自解决哪一段问题

  • Map 定义键到值的映射。事实表禁止 null 键和值,facts.get(key)==null 才能唯一表示缺失;汇总表只存正数并在零时删键,所以缺键可解释为该仓 SKU 当前预占量为零。
  • ReservationKey(warehouseId, orderId, lineNo) 的三个不可变分量共同参与 equals/hashCode。不同仓的同订单行可以并存,数量、SKU、到期时间和状态属于 value,不能改写身份。
  • HashMap 在散列分布正常时精确访问期望 O(1),但碰撞、扩容、链与树桶使单次并非严格 O(1);它的遇见顺序从来不是到期顺序。
  • TreeMap 的 Comparator 同时决定排序和树键同一性。到期键按分钟升序,再按仓、订单、行号补足全序;若比较为 0,TreeMap 就把两项视为同一键。
  • putIfAbsent 适合事实去重;merge 可累加正向预占;compute 返回 null 会删除映射,适合“扣到零删键”,但映射函数不能顺手修改同一 Map,且必须先防止缺项、负数和溢出。
  • headMap/subMapentrySet/keySet/values 是背靠根容器的活视图。只读包装仍跟随根 Map;对外历史结果必须复制为稳定快照。
  • access-order LinkedHashMap 的成功命中和已有键更新会改变热度,容量 3 的淘汰只删除缓存投影,不能释放库存。
  • EnumMap 用枚举序号定位封闭状态槽,适合 HELD/CONFIRMED/EXPIRED;WeakHashMap 受 GC 影响,IdentityHashMap 用 ==,都不适合作为稳定订单事实表。

随堂检查 1:warehouseId 相同、orderId 相同但 lineNo 不同的两笔预占,能否共用一个事实键,靠 SKU 再区分?

**即时答案:**不能。订单行号属于业务身份;若事实表先覆盖,任何调度或 SKU 聚合都无法恢复丢失的那一行。SKU 是被预占的商品属性,不替代预占身份。

3. 定义:五张 Map 和四条可重算不变量

今天的结构职责是:

  1. HashMap<ReservationKey, Reservation> facts 是唯一事实来源,value 为不可变 record。
  2. TreeMap<ExpiryKey, ReservationKey> expirySchedule 只保存状态为 HELD 的候选;ExpiryKey 按到期分钟、仓、订单、行号全序排列。
  3. HashMap<StockKey, Integer> heldQuantityBySku 汇总每个“仓 + SKU”的 HELD 数量;StockKey(warehouseId, sku) 不跨仓混算,只允许正 value,零值不保留。
  4. 容量为 3 的 access-order LinkedHashMap<ReservationKey, Reservation> recentCache 保存最近成功读取或更新的事实副本,允许缺项。
  5. EnumMap<ReservationState, Integer> stateCounts 统计 HELD、CONFIRMED、EXPIRED,零计数项删除。

四条不变量可从 facts 重算:每个 HELD 事实恰好有一条匹配的到期项;每条到期项都能回查同一 HELD 事实;每个仓 SKU 汇总值等于所有匹配 HELD 事实数量之和;状态计数等于事实状态分组计数。缓存不参与等式,它只是可丢弃投影。confirm 与过期处理都会让一条事实退出 HELD 集合,因此都必须删除到期项、扣减汇总量、迁移状态计数并更新缓存;区别只是目标状态和候选选择方式。

4. 心智模型:主账、到期日历、分仓小计账和限额传送带

把 facts 想成逐行主账,身份写在封面,状态写在可替换的不可变页上。TreeMap 是到期日历,只把仍占库存的主账行挂到正确时间。heldQuantityBySku 是分仓小计账:多笔预占会共同加在同一格,任意一笔离开 HELD 就扣自己的数量,格子归零时整格擦除。recentCache 是只有三个座位的观察窗,淘汰不影响仓库。EnumMap 是状态牌。

批处理器像限额传送带:时间闸门先形成所有 < beforeExclusive 的候选前缀,maxItems 再只取其中最前的若干条。时间上界回答“有资格吗”,条数上限回答“本批最多做几个”,二者不能互换。maxItems=0 合法并返回空结果、所有 Map 不变;负数必须在读取业务 Map 前失败。处理每项前要用 ReservationKey 回查事实并核对状态、到期键、SKU 与数量;然后让事实、日历、小计账、状态牌和缓存一起完成一次转换。

随堂检查 2:候选有 3 条,分钟分别为 90、90、95;调用参数为 beforeExclusive=96, limit=2。第三条是否因为也已到期而必须本批处理?

**即时答案:**不处理。三条都通过严格时间上界,但批次上限只允许按 TreeMap 全序取前两条;第三条保留 HELD 和到期项,等待下一批。

5. 完整数值与状态推演:两条过期怎样改变共享聚合

设四笔 HELD 预占如下:

  • A:wh-east/o-701/1sku-red,数量 3,到期 100。
  • B:wh-west/o-702/1sku-red,数量 4,到期 90。
  • C:wh-east/o-703/2sku-red,数量 2,到期 90。
  • D:wh-east/o-704/1sku-blue,数量 5,到期 95。

登记完成后 facts 有 4 项,stateCounts={HELD=4}。到期全序为 [C@90,B@90,D@95,A@100]:C、B 同分钟时,wh-east 先于 wh-west;仓与订单也相同时再用行号区分,绝不借 HashMap 顺序。聚合为 {wh-east/sku-blue=5, wh-east/sku-red=5, wh-west/sku-red=4},其中 east/red 的 5 来自 A 的 3 加 C 的 2。

缓存容量为 3。依次写入 A、B、C 后为 [A,B,C];成功读取 A 得 [B,C,A];写入 D 淘汰 B,变成 [C,A,D]。B 缓存缺失,但 facts、到期日历和 west/red=4 都仍存在。

现在先取得严格上界 96 的导航活视图和字符串快照,二者都是 [C@90,B@90,D@95]。执行限额为 2 的过期批次:

  1. 首项 C 回查为 HELD,数量 2、键与日历一致。以新 record 把事实换成 EXPIRED,删除 C@90;east/red 从 5 减到 3,仍为正所以保留;计数暂为 HELD=3, EXPIRED=1。缓存更新已有 C,顺序从 [C,A,D] 变为 [A,D,C]
  2. 次项 B 同样通过核对。事实换成 EXPIRED 并删 B@90;west/red 从 4 减 4 得 0,因此删除聚合键而不是保存 0;计数变为 HELD=2, EXPIRED=2。B 原先不在缓存,插入尾部后超容量,淘汰 A,最终缓存为 [D,C,B]
  3. 已处理数量达到 2,停止。D@95 虽满足 <96,仍保持 HELD;A@100 因严格上界本来就不合格。

批后到期树为 [D@95,A@100],聚合为 {wh-east/sku-blue=5, wh-east/sku-red=3}。旧活视图现在只含 [D@95],因为删除 C、B 写穿根树;旧字符串快照仍保留 [C@90,B@90,D@95]。facts 中 C、B 是 EXPIRED,A、D 是 HELD。下一批用同一上界可继续处理 D,不会重复处理已退出日历的 C、B。

复杂度要按完整批次回答。设 facts 有 n 项,本批实际处理 b 项,b≤limit:TreeMap 边界定位 O(log n),每项回查 facts 期望 O(1)、树删除 O(log n)、两个 HashMap 和 EnumMap/缓存更新为常数或期望 O(1),合计 O(log n+b log n),常写作 O((b+1)log n)。生成含 k 项的快照为 O(log n+k) 时间、O(k) 空间。聚合表大小由实际“仓 + SKU”组合数决定,最坏仍为 O(n)。

注意错误中断语义:若第 2 项才发现聚合不足,前一项是否已经生效必须由接口合同定义。要求整批全有或全无时,可先验证本批、构造新的不可变聚合状态再一次交换,或使用数据库事务;允许逐项提交时,应返回明确的成功项和失败点并保证每项自身一致。练习中的单线程顺序只消除并发穿插,不会自动提供崩溃原子性。

6. 源码映射:从批量现象回到 OpenJDK 21 路径

固定基线为 Java 21、OpenJDK jdk-21+35。每组都按“问题 → 入口类/方法 → 关键字段 → 主调用链 → 扩展点 → 调试练习”阅读;私有结构只解释固定版本现象,不升级为公共业务合同。

6.1 HashMap:事实去重、聚合累加与零值删除

  • **问题:**等值 ReservationKey 为什么命中同一事实,compute 返回 null 又为何删除汇总项?
  • 入口类/方法:HashMap.get/put/putIfAbsent/compute/merge/remove关键字段:table、size、threshold、loadFactor、modCount
  • 主调用链:get→getNode→桶首/链/树匹配;写入进入 putVal,相等键替换、新键增加 size;重映射先定位旧节点,再根据新结果替换、添加或删除。
  • **扩展点:**容量 4、阈值 3、size 2 时,新增第 3 项不扩容;第 4 项使 size 超阈值后扩到 8。旧表同在桶 1、散列值相差旧容量位 4 的节点,在新表可分到桶 1 与 5。碰撞桶申请树化时,table 容量小于 MIN_TREEIFY_CAPACITY=64 会优先扩容。
  • **调试练习:**观察重复事实写入的返回值、size 与 modCount;再让某聚合 compute 得到 null,确认节点和键一起消失,而不是留下 value=0。

6.2 TreeMap:严格前缀、批次上限与安全删除

  • **问题:**怎样确定 <96 的有序候选,并只移除前两项而不破坏遍历?
  • 入口类/方法:TreeMap.headMap/firstEntry/pollFirstEntry/remove 及视图迭代器;关键字段:root、size、comparator、modCount 和子图边界。
  • **主调用链:**范围入口保存边界→导航定位首个合法节点→按后继迭代;删除按 comparator 找节点→红黑树删除与再平衡→修改结构计数。活视图删除会写穿根树。
  • **扩展点:**边界键必须排在该分钟所有合法业务键之前,排除它才能排除整个上界分钟;limit 在时间过滤之后计数。增强 for 中直接调用根 Map.remove 可能触发 fail-fast,应使用迭代器自身删除、反复取首项,或先复制有限键后再处理,并说明所有权与一致性。
  • **调试练习:**在 90、90、95、100 四项上观察严格上界 96 的视图;删前两项后比较活视图与先前复制列表,并检查第三项仍存在。

6.3 LinkedHashMap、EnumMap 与迭代检测

  • **问题:**为什么缓存命中会重排、枚举计数却按声明顺序输出,异常又为何不代表线程安全?
  • 入口类/方法:LinkedHashMap.get/putEnumMap.get/put/merge、集合视图的 iterator;**关键字段:**前者 head、tail、accessOrder,后者 keyType、keyUniverse、vals、size,迭代器持有 expectedModCount
  • **主调用链:**访问顺序表 get→getNode→afterNodeAccess→移到尾部;插入完成后 afterNodeInsertion→removeEldestEntry;EnumMap 校验 key 类型后以 ordinal 定位数组槽;迭代器比较期望与实际结构版本。
  • **扩展点:**缓存读取在 access-order 模式可能是结构修改;EnumMap 的 null key 被拒绝,内部哨兵可区分 null value,但本题仍禁止 null;fail-fast 只尽力暴露误用,不提供锁、可见性或一致快照。
  • **调试练习:**持有缓存迭代器后命中中间键,观察顺序与 modCount,再解释异常为什么不能替代共同并发边界。

固定源码直链:HashMap.javaLinkedHashMap.javaTreeMap.javaEnumMap.java。公开合同见 Java SE 21 MapHashMapNavigableMap

7. 真实后端应用:库存真相、幂等任务和故障恢复

生产系统通常让数据库库存明细与预占记录承担事实,内存 Map 只是单进程投影。定时任务按分片读取“到期且仍 HELD”的有限批次,通过状态条件更新实现幂等:只有成功把 HELD 改为 EXPIRED 的行才能释放对应库存。多仓不能只靠本地锁,因为同一任务可能在多个实例重试;常见做法是数据库事务、版本字段或带状态条件的更新,再用事务消息重建到期索引和聚合缓存。

如果使用纯内存模型,至少应由单线程事件所有者或覆盖五张 Map 的共同锁保护读写;读多写少也可先复制 facts,并从新 facts 重算 schedule、聚合和计数,再原子交换整个不可变状态。单独把 facts 换成 ConcurrentHashMap 不会让 TreeMap、聚合 Map、EnumMap 与缓存一起原子,也不会把两个不同 ReservationKey 的批量转换变成一个事务。

聚合量尤其适合做校验而不是第二真相:定期从所有 HELD facts 重算并与 heldQuantityBySku 对比,发现负数、零值残留或缺项就报警并重建。外部请求中的仓、订单、行号、SKU、数量与 limit 都应先校验;数量加法还应考虑整数溢出。日志不能泄露用户信息,错误应由真正能隔离批项或回滚事务的边界处理,不能捕获异常后假装整批成功。

随堂检查 3:把 facts 和 heldQuantityBySku 都换成 ConcurrentHashMap,并分别使用 compute,能否保证批量中两条事实、两条到期项、汇总量和状态计数一起提交?

**即时答案:**不能。每次 compute 的原子边界仍是单张 Map 的单个键;跨键、跨 Map、跨进程与崩溃恢复都需要更外层的事务、共同锁、单线程所有者或不可变聚合交换。

8. 错误示例:把缓存、零值和 fail-fast 当作业务保证

factKey = orderId                              // 漏 warehouseId/lineNo,事实覆盖
expiryCompare = expireMinuteOnly               // 同分钟预占 compare==0,日历丢项
for (entry : expiredView) schedule.remove(key) // 迭代时绕过迭代器修改根树
facts.put(key, EXPIRED)                        // 只改事实,不删日历与扣聚合
heldQuantity.merge(skuKey, -qty, sum)          // 缺键时反而创建负数
heldQuantity.put(skuKey, 0)                    // 破坏“缺键即零”的合同
cache eviction -> facts.remove(key)            // 缓存淘汰误删库存事实
catch ConcurrentModificationException          // 把 fail-fast 当线程安全和重试

也不要把未知位置的 LinkedList 删除说成 O(1):没有节点引用时先找候选仍需 O(n)。TreeMap 解决的是有序导航,HashMap 解决的是映射定位,两者的单项优势都不能证明五张表的一致性。

9. 正确示例:只验证集合合同,不交付三个 TODO

下面是独立的合同演示。它用不同数据手工处理已知的 C、B 两项,展示严格前缀、limit(2) 选择、共享聚合一项保留一项归零删除、缓存重新进入和活视图变化;它没有 expirySnapshotconfirmexpireBatch 方法,没有缺失/状态/索引失配分支,也没有整批原子性方案,因此不能替代今日编码题答案。

import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;

public final class ReservationBatchContractsDemo {
    private enum State {
        HELD,
        CONFIRMED,
        EXPIRED
    }

    private record ReservationKey(
            String warehouseId,
            String orderId,
            int lineNo) {
        ReservationKey {
            Objects.requireNonNull(warehouseId, "warehouseId");
            Objects.requireNonNull(orderId, "orderId");
            if (warehouseId.isBlank() || orderId.isBlank() || lineNo <= 0) {
                throw new IllegalArgumentException("invalid reservation key");
            }
        }

        String label() {
            return warehouseId + "/" + orderId + "/" + lineNo;
        }
    }

    private record StockKey(
            String warehouseId,
            String sku) implements Comparable<StockKey> {
        StockKey {
            Objects.requireNonNull(warehouseId, "warehouseId");
            Objects.requireNonNull(sku, "sku");
        }

        @Override
        public int compareTo(StockKey other) {
            int byWarehouse = warehouseId.compareTo(other.warehouseId);
            return byWarehouse != 0
                    ? byWarehouse
                    : sku.compareTo(other.sku);
        }

        String label() {
            return warehouseId + "/" + sku;
        }
    }

    private record Reservation(
            ReservationKey key,
            String sku,
            int quantity,
            long expireMinute,
            State state) {
        Reservation {
            Objects.requireNonNull(key, "key");
            Objects.requireNonNull(sku, "sku");
            Objects.requireNonNull(state, "state");
            if (sku.isBlank() || quantity <= 0 || expireMinute < 0) {
                throw new IllegalArgumentException("invalid reservation");
            }
        }
    }

    private record ExpiryKey(
            long expireMinute,
            String warehouseId,
            String orderId,
            int lineNo) implements Comparable<ExpiryKey> {
        static ExpiryKey from(Reservation reservation) {
            return new ExpiryKey(
                    reservation.expireMinute(),
                    reservation.key().warehouseId(),
                    reservation.key().orderId(),
                    reservation.key().lineNo());
        }

        @Override
        public int compareTo(ExpiryKey other) {
            int byMinute = Long.compare(expireMinute, other.expireMinute);
            if (byMinute != 0) {
                return byMinute;
            }
            int byWarehouse = warehouseId.compareTo(other.warehouseId);
            if (byWarehouse != 0) {
                return byWarehouse;
            }
            int byOrder = orderId.compareTo(other.orderId);
            return byOrder != 0
                    ? byOrder
                    : Integer.compare(lineNo, other.lineNo);
        }

        String label() {
            return warehouseId + "/" + orderId + "/" + lineNo
                    + "@" + expireMinute;
        }
    }

    public static void main(String[] args) {
        Reservation a = reservation("wh-east", "o-701", 1,
                "sku-red", 3, 100);
        Reservation b = reservation("wh-west", "o-702", 1,
                "sku-red", 4, 90);
        Reservation c = reservation("wh-east", "o-703", 2,
                "sku-red", 2, 90);
        Reservation d = reservation("wh-east", "o-704", 1,
                "sku-blue", 5, 95);

        Map<ReservationKey, Reservation> facts = new HashMap<>();
        NavigableMap<ExpiryKey, ReservationKey> schedule = new TreeMap<>();
        Map<StockKey, Integer> held = new HashMap<>();
        for (Reservation reservation : List.of(a, b, c, d)) {
            facts.put(reservation.key(), reservation);
            schedule.put(ExpiryKey.from(reservation), reservation.key());
            held.merge(skuKey(reservation), reservation.quantity(), Integer::sum);
        }

        LinkedHashMap<ReservationKey, Reservation> recent =
                new LinkedHashMap<>(4, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(
                            Map.Entry<ReservationKey, Reservation> eldest) {
                        return size() > 3;
                    }
                };
        recent.put(a.key(), a);
        recent.put(b.key(), b);
        recent.put(c.key(), c);
        recent.get(a.key());
        recent.put(d.key(), d);

        EnumMap<State, Integer> counts = new EnumMap<>(State.class);
        counts.put(State.HELD, 4);
        ExpiryKey upper = new ExpiryKey(96, "", "", Integer.MIN_VALUE);
        NavigableMap<ExpiryKey, ReservationKey> liveEligible =
                schedule.headMap(upper, false);
        List<String> snapshot = liveEligible.keySet().stream()
                .map(ExpiryKey::label)
                .toList();
        List<ReservationKey> selected = liveEligible.values().stream()
                .limit(2)
                .toList();

        System.out.println("schedule-before=" + expiryLabels(schedule));
        System.out.println("held-before=" + heldLines(held));
        System.out.println("cache-before=" + reservationLabels(recent));
        System.out.println("eligible-before=" + expiryLabels(liveEligible));
        System.out.println("selected=" + selected.stream()
                .map(ReservationKey::label).toList());

        Reservation expiredC = new Reservation(
                c.key(), c.sku(), c.quantity(), c.expireMinute(), State.EXPIRED);
        Reservation expiredB = new Reservation(
                b.key(), b.sku(), b.quantity(), b.expireMinute(), State.EXPIRED);
        facts.put(c.key(), expiredC);
        facts.put(b.key(), expiredB);
        schedule.remove(ExpiryKey.from(c));
        schedule.remove(ExpiryKey.from(b));
        held.compute(skuKey(c), (ignored, current) ->
                current - c.quantity());
        held.compute(skuKey(b), (ignored, current) -> {
            int next = current - b.quantity();
            return next == 0 ? null : next;
        });
        counts.merge(State.HELD, -2, Integer::sum);
        counts.merge(State.EXPIRED, 2, Integer::sum);
        recent.put(c.key(), expiredC);
        recent.put(b.key(), expiredB);

        System.out.println("schedule-after=" + expiryLabels(schedule));
        System.out.println("eligible-after=" + expiryLabels(liveEligible));
        System.out.println("snapshot-stable=" + snapshot);
        System.out.println("held-after=" + heldLines(held));
        System.out.println("counts-after=" + counts);
        System.out.println("cache-after=" + reservationLabels(recent));
        System.out.println("states=" + facts.get(c.key()).state()
                + "," + facts.get(b.key()).state());
    }

    private static Reservation reservation(
            String warehouseId,
            String orderId,
            int lineNo,
            String sku,
            int quantity,
            long expireMinute) {
        ReservationKey key = new ReservationKey(warehouseId, orderId, lineNo);
        return new Reservation(
                key, sku, quantity, expireMinute, State.HELD);
    }

    private static StockKey skuKey(Reservation reservation) {
        return new StockKey(
                reservation.key().warehouseId(), reservation.sku());
    }

    private static List<String> expiryLabels(
            Map<ExpiryKey, ReservationKey> schedule) {
        return schedule.keySet().stream().map(ExpiryKey::label).toList();
    }

    private static List<String> heldLines(
            Map<StockKey, Integer> held) {
        return held.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(entry -> entry.getKey().label() + "=" + entry.getValue())
                .toList();
    }

    private static List<String> reservationLabels(
            Map<ReservationKey, Reservation> reservations) {
        return reservations.keySet().stream()
                .map(ReservationKey::label)
                .toList();
    }
}

预期输出:

schedule-before=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95, wh-east/o-701/1@100]
held-before=[wh-east/sku-blue=5, wh-east/sku-red=5, wh-west/sku-red=4]
cache-before=[wh-east/o-703/2, wh-east/o-701/1, wh-east/o-704/1]
eligible-before=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95]
selected=[wh-east/o-703/2, wh-west/o-702/1]
schedule-after=[wh-east/o-704/1@95, wh-east/o-701/1@100]
eligible-after=[wh-east/o-704/1@95]
snapshot-stable=[wh-east/o-703/2@90, wh-west/o-702/1@90, wh-east/o-704/1@95]
held-after=[wh-east/sku-blue=5, wh-east/sku-red=3]
counts-after={HELD=2, EXPIRED=2}
cache-after=[wh-east/o-704/1, wh-east/o-703/2, wh-west/o-702/1]
states=EXPIRED,EXPIRED

10. 边界总结:六类红线与本题放行条件

  1. HashMap、HashSet 的遇见顺序不稳定;批量顺序只能来自完整的 ExpiryKey 全序,缓存热度来自明确的 access-order。
  2. ReservationKey 的 equals/hashCode 必须覆盖同一组不可变身份字段;Comparator 为 0 必须只代表同一到期投影,不能漏仓、单、行。
  3. 导航视图和集合视图背靠根 Map,只读包装不等于不可变快照;跨边界报告必须复制并说明对象所有权。
  4. 未知节点位置时 LinkedList 的查找和删除整体仍是 O(n),不能把已持有节点后的局部改链冒充完整复杂度。
  5. fail-fast 只是尽力发现结构修改误用,不提供互斥、可见性、线程安全或批次回滚;正确迭代删除也不等于事务。
  6. ConcurrentHashMap 的单键 compute 不会让多 ReservationKey、TreeMap、聚合 HashMap、EnumMap 与缓存自动原子;应使用共同锁、单线程所有者、不可变聚合交换或数据库事务。

还要守住三个领域边界:缓存淘汰不删除事实;聚合缺项和数量不足是索引失配,不能扣成负数;计数或聚合降到零时删除键,保持“缺键即零”的合同。maxItems=0 必须空返回且无副作用,负数参数先失败。putIfAbsent/compute/merge/remove(key,value) 的返回值应参与状态校验,WeakHashMap 与 IdentityHashMap 不承担稳定业务身份。

一句话收束:**事实回答“是哪笔预占”,日历回答“谁先过期”,仓 SKU 汇总回答“还占多少”,limit 控制本批工作量,外层一致性边界保证这些答案同时成立。**只有大大提交完整闯关答案,Java 21 编译运行符合预期,总分至少 80 且六类红线均未命中,challenge.map 才能从 covered_unverified 变为 mastered 并进入 Set。

返回今日索引

返回今日索引

编码闯关:多仓库库存预占过期批处理索引

评估项:challenge.map。建议用时:18~20 分钟。本题必须提交完整 Java 21 代码、成功编译证据与逐行一致的运行输出;只写思路不能通过 Map 阶段闯关。

你要为仓储服务完成一个单线程库存预占索引。一条预占既要按“仓库 + 订单 + 行号”精确查询,又要按过期分钟批量释放;确认或过期时还要扣减“仓库 + SKU”的已预占量、迁移状态计数并更新最近缓存。今天的主轴是有界批处理与聚合归零删键,不再重复昨日的排序字段重排。

1. 业务背景

同一订单号可以出现在多个仓库,一张订单也可以有多个行项。因此事实键必须是不可变 ReservationKey(warehouseId, orderId, lineNo)sku、过期分钟、数量和状态都属于 value,不能缩减或改写事实身份。

五张 Map 的职责分开:

  • factsHashMap<ReservationKey, StockReservation>,唯一事实来源。确认和过期都保留事实,只用新的不可变 value 迁移状态。
  • expiryScheduleTreeMap<ExpiryKey, ReservationKey>,只保存 HELD 预占的过期投影,按过期分钟、仓库、订单、行号升序补足全序。
  • heldQuantityBySkuHashMap<StockKey, Integer>StockKey(warehouseId, sku) 不跨仓混合库存;确认或过期扣到 0 时删除聚合键。
  • recentCache:容量为 3 的 access-order LinkedHashMap;首次预占、缓存命中、确认和过期会改变热度,淘汰仅作用于缓存。
  • stateCountsEnumMap<ReservationState, Integer>,按 HELD、CONFIRMED、EXPIRED 封闭状态域统计。

已实现的 reserve 负责写入种子数据。你只需补全时间窗快照、确认、有上限的过期批次三个位置。五张普通 Map 连续写入不是事务:生产环境要用共同锁、单线程事件所有者、不可变聚合交换或数据库事务覆盖完整转换,并能由事实表重建派生索引。

2. 约束与验收边界

  • 基线为 Java 21,仅使用 JDK;不添加依赖、日志、调试输出或无法恢复问题的 try/catch
  • ReservationKeyStockKey 均为不可变 record;仓库、订单、SKU 不能为空白,行号和预占数量为正数,过期分钟非负。
  • 新预占必须是 HELD。已实现的 reserve 对完整事实键去重;重复预占返回 false,不重复聚合、不覆盖过期键、不刷新缓存热度。
  • ExpiryKeyexpiresMinute、warehouseId、orderId、lineNo 升序比较;合法业务键上 compareTo == 0 必须恰好表示四个分量全部相等。
  • expirySnapshot(fromInclusive,toExclusive) 必须从 TreeMap 的 [fromInclusive,toExclusive) 活视图投影出稳定、不可修改的字符串快照,不得扫描 HashMap 后再排序。
  • confirm(key) 仅能转换存在且仍为 HELD 的预占;缺失或已终态返回 false 且无副作用。成功时删除过期投影、用新 value 改为 CONFIRMED、扣减对应仓库 SKU 的 HELD 聚合、迁移计数并更新缓存。
  • expireBatch(beforeExclusive,maxItems) 只处理过期分钟严格小于 beforeExclusive 的前 maxItems 项;结果顺序来自 TreeMap,maxItems == 0 合法且无副作用,负数参数在任何业务读写前失败。
  • 确认和过期都要先核对事实状态与完整排序键;聚合量不得为负,减至 0 删键,不得留下“数值为 0 却仍存在”的假预占。
  • 禁止依赖 HashMap/HashSet 顺序、破坏 equals/hashCode 或 Comparator 契约、混淆活视图/只读包装/快照、把 fail-fast 当线程安全,或把 ConcurrentHashMap 的单键能力当成跨键、跨 Map 事务。

3. 目标拆解与实施顺序

  1. 阅读已实现的 reserve,核对只有事实键首次出现时才建立过期投影、聚合、计数和缓存投影。
  2. 完成快照:用两个边界键得到半开导航视图,逐项回查 HELD 事实并固化字符串。
  3. 完成确认:在更改任何 Map 前核对完整过期键,再依次删除过期投影、替换事实、扣减聚合、迁移计数并更新缓存。
  4. 完成批量过期:先建立严格上界视图,每轮只取当前首项,核对后迁移事实及四个派生投影,到达上限即停止。
  5. 以 Java 21 编译、运行并逐行核对规定输出;提交完整代码、编译证据和输出。

4. 完整可编译的 Java 21 起始代码

保存为 WarehouseReservationExpiryChallenge.java。未补代码时仍能通过编译;直接运行会完成种子预占和缓存命中,然后在第一个待补位置以 TODO 1: expirySnapshot 明确失败。起始代码恰好只有 3 个待补位置,当天不提供完整实现。

import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;

public final class WarehouseReservationExpiryChallenge {
    private enum ReservationState {
        HELD,
        CONFIRMED,
        EXPIRED
    }

    private record ReservationKey(
            String warehouseId,
            String orderId,
            int lineNo) {
        ReservationKey {
            Objects.requireNonNull(warehouseId, "warehouseId");
            Objects.requireNonNull(orderId, "orderId");
            if (warehouseId.isBlank() || orderId.isBlank() || lineNo <= 0) {
                throw new IllegalArgumentException("invalid reservation key");
            }
        }

        String label() {
            return warehouseId + "/" + orderId + "/" + lineNo;
        }
    }

    private record StockKey(String warehouseId, String sku) {
        StockKey {
            Objects.requireNonNull(warehouseId, "warehouseId");
            Objects.requireNonNull(sku, "sku");
            if (warehouseId.isBlank() || sku.isBlank()) {
                throw new IllegalArgumentException("invalid stock key");
            }
        }
    }

    private record StockReservation(
            ReservationKey key,
            String sku,
            long expiresMinute,
            int quantity,
            ReservationState state) {
        StockReservation {
            Objects.requireNonNull(key, "key");
            Objects.requireNonNull(sku, "sku");
            Objects.requireNonNull(state, "state");
            if (sku.isBlank() || expiresMinute < 0 || quantity <= 0) {
                throw new IllegalArgumentException("invalid reservation");
            }
        }

        StockKey stockKey() {
            return new StockKey(key.warehouseId(), sku);
        }

        StockReservation confirmed() {
            return new StockReservation(
                    key, sku, expiresMinute, quantity,
                    ReservationState.CONFIRMED);
        }

        StockReservation expired() {
            return new StockReservation(
                    key, sku, expiresMinute, quantity,
                    ReservationState.EXPIRED);
        }

        String snapshotLine() {
            return key.label() + ":" + sku + "@" + expiresMinute
                    + "x" + quantity + ":" + state;
        }
    }

    private record ExpiryKey(
            long expiresMinute,
            String warehouseId,
            String orderId,
            int lineNo) implements Comparable<ExpiryKey> {
        static ExpiryKey from(StockReservation reservation) {
            return new ExpiryKey(
                    reservation.expiresMinute(),
                    reservation.key().warehouseId(),
                    reservation.key().orderId(),
                    reservation.key().lineNo());
        }

        static ExpiryKey boundary(long minute) {
            return new ExpiryKey(minute, "", "", Integer.MIN_VALUE);
        }

        @Override
        public int compareTo(ExpiryKey other) {
            int byMinute = Long.compare(expiresMinute, other.expiresMinute);
            if (byMinute != 0) {
                return byMinute;
            }
            int byWarehouse = warehouseId.compareTo(other.warehouseId);
            if (byWarehouse != 0) {
                return byWarehouse;
            }
            int byOrder = orderId.compareTo(other.orderId);
            return byOrder != 0
                    ? byOrder
                    : Integer.compare(lineNo, other.lineNo);
        }
    }

    private static final class ReservationIndex {
        private static final int RECENT_LIMIT = 3;

        private final Map<ReservationKey, StockReservation> facts =
                new HashMap<>();
        private final NavigableMap<ExpiryKey, ReservationKey> expirySchedule =
                new TreeMap<>();
        private final Map<StockKey, Integer> heldQuantityBySku =
                new HashMap<>();
        private final LinkedHashMap<ReservationKey, StockReservation>
                recentCache = new LinkedHashMap<>(4, 0.75f, true) {
                    @Override
                    protected boolean removeEldestEntry(
                            Map.Entry<
                                    ReservationKey,
                                    StockReservation> eldest) {
                        return size() > RECENT_LIMIT;
                    }
                };
        private final EnumMap<ReservationState, Integer> stateCounts =
                new EnumMap<>(ReservationState.class);

        private boolean reserve(StockReservation reservation) {
            Objects.requireNonNull(reservation, "reservation");
            if (reservation.state() != ReservationState.HELD) {
                throw new IllegalArgumentException(
                        "new reservation must be held");
            }
            if (facts.putIfAbsent(reservation.key(), reservation) != null) {
                return false;
            }

            // 事实新增成功后才建立派生投影;生产中需共同原子边界。
            ReservationKey previous = expirySchedule.put(
                    ExpiryKey.from(reservation), reservation.key());
            if (previous != null) {
                throw new IllegalStateException("duplicate expiry key");
            }
            heldQuantityBySku.merge(
                    reservation.stockKey(),
                    reservation.quantity(),
                    Math::addExact);
            stateCounts.merge(ReservationState.HELD, 1, Math::addExact);
            recentCache.put(reservation.key(), reservation);
            return true;
        }

        private List<String> expirySnapshot(
                long fromInclusive,
                long toExclusive) {
            if (fromInclusive < 0 || toExclusive < 0
                    || fromInclusive > toExclusive) {
                throw new IllegalArgumentException("invalid expiry window");
            }
            // 从半开导航活视图产生拥有独立数据的字符串快照。
            throw new UnsupportedOperationException(
                    "TODO 1: expirySnapshot");
        }

        private boolean confirm(ReservationKey key) {
            // 只转换 HELD 事实,并同步删导航、扣聚合、迁计数和更新缓存。
            throw new UnsupportedOperationException("TODO 2: confirm");
        }

        private List<String> expireBatch(
                long beforeExclusive,
                int maxItems) {
            if (beforeExclusive < 0 || maxItems < 0) {
                throw new IllegalArgumentException(
                        "expiry boundary and batch size must not be negative");
            }
            // 只处理严格上界内的前 maxItems 项,每项都要维护完整不变量。
            throw new UnsupportedOperationException("TODO 3: expireBatch");
        }

        private void decreaseHeldQuantity(StockReservation reservation) {
            StockKey stockKey = reservation.stockKey();
            Integer current = heldQuantityBySku.get(stockKey);
            if (current == null || current < reservation.quantity()) {
                throw new IllegalStateException(
                        "held quantity is inconsistent with facts");
            }
            int remaining = current - reservation.quantity();
            if (remaining == 0) {
                heldQuantityBySku.remove(stockKey);
            } else {
                heldQuantityBySku.put(stockKey, remaining);
            }
        }

        private void moveState(
                ReservationState from,
                ReservationState to) {
            Integer current = stateCounts.get(from);
            if (current == null || current <= 0) {
                throw new IllegalStateException(
                        "state count is inconsistent with facts");
            }
            if (current == 1) {
                stateCounts.remove(from);
            } else {
                stateCounts.put(from, current - 1);
            }
            stateCounts.merge(to, 1, Math::addExact);
        }

        private int heldQuantity(StockKey stockKey) {
            return heldQuantityBySku.getOrDefault(
                    Objects.requireNonNull(stockKey, "stockKey"), 0);
        }

        private String recentState(ReservationKey key) {
            StockReservation reservation = recentCache.get(
                    Objects.requireNonNull(key, "key"));
            return reservation == null ? "MISS" : reservation.state().name();
        }

        private List<String> scheduleOrder() {
            return expirySchedule.entrySet().stream()
                    .map(entry -> entry.getValue().label()
                            + "@" + entry.getKey().expiresMinute())
                    .toList();
        }

        private List<String> recentOrder() {
            return recentCache.keySet().stream()
                    .map(ReservationKey::label)
                    .toList();
        }

        private List<String> countSummary() {
            return stateCounts.entrySet().stream()
                    .map(entry -> entry.getKey() + "=" + entry.getValue())
                    .toList();
        }
    }

    public static void main(String[] args) {
        ReservationKey aKey = new ReservationKey("w-a", "o-7", 1);
        ReservationKey bKey = new ReservationKey("w-b", "o-7", 1);
        ReservationKey cKey = new ReservationKey("w-a", "o-8", 2);
        ReservationKey dKey = new ReservationKey("w-a", "o-9", 1);

        StockReservation a = new StockReservation(
                aKey, "book", 600, 3, ReservationState.HELD);
        StockReservation b = new StockReservation(
                bKey, "book", 580, 4, ReservationState.HELD);
        StockReservation c = new StockReservation(
                cKey, "pen", 580, 2, ReservationState.HELD);
        StockReservation d = new StockReservation(
                dKey, "book", 620, 5, ReservationState.HELD);

        ReservationIndex index = new ReservationIndex();
        System.out.println("RESERVE A=" + index.reserve(a));
        System.out.println("RESERVE B=" + index.reserve(b));
        System.out.println("RESERVE C=" + index.reserve(c));
        System.out.println("RESERVE D=" + index.reserve(d));
        System.out.println("RESERVE duplicate-A=" + index.reserve(a));
        System.out.println("CACHE initial=" + index.recentOrder());
        System.out.println("CACHE access-B=" + index.recentState(bKey));
        System.out.println("CACHE after-access=" + index.recentOrder());

        List<String> beforeChanges = index.expirySnapshot(580, 601);
        System.out.println("WINDOW before=" + beforeChanges);
        System.out.println("CONFIRM C=" + index.confirm(cKey));
        System.out.println("CACHE after-confirm=" + index.recentOrder());
        System.out.println("HELD w-a/pen="
                + index.heldQuantity(new StockKey("w-a", "pen")));
        System.out.println("COUNTS after-confirm=" + index.countSummary());

        System.out.println("EXPIRE before-580 max-2="
                + index.expireBatch(580, 2));
        System.out.println("EXPIRE before-601 max-1="
                + index.expireBatch(601, 1));
        System.out.println("CACHE after-first-expire="
                + index.recentOrder());
        System.out.println("EXPIRE before-601 max-3="
                + index.expireBatch(601, 3));
        System.out.println("CACHE after-second-expire="
                + index.recentOrder());

        System.out.println("COUNTS final=" + index.countSummary());
        System.out.println("SCHEDULE final=" + index.scheduleOrder());
        System.out.println("HELD w-a/book="
                + index.heldQuantity(new StockKey("w-a", "book")));
        System.out.println("HELD w-b/book="
                + index.heldQuantity(new StockKey("w-b", "book")));
        System.out.println("HELD w-a/pen="
                + index.heldQuantity(new StockKey("w-a", "pen")));
        System.out.println("SNAPSHOT unchanged=" + beforeChanges);
    }
}

直接编译运行:

javac --release 21 WarehouseReservationExpiryChallenge.java
java WarehouseReservationExpiryChallenge

5. 三个待补位置的验收条件

位置 1:半开过期窗口转稳定快照

  • ExpiryKey.boundary 与四参数 subMap 表达 [fromInclusive,toExclusive),不遍历 facts 再排序。
  • 按视图顺序回查事实,核对状态仍为 HELDExpiryKey.from(reservation) 等于当前树键;失配必须暴露索引损坏。
  • 投影为 snapshotLine() 并返回不可修改列表;后续确认、过期或树删除均不得改变旧字符串。
  • [x,x) 合法且为空;负数边界或下界大于上界时,在读取业务 Map 前失败。

位置 2:确认一条预占

  • keynull 时先失败;事实缺失、CONFIRMEDEXPIRED 时返回 false,五张 Map 均不变。
  • 用当前事实构造完整 ExpiryKey,确认树中恰好映射到该事实键;删除失败不能伪装成确认成功。
  • 使用新 CONFIRMED value 替换事实,调用已给的辅助方法扣减该仓 SKU 聚合、从 HELD 迁移计数,并以新 value 写入缓存。
  • 聚合减至 0 必须删键;缓存中已存在的 C 会移到最热端,但不影响事实寿命。

位置 3:严格上界与批次上限

  • headMap(ExpiryKey.boundary(beforeExclusive), false) 建立严格上界视图;边界分钟的全部业务键都必须被排除。
  • 最多处理 maxItems 项,每轮取视图当前首项;无候选或上限为 0 时返回空且无副作用。
  • 每项都要核对 HELD 事实与完整排序键,然后删树项、以新 EXPIRED value 替换事实、扣减聚合、迁移计数、更新缓存,并按树序返回完整事实标签。
  • 批次循环只提供数量上限,不提供跨 Map 事务;生产中还需共同原子边界、幂等重试或可重建投影。

6. 三级提示

一级提示:边界键要排在同分钟所有业务键之前

ExpiryKey.boundary(minute) 使用空仓库和空订单。合法身份都是非空白字符串,所以边界键排在该分钟业务键之前:包含下界边界会收进下界整分钟,排除上界边界会排除上界整分钟。

二级提示:聚合键不是事实键

扣减聚合时用 (warehouseId, sku),而删除过期项和替换事实时用 (warehouseId, orderId, lineNo)。聚合为 0 删键只表示当前该仓 SKU 没有 HELD 数量,不能删除已确认或已过期的事实。

三级提示:每轮处理当前首项

批处理不要在遍历器上一边迭代一边用根 Map 删除。可以在每轮重新取严格上界视图的 firstEntry,核对完整不变量后删除当前项。返回列表则另行复制,不暴露活视图。

7. 精确预期输出

补全三个待补位置后,实际输出必须逐行一致:

RESERVE A=true
RESERVE B=true
RESERVE C=true
RESERVE D=true
RESERVE duplicate-A=false
CACHE initial=[w-b/o-7/1, w-a/o-8/2, w-a/o-9/1]
CACHE access-B=HELD
CACHE after-access=[w-a/o-8/2, w-a/o-9/1, w-b/o-7/1]
WINDOW before=[w-a/o-8/2:pen@580x2:HELD, w-b/o-7/1:book@580x4:HELD, w-a/o-7/1:book@600x3:HELD]
CONFIRM C=true
CACHE after-confirm=[w-a/o-9/1, w-b/o-7/1, w-a/o-8/2]
HELD w-a/pen=0
COUNTS after-confirm=[HELD=3, CONFIRMED=1]
EXPIRE before-580 max-2=[]
EXPIRE before-601 max-1=[w-b/o-7/1]
CACHE after-first-expire=[w-a/o-9/1, w-a/o-8/2, w-b/o-7/1]
EXPIRE before-601 max-3=[w-a/o-7/1]
CACHE after-second-expire=[w-a/o-8/2, w-b/o-7/1, w-a/o-7/1]
COUNTS final=[HELD=1, CONFIRMED=1, EXPIRED=2]
SCHEDULE final=[w-a/o-9/1@620]
HELD w-a/book=5
HELD w-b/book=0
HELD w-a/pen=0
SNAPSHOT unchanged=[w-a/o-8/2:pen@580x2:HELD, w-b/o-7/1:book@580x4:HELD, w-a/o-7/1:book@600x3:HELD]

验收时还要解释:第四次预占为何只把 A 挤出缓存;重复 A 为何不能升温;命中 B 为何得到 [C,D,B];同为 580 时 C 为何先于 B;确认 C 为何删除 w-a/pen 聚合键;严格上界 580 为何一项也不处理;两次过期为何分别只处理 B 和 A;旧快照为何仍全部显示 HELD。这些顺序不得来自 HashMap 的遇见次序。

8. 复杂度要求

  • reserve 的事实表和聚合表写入期望 O(1),过期树插入 O(log n) 主导,整次为 O(log n)。HashMap 碰撞、扩容和树化使单次严格 O(1) 不成立。
  • 窗口边界定位加 k 项固化为 O(log n + k) 时间,快照额外空间为 O(k)
  • confirm 含一次 TreeMap 删除,整次为 O(log n);事实替换、聚合扣减、计数与缓存更新为常数或期望 O(1)
  • 批次实际处理 m <= maxItems 项时,时间为 O(m log n),返回标签的额外空间为 O(m);上限只控制单批工作量,不改变总数据规模。
  • heldQuantityBySku 最多按有 HELD 量的仓库 SKU 组合增长,上界 O(n);事实表与过期树为 O(n),固定状态表和容量 3 缓存为 O(1)

9. 边界用例

补全后至少自行验证:

  1. 不同仓库的同订单同行号可并存,同订单的不同行也可并存;事实键不含可变状态。
  2. 重复预占返回 false,事实、日程、聚合和计数不重复,已被淘汰项也不借此返回缓存。
  3. 确认缺失或已终态事实返回 false 且无副作用;事实与过期键失配时明确失败。
  4. 聚合扣减不得为负;减到 0 删除键,但 facts 中的 CONFIRMED/EXPIRED 事实仍存在。
  5. [580,580) 是空快照;负数边界或下界大于上界时不读写业务状态。
  6. expireBatch(580,2) 不包含恰在 580 分钟的 B;maxItems == 0 不处理,负数上限先失败。
  7. 同分钟依仓库、订单、行号全序不覆盖;不依赖 HashMap/HashSet 顺序。
  8. 确认和过期会让活导航视图变化,旧字符串快照不变,只读包装也不等于快照。
  9. 批量循环、fail-fast 或 ConcurrentHashMap 单键方法均不提供跨五张 Map 事务。

10. 闯关提交与自检

  • 提交补全后的完整 WarehouseReservationExpiryChallenge.java,不是三个零散片段。
  • 提交 javac --release 21 成功证据与完整、逐行一致的运行输出。
  • 恰好补全 3 个 TODO,没有用样例数据特判绕过合同。
  • ReservationKey 包含仓库、订单、行号并保持不可变;StockKey 不跨仓聚合。
  • 过期比较器包含全部区分量,半开窗口和严格批处理上界均正确。
  • 确认、过期后树项、事实状态、HELD 聚合、枚举计数和缓存一致。
  • 能区分 TreeMap 过期顺序、LinkedHashMap 访问顺序、HashMap 无序与时间点快照。
  • 没有把聚合 merge、批量循环、fail-fast 或 ConcurrentHashMap 描述为跨 Map 事务。
  • 没有无意义 try/catch、调试日志、多余公开 API 或第三方依赖。
  • 能解释时间、空间复杂度与至少 4 个边界用例。

11. Java 21 与固定源码资料

返回今日索引

返回今日索引

Map 阶段闯关题:多仓库库存预占过期批处理索引

建议用时:约 8~9 分钟。请优先在 互动页面 作答,聊天提交仅作补充。当天不提供答案。 第 1~3、5 题提交结论与最短必要推理;第 4 题必须提交完整 Java 21 代码、编译证据和规定输出。

题号 固定维度 知识点 ID 分值 可能触发的红线
1 契约与选型 ds.map.contractds.map.equalityds.map.linkedhashmap-sourceds.map.treemap-sourceds.map.specialized 25 假设 HashMap/HashSet 顺序稳定;破坏 equals/hashCode 或 Comparator;把聚合键、过期键当成事实身份
2 复杂度与状态推演 ds.map.hashmap-structureds.map.hashmap-put-get-remove-sourceds.map.hashmap-resize-treeify-iterator-sourceds.map.treemap-source 12 把期望复杂度写成严格保证;认为未知节点位置时 LinkedList 中间操作天然 O(1);把 fail-fast 当线程安全
3 复杂度与状态推演 ds.map.contractds.map.compute-merge-viewsds.map.linkedhashmap-sourceds.map.treemap-sourceds.map.specialized 13 混淆活视图、只读包装与快照;聚合归零不删键;把批次或多 Map 更新当事务
4 编码 ds.map.contractds.map.equalityds.map.compute-merge-viewsds.map.linkedhashmap-sourceds.map.treemap-sourceds.map.specialized 30 半开/严格上界错误、批次越限、事实或聚合失配;把 ConcurrentHashMap 误认为跨键或跨集合自动原子
5 源码讲解 ds.map.hashmap-structureds.map.hashmap-put-get-remove-sourceds.map.hashmap-resize-treeify-iterator-sourceds.map.compute-merge-viewsds.map.linkedhashmap-sourceds.map.treemap-sourceds.map.specialized 20 猜测源码或红黑树形状;把实现细节当公共契约;把 fail-fast、缓存或单 Map 方法当并发事务
合计 契约与选型 25;复杂度与状态推演 25;编码 30;源码讲解 20 覆盖 Map 9 个知识项 100

闯关放行必须同时满足:总分至少 80/100、第 4 题 Java 21 完整代码成功编译运行且产生规定输出、六类红线均未命中。只达到分数、只交片段、编译失败或命中任一红线,都不能进入 Set;应根据有效证据补强并重闯。

1. 契约与方案设计:事实、过期、聚合与缓存(25 分)

多仓库预占服务要支持事实精确查询、按分钟过期、每仓 SKU 的 HELD 数量、封闭状态计数和容量 3 的最近访问缓存。用最多 9 条定义五张 Map 的选型、事实/派生关系和不变量,并说明:ReservationKeyStockKey 各自的身份边界,null 键/value 策略,ExpiryKey 如何补足全序,聚合归零为何删键,access-order 命中与淘汰的语义,导航活视图与对外快照的所有权。解释为何 HashMap/HashSet 顺序不能表示过期次序,以及 WeakHashMap、IdentityHashMap 为何不适合持久业务事实。

2. 复杂度与结构推演:批处理上限不改变单项成本(12 分)

设事实数为 n,时间窗命中 k 项,一次批处理实际过期 m <= maxItems 项。给出事实精确查询、首次预占、窗口转快照、确认、批量过期、聚合扣减、枚举计数和有界缓存的时间/空间复杂度。说明 HashMap 碰撞、扩容与 OpenJDK 21 树化容量门槛为何否定“每次严格 O(1)”;再比较不知候选节点位置时 LinkedList 查找/删除与 TreeMap 边界导航的完整成本,并指出 fail-fast 没有提供的互斥、可见性、原子性与一致快照性质。

3. 状态推演与代码分析:确认、严格过期与归零删键(13 分)

最近缓存容量为 2。依次预占 X=w-x/o-1/1:book@700x2、Y=w-y/o-1/1:book@680x3、Z=w-x/o-2/2:pen@680x4,再命中 Y;随后取得 [680,701) 的导航活视图并投影为不可变字符串快照,确认 Z,执行 expireBatch(700,1),最后执行 expireBatch(701,2)

逐步写出过期树、最近缓存、HELD/CONFIRMED/EXPIRED 计数和三个仓库 SKU 聚合的变化;写出两次批处理的返回项、最终事实状态、过期树、旧活视图当前内容和旧快照内容。说明同分钟为何 Z 先于 Y,严格上界 700 为何排除 X,聚合为 0 为何删键,以及已被淘汰的 X 过期后如何重新进入缓存并触发新淘汰。不得依赖 HashMap 顺序,不得把批处理或多 Map 写入称为自动事务。

4. 必交编码:完成快照、确认与过期批次(30 分)

补全 编码练习 的 3 个待补位置,提交完整 WarehouseReservationExpiryChallenge.javajavac --release 21 WarehouseReservationExpiryChallenge.java 的成功结果,以及 java WarehouseReservationExpiryChallenge 的完整且逐行一致输出。再用不超过 7 句话说明重复预占、半开快照、确认、严格过期上界、批次上限、聚合归零删键、缓存重新进入和跨 Map 原子性边界。

评分拆分:半开导航视图转稳定快照 7 分;确认、聚合与状态迁移 8 分;严格上界批处理、上限与缓存 9 分;Java 21 编译、规定输出和边界说明 6 分。缺少完整代码、编译失败或输出不符时,编码维度不能视为通过,整个 Map 闯关也不得放行。

5. 源码追踪:用固定入口解释批处理结果(20 分)

固定版本为 OpenJDK jdk-21+35。用 5 行表格作答,每行包含“问题、公开入口、关键字段、最多 4 个箭头节点的主路径、可观察结果或扩展点”,分别覆盖:HashMap 复合键查询、首次写入、事实替换、聚合 merge/get/put/remove;阈值、扩容高低链拆分、树化容量条件与迭代器结构修改检测;LinkedHashMap access-order 的命中、已有键更新与最老项钩子;TreeMap 的比较定位、subMap/headMap、当前首项及视图删除;EnumMap 的枚举槽位与三状态迁移。

每行 4 分。必须从源码路径回到本题的返回值、size、日程顺序、范围、聚合、缓存和状态结果;不能把方法名清单当答案,不能猜红黑树具体形状,也不能把内部阈值、fail-fast、普通 Map 或 ConcurrentHashMap 单键能力扩张成业务顺序、线程安全或跨键/跨 Map 事务。

返回今日索引

课后作答

复盘问题与编码作答

草稿保存在当前浏览器;点击保存或按 ⌘/Ctrl + S 后,才会写入当天的 04-复盘作答.md

1. 契约与方案设计:事实、过期、聚合与缓存(25 分)

多仓库预占服务要支持事实精确查询、按分钟过期、每仓 SKU 的 HELD 数量、封闭状态计数和容量 3 的最近访问缓存。用最多 9 条定义五张 Map 的选型、事实/派生关系和不变量,并说明:ReservationKeyStockKey 各自的身份边界,null 键/value 策略,ExpiryKey 如何补足全序,聚合归零为何删键,access-order 命中与淘汰的语义,导航活视图与对外快照的所有权。解释为何 HashMap/HashSet 顺序不能表示过期次序,以及 WeakHashMap、IdentityHashMap 为何不适合持久业务事实。

2. 复杂度与结构推演:批处理上限不改变单项成本(12 分)

设事实数为 n,时间窗命中 k 项,一次批处理实际过期 m <= maxItems 项。给出事实精确查询、首次预占、窗口转快照、确认、批量过期、聚合扣减、枚举计数和有界缓存的时间/空间复杂度。说明 HashMap 碰撞、扩容与 OpenJDK 21 树化容量门槛为何否定“每次严格 O(1)”;再比较不知候选节点位置时 LinkedList 查找/删除与 TreeMap 边界导航的完整成本,并指出 fail-fast 没有提供的互斥、可见性、原子性与一致快照性质。

3. 状态推演与代码分析:确认、严格过期与归零删键(13 分)

最近缓存容量为 2。依次预占 X=w-x/o-1/1:book@700x2、Y=w-y/o-1/1:book@680x3、Z=w-x/o-2/2:pen@680x4,再命中 Y;随后取得 [680,701) 的导航活视图并投影为不可变字符串快照,确认 Z,执行 expireBatch(700,1),最后执行 expireBatch(701,2)

逐步写出过期树、最近缓存、HELD/CONFIRMED/EXPIRED 计数和三个仓库 SKU 聚合的变化;写出两次批处理的返回项、最终事实状态、过期树、旧活视图当前内容和旧快照内容。说明同分钟为何 Z 先于 Y,严格上界 700 为何排除 X,聚合为 0 为何删键,以及已被淘汰的 X 过期后如何重新进入缓存并触发新淘汰。不得依赖 HashMap 顺序,不得把批处理或多 Map 写入称为自动事务。

4. 必交编码:完成快照、确认与过期批次(30 分)

补全 编码练习 的 3 个待补位置,提交完整 WarehouseReservationExpiryChallenge.javajavac --release 21 WarehouseReservationExpiryChallenge.java 的成功结果,以及 java WarehouseReservationExpiryChallenge 的完整且逐行一致输出。再用不超过 7 句话说明重复预占、半开快照、确认、严格过期上界、批次上限、聚合归零删键、缓存重新进入和跨 Map 原子性边界。

评分拆分:半开导航视图转稳定快照 7 分;确认、聚合与状态迁移 8 分;严格上界批处理、上限与缓存 9 分;Java 21 编译、规定输出和边界说明 6 分。缺少完整代码、编译失败或输出不符时,编码维度不能视为通过,整个 Map 闯关也不得放行。

5. 源码追踪:用固定入口解释批处理结果(20 分)

固定版本为 OpenJDK jdk-21+35。用 5 行表格作答,每行包含“问题、公开入口、关键字段、最多 4 个箭头节点的主路径、可观察结果或扩展点”,分别覆盖:HashMap 复合键查询、首次写入、事实替换、聚合 merge/get/put/remove;阈值、扩容高低链拆分、树化容量条件与迭代器结构修改检测;LinkedHashMap access-order 的命中、已有键更新与最老项钩子;TreeMap 的比较定位、subMap/headMap、当前首项及视图删除;EnumMap 的枚举槽位与三状态迁移。

每行 4 分。必须从源码路径回到本题的返回值、size、日程顺序、范围、聚合、缓存和状态结果;不能把方法名清单当答案,不能猜红黑树具体形状,也不能把内部阈值、fail-fast、普通 Map 或 ConcurrentHashMap 单键能力扩张成业务顺序、线程安全或跨键/跨 Map 事务。

返回今日索引

可选:编码作答

尚未保存