Java 后端每日学习 · Day 17 · 2026-09-10
打开今日互动学习页 :汇总五个课程章节,支持代码复制、复盘作答、浏览器草稿和本地 Markdown 保存。网页作答优先、聊天提交补充。
今日主题
Map 阶段闯关变式:多租户限流策略版本切换索引的契约、重排、编码与源码解释。
方向元数据
字段
值
directionId
java-data-structures
directionSession
15
curriculumItemId
challenge.map
lessonMode
checkpoint
节点进入前状态
covered_unverified
完成本课后的预期状态
covered_unverified;生成第五个变式不等于闯关通过
当前方向状态
active
查看全局 Java 后端知识地图 。固定同步助手对最近完整课程 Day 16 的拉取结果为 remote_missing,服务器与本地均没有 2026-09-09/04-复盘作答.md,当前任务也没有能明确映射到 Day 16 题号和问题源哈希的答案。因此答案来源为 none,不评分、不推断薄弱项或红线;challenge.map 保持 covered_unverified,今天继续在 Map 模块使用全新的限流策略版本数据与重排约束完成闯关变式。
今天把重心从“首次登记后领取”移到“排序字段改变时重建派生索引”:事实身份不变,但激活分钟或优先级变化时,必须移除旧 TreeMap 键、替换不可变事实 value、再插入新排序键。这个新约束用于检验大大能否把身份、排序投影、缓存热度和跨 Map 一致性真正分开。
可验证目标
能从限流策略的复合身份、缺失语义、版本归属、激活顺序和有限状态域推出 Map 选型与事实/派生关系。
能解释为什么调整参与比较的字段不能只替换事实 value,并逐步推演旧排序投影删除、新投影插入、缓存重新进入与状态计数。
能完成 Java 21 必做编码,编译并产生规定输出,同时守住不可变复合键、比较器全序、半开范围、严格上界和稳定快照。
能沿 OpenJDK jdk-21+35 的公开入口、关键字段和主调用链解释 HashMap、LinkedHashMap、TreeMap、EnumMap 与条件更新的可观察行为。
能明确单 Map 条件 API、fail-fast、多张普通 Map 连续写入与真正业务原子性之间的边界。
60~75 分钟学习顺序
昨日复盘 :Day 16 五题完整参考答案与对账差异索引完整实现(约 12~14 分钟)。
核心讲解 :用限流策略版本切换推演身份与可变排序投影的分离(约 31~32 分钟)。
编码练习 :完成策略重排、窗口快照和到期激活;这是闯关通过的必交编码(约 18~20 分钟)。
复盘问题 :按四个固定维度提交 Map 阶段闯关答案(约 8~9 分钟)。
总预计用时约 69~75 分钟 。今天只验证 Map 模块,不提前讲 Set,也不把昨日参考答案算成大大的作答证据。
方向进度
课前与课程完成后的标准课覆盖均为 10/35(28.57%);今天是同一稳定闯关项的第五个变式,不重复计数。
知识点覆盖保持 9/27;已评估 0、已掌握 0、待补强 0。
源码型知识点覆盖保持 4/9,已掌握仍为 0/9。
Map 闯关保持“已出题待验证”:闯关已开始 1/6、通过 0/6;综合项目 0/1、方向总测 0/1。
无作答只表示尚未验证,不等于失败;也不能据此放行到 Set。
课程完成标准
完成四部分学习,并能说明身份键与排序键为何分离、旧排序投影为何必须显式删除,以及缓存淘汰为何不能删除事实。
将编码练习的 3 个 TODO 补全,以 javac --release 21 编译并运行,保存完整代码与规定输出。
回答复盘页全部闯关题;回答必须映射到当天题号和问题源,不能用本文或昨日参考答案代替。
不假设 HashMap 顺序稳定,不破坏 equals/hashCode 或 Comparator 契约,不混淆活视图、只读包装与快照,也不把 fail-fast 当线程安全。
闯关放行条件
总分至少 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 16(2026-09-09),评估项 challenge.map。建议用时:12~14 分钟。先用 1~2 分钟确认第 1 节证据与推进边界,遮住答案用 5~6 分钟口述第 2 节,再用约 6 分钟核对第 3 节三个 TODO、规定输出和复杂度。下文全部是教学参考答案,不构成大大的作答、分数、闯关通过或掌握证据 。
1. 作答证据、逐题评分与推进结论
固定同步助手拉取 Day 16 作答的结果为 remote_missing,服务器上没有 2026-09-09/04-复盘作答.md;本地也没有该文件。
Day 16 问题源哈希:sha256:abe224ecee2ff4ac40945660714e72104b91bc15fa8bd5f07ef12df85fb5ebc8。
当前任务中没有能明确映射到 Day 16、题号和上述问题源哈希的聊天答案。
答案来源: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 16 五道闯关题完整参考答案
第 1 题:先固定事实身份,再为三个派生查询选型
事实主表使用 HashMap<DifferenceKey, ReconciliationDifference>。不可变 DifferenceKey(merchantId, ticketNo) 的两个组件共同参与 equals/hashCode;只有商户与差异单号都相同才是同一业务键,因此不同商户的 D-41 可以并存。键和值均禁止 null,本题的 get()==null 才能直接表示缺失;若未来允许 null value,必须用 containsKey 消歧。
正负 deltaCents、严重级别、截止分钟和状态都是会变化的事实属性,应放入不可变 value 的新版本,不能进入事实 key。金额为正或负都不改变“这是哪一笔差异”。
升级索引使用 TreeMap<UpgradeKey, DifferenceKey>。UpgradeKey 按截止分钟升序、严重级别降序、商户升序、差异单号升序比较;合法键上 compareTo==0 恰好表示四个排序组件相同,避免同分钟差异相互覆盖。升级顺序来自这个比较合同,不能用 HashMap 或 HashSet 的遇见顺序代替。
状态计数使用 EnumMap<DifferenceState, Integer>,封闭键域按枚举声明顺序遇见;OPEN 减到零时删除槽位。诊断缓存使用容量为 3 的 access-order LinkedHashMap,命中、已有键更新或插入会影响热度;淘汰只删除缓存投影,不能删除事实或调度项。
TreeMap 的 subMap/headMap 是背靠根树的活视图,适合所有者内部导航或写穿;对外报告要立即投影成独立不可变字符串快照。只读包装仍可能随原容器改变,不能冒充快照。
byKey 是唯一事实来源,升级树、计数和缓存均为可校验、可重建的派生索引。四张普通 Map 的连续写入不是事务,生产环境需由单线程事件所有者、共同锁、不可变聚合交换或数据库事务覆盖完整转换。
WeakHashMap 会让事实寿命受键可达性与非确定性 GC 影响;IdentityHashMap 用 == 区分键,反序列化得到的等值新实例会查不到旧事实。二者都不符合稳定财务业务身份。
这些约束把“映射关系、升级顺序、诊断热度、状态计数”明确分开。缓存未命中只表示不在热窗口;调度项存在也必须回查事实,任何派生投影都不能被提升为第二事实来源。
第 2 题:复杂度回答完整动作,并写清退化条件
设事实表有 n 条差异,时间窗命中 k 条:
复合键精确查询的期望时间为 O(1),但哈希碰撞和当前桶结构使它不是每次严格 O(1)。
首次登记包含事实表条件写入、调度树插入、枚举计数和诊断缓存写入;TreeMap 插入 O(log n) 主导,整次为 O(log n)。
升级最早候选需要按边界定位、删除调度树首项,并同步事实、计数和缓存;树操作主导,成功分支为 O(log n)。
半开范围定位并固化 k 条字符串为 O(log n + k),快照额外空间为 O(k)。
EnumMap 的键域固定,单次计数读写按 O(1) 理解;容量固定为 3 的诊断缓存命中、已有键移动、插入与一次最老项淘汰的期望时间均为 O(1)。
事实表和升级树随数据量增长,总索引空间为 O(n);固定状态表与有界缓存为 O(1)。
固定 OpenJDK jdk-21+35 中,新增节点超过负载阈值会触发 resize,该次操作必须处理旧 table 中的节点;碰撞查询还可能沿链或树前进。碰撞桶请求树化时,table 容量小于 MIN_TREEIFY_CAPACITY=64 会优先扩容,容量满足条件后才可能树化。因此正常分布下的期望 O(1) 不能写成单次绝对保证。
TreeMap 利用比较树定位与删除候选,单项成本为 O(log n)。若改用 LinkedList 且事先不知道目标节点位置,就必须先线性查找 O(n);即使拿到节点后链接删除可为 O(1),整次“查找并删除”仍是 O(n)。迭代器的 fail-fast 只会尽力比较结构修改计数,不提供互斥、内存可见性、原子性或一致快照,因此不能当作线程安全方案。
第 3 题:被淘汰的缓存项可重新进入,但事实始终存在
用 A、B、C、D 表示:
A:merchant-a/T1@900#S2
B:merchant-b/T1@880#S4
C:merchant-a/T2@880#S5
D:merchant-c/T9@920#S3
诊断缓存容量为 2。逐步变化如下:
登记 A:[A]。
登记 B:[A,B]。
登记 C:插入 C 后淘汰最久未访问的 A,得到 [B,C]。
诊断访问 B:命中后 B 移到尾部,得到 [C,B]。
登记 D:插入 D 后淘汰 C,得到 [B,D]。
取得窗口和快照不访问诊断缓存,仍为 [B,D]。
重复登记 A:事实表已存在,立即返回 false,不能借重复请求把 A 装回或升温,仍为 [B,D]。
登记完成后的升级树为 [C@880#S5, B@880#S4, A@900#S2, D@920#S3]。[880,901) 活视图与复制时刻的快照均按 [C,B,A] 排列。执行 escalateNext(900) 时,严格上界排除截止恰为 900 的 A;同一分钟严重级别 5 高于 4,所以领取并升级 C,即 merchant-a/T2。
升级后的结果为:
升级索引顺序:merchant-b/T1@880#S4、merchant-a/T1@900#S2、merchant-c/T9@920#S3。
状态计数:OPEN=3、ESCALATED=1。
C 在登记 D 时已从缓存淘汰;升级把新 value 插入缓存尾部,使 [B,D,C] 超过容量,再淘汰最老的 B,最终为 [D,C],即 [merchant-c/T9, merchant-a/T2]。
A、B、C 即便曾被缓存淘汰,仍由 byKey 保存;缓存淘汰没有事实删除语义。
旧 [880,901) 活视图当前为 [B,A],因为删除 C 写穿根 TreeMap,且范围上界 901 仍包含分钟 900 的 A。
旧快照仍是 [C@880#S5:OPEN, B@880#S4:OPEN, A@900#S2:OPEN],保存复制时刻的状态,不随升级改变。
事实替换、导航删除、计数迁移和缓存插入跨越四张 Map。单线程题设避免同进程内并发交错,却不能抵御进程在中途退出;putIfAbsent、computeIfPresent 或 ConcurrentHashMap 的单键能力也不会自动覆盖其他 Map。因此真实服务必须提供共同事务或可重建投影。
第 4 题:三个 TODO 的最小完整实现策略
登记:先检查 difference 非空且为 OPEN,再让 byKey.putIfAbsent 决定复合键是否首次出现。重复时立即返回,三个派生投影都不能改变;首次成功后才维护升级树、计数和诊断缓存。
窗口快照:用两个排在同分钟所有合法业务键之前的边界键建立 subMap(lower, true, upper, false),按比较顺序回查事实并生成字符串;回查缺失要暴露索引失配,返回结果不再背靠内部树。
升级:从严格上界 headMap 中取得第一项,核对事实存在、仍为 OPEN 且完整 UpgradeKey 一致;用新的 record 替换事实,从活视图删除导航项,迁移枚举计数并将升级后的 value 写入访问顺序缓存。
缓存:已有键更新会移到尾部,被淘汰键升级时会重新插入;容量钩子只影响缓存,不能操作事实或调度树。
原子性:这些步骤在练习的单线程所有权中按顺序执行,不代表跨 Map 自动原子;生产仍需更外层共同边界。
第 3 节给出与 Day 16 起始代码一致的完整 Java 21 参考实现和规定输出。
第 5 题:源码路径必须解释本题的公开现象
固定版本为 OpenJDK jdk-21+35:
问题
公开入口
关键字段
最多四个箭头节点的主路径
可观察结果或扩展点
HashMap 精确查询与新键、同键更新、删除
get、put、remove
table、size、modCount
get→getNode→桶首/链/树匹配;put→putVal→相等替换或新增;remove→removeNode→摘链/树删除
等值键更新返回旧值且 size 不增;新增和成功结构删除改变结构;遇见顺序无保证
阈值、扩容高低链拆分、树化条件与迭代检测
put、集合视图的 iterator
threshold、loadFactor、table、modCount、expectedModCount
putVal→size 超阈值→resize→按旧容量位拆高低链;treeifyBin→容量判断→扩容或树化;nextNode→比较 modCount
容量不足 64 时长链优先扩容;快速失败仅尽力检测结构修改,不提供线程安全
LinkedHashMap access-order 命中、已有键更新与淘汰钩子
get、put
head、tail、accessOrder
get→afterNodeAccess→节点移尾;putVal→afterNodeAccess/afterNodeInsertion→removeEldestEntry
成功命中和已有键更新会改变热度;插入后子类可淘汰最老项,但不会影响另一张事实表
TreeMap 比较定位、范围视图与写穿边界
put、subMap、headMap
root、size、comparator、范围端点
put→逐节点 compare→替换或插入并平衡;subMap/headMap→NavigableSubMap→边界导航/写穿
compare==0 视为同一排序键;视图按比较顺序遇见,删除写穿根树,越界写入失败
EnumMap 枚举键域表示与计数更新
put、get、merge
keyType、keyUniverse、vals、size
put→typeCheck→key.ordinal→maskNull 后写槽位
遇见顺序为枚举声明顺序;同一常量使用同一槽位,null key 被拒绝,null value 由内部哨兵区分
这些路径要收束回返回值、size、比较顺序、范围和状态结果。私有节点形状、树化阈值、当前打印顺序及 fail-fast 都不能升级为业务排序合同、线程安全或跨键、跨 Map 事务。可核对 HashMap.java 、LinkedHashMap.java 、TreeMap.java 与 EnumMap.java 。
3. Day 16 主练习完整可运行参考实现
下面只补全原起始代码的三个 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 ReconciliationDifferenceChallenge {
private enum DifferenceState {
OPEN,
ESCALATED
}
private record DifferenceKey(String merchantId, String ticketNo) {
DifferenceKey {
Objects.requireNonNull(merchantId, "merchantId");
Objects.requireNonNull(ticketNo, "ticketNo");
if (merchantId.isBlank() || ticketNo.isBlank()) {
throw new IllegalArgumentException(
"merchantId and ticketNo must not be blank");
}
}
String label() {
return merchantId + "/" + ticketNo;
}
}
private record ReconciliationDifference(
DifferenceKey key,
long dueMinute,
int severity,
long deltaCents,
DifferenceState state) {
ReconciliationDifference {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(state, "state");
if (dueMinute < 0
|| severity < 1
|| severity > 5
|| deltaCents == 0) {
throw new IllegalArgumentException(
"invalid reconciliation difference");
}
}
ReconciliationDifference escalated() {
return new ReconciliationDifference(
key,
dueMinute,
severity,
deltaCents,
DifferenceState.ESCALATED);
}
String snapshotLine() {
return key.label() + "@" + dueMinute
+ "#S" + severity + ":" + state + ":" + deltaCents;
}
}
private record UpgradeKey(
long dueMinute,
int severity,
String merchantId,
String ticketNo) implements Comparable<UpgradeKey> {
static UpgradeKey from(ReconciliationDifference difference) {
return new UpgradeKey(
difference.dueMinute(),
difference.severity(),
difference.key().merchantId(),
difference.key().ticketNo());
}
static UpgradeKey boundary(long minute) {
return new UpgradeKey(
minute,
Integer.MAX_VALUE,
"",
"");
}
@Override
public int compareTo(UpgradeKey other) {
int byMinute = Long.compare(dueMinute, other.dueMinute);
if (byMinute != 0) {
return byMinute;
}
int bySeverity = Integer.compare(other.severity, severity);
if (bySeverity != 0) {
return bySeverity;
}
int byMerchant = merchantId.compareTo(other.merchantId);
return byMerchant != 0
? byMerchant
: ticketNo.compareTo(other.ticketNo);
}
}
private static final class DifferenceIndex {
private static final int DIAGNOSTIC_LIMIT = 3;
private final Map<DifferenceKey, ReconciliationDifference> byKey =
new HashMap<>();
private final NavigableMap<UpgradeKey, DifferenceKey> upgradeSchedule =
new TreeMap<>();
private final EnumMap<DifferenceState, Integer> stateCounts =
new EnumMap<>(DifferenceState.class);
private final LinkedHashMap<
DifferenceKey, ReconciliationDifference> diagnostics =
new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<
DifferenceKey,
ReconciliationDifference> eldest) {
return size() > DIAGNOSTIC_LIMIT;
}
};
private boolean register(ReconciliationDifference difference) {
Objects.requireNonNull(difference, "difference");
if (difference.state() != DifferenceState.OPEN) {
throw new IllegalArgumentException(
"new difference must be open");
}
if (byKey.putIfAbsent(difference.key(), difference) != null) {
return false;
}
// 事实登记成功后才维护投影;生产环境需覆盖整段的原子边界。
upgradeSchedule.put(
UpgradeKey.from(difference),
difference.key());
stateCounts.merge(DifferenceState.OPEN, 1, Integer::sum);
diagnostics.put(difference.key(), difference);
return true;
}
private List<String> dueSnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive > toExclusive) {
throw new IllegalArgumentException(
"fromInclusive must be <= toExclusive");
}
NavigableMap<UpgradeKey, DifferenceKey> window =
upgradeSchedule.subMap(
UpgradeKey.boundary(fromInclusive), true,
UpgradeKey.boundary(toExclusive), false);
return window.values().stream()
.map(key -> Objects.requireNonNull(
byKey.get(key),
"upgrade schedule must reference an existing fact"))
.map(ReconciliationDifference::snapshotLine)
.toList();
}
private String escalateNext(long beforeExclusive) {
if (beforeExclusive < 0) {
throw new IllegalArgumentException(
"beforeExclusive must be >= 0");
}
NavigableMap<UpgradeKey, DifferenceKey> candidates =
upgradeSchedule.headMap(
UpgradeKey.boundary(beforeExclusive), false);
Map.Entry<UpgradeKey, DifferenceKey> first =
candidates.firstEntry();
if (first == null) {
return "NONE";
}
DifferenceKey key = first.getValue();
ReconciliationDifference current = byKey.get(key);
if (current == null
|| current.state() != DifferenceState.OPEN
|| !UpgradeKey.from(current).equals(first.getKey())) {
throw new IllegalStateException(
"upgrade schedule is inconsistent with fact map");
}
ReconciliationDifference escalated = byKey.computeIfPresent(
key,
(ignored, existing) -> existing.escalated());
// 删除子视图条目会写穿根树,随后同步计数和诊断投影。
candidates.remove(first.getKey());
int open = stateCounts.merge(
DifferenceState.OPEN, -1, Integer::sum);
if (open == 0) {
stateCounts.remove(DifferenceState.OPEN);
}
stateCounts.merge(
DifferenceState.ESCALATED,
1,
Integer::sum);
diagnostics.put(key, escalated);
return key.label();
}
private String deltaOf(DifferenceKey key) {
ReconciliationDifference difference = byKey.get(
Objects.requireNonNull(key, "key"));
return difference == null
? "MISSING"
: Long.toString(difference.deltaCents());
}
private String diagnosticState(DifferenceKey key) {
ReconciliationDifference difference = diagnostics.get(
Objects.requireNonNull(key, "key"));
return difference == null ? "MISS" : difference.state().name();
}
private List<String> scheduleOrder() {
return upgradeSchedule.entrySet().stream()
.map(entry -> entry.getValue().label()
+ "@" + entry.getKey().dueMinute()
+ "#S" + entry.getKey().severity())
.toList();
}
private List<String> diagnosticOrder() {
return diagnostics.keySet().stream()
.map(DifferenceKey::label)
.toList();
}
private List<String> countSummary() {
return stateCounts.entrySet().stream()
.map(entry -> entry.getKey() + "=" + entry.getValue())
.toList();
}
}
public static void main(String[] args) {
DifferenceKey merchantAFirst =
new DifferenceKey("merchant-a", "D-41");
DifferenceKey merchantBFirst =
new DifferenceKey("merchant-b", "D-41");
DifferenceKey merchantASecond =
new DifferenceKey("merchant-a", "D-42");
DifferenceKey merchantDFirst =
new DifferenceKey("merchant-d", "D-9");
ReconciliationDifference first = new ReconciliationDifference(
merchantAFirst, 720, 2, 1_500, DifferenceState.OPEN);
ReconciliationDifference second = new ReconciliationDifference(
merchantBFirst, 700, 4, -800, DifferenceState.OPEN);
ReconciliationDifference third = new ReconciliationDifference(
merchantASecond, 700, 5, 250, DifferenceState.OPEN);
ReconciliationDifference fourth = new ReconciliationDifference(
merchantDFirst, 740, 3, 4_000, DifferenceState.OPEN);
DifferenceIndex index = new DifferenceIndex();
System.out.println("REGISTER first=" + index.register(first));
System.out.println("REGISTER second=" + index.register(second));
System.out.println("REGISTER third=" + index.register(third));
System.out.println("REGISTER fourth=" + index.register(fourth));
System.out.println("DIAG evicted-first="
+ index.diagnosticState(merchantAFirst));
System.out.println("DIAG access-second="
+ index.diagnosticState(merchantBFirst));
System.out.println("REGISTER duplicate=" + index.register(first));
System.out.println("BY-KEY merchant-a/D-41="
+ index.deltaOf(merchantAFirst));
System.out.println("BY-KEY merchant-b/D-41="
+ index.deltaOf(merchantBFirst));
System.out.println("DIAG before=" + index.diagnosticOrder());
List<String> beforeEscalation = index.dueSnapshot(700, 721);
System.out.println("WINDOW before=" + beforeEscalation);
System.out.println("COUNTS before=" + index.countSummary());
System.out.println("ESCALATE before-700="
+ index.escalateNext(700));
System.out.println("ESCALATE before-720="
+ index.escalateNext(720));
System.out.println("COUNTS after=" + index.countSummary());
System.out.println("SCHEDULE after=" + index.scheduleOrder());
System.out.println("DIAG after=" + index.diagnosticOrder());
System.out.println("SNAPSHOT unchanged=" + beforeEscalation);
}
}
三个待补位置的关键步骤
register 在任何 Map 写入前校验非空和 OPEN,再让事实表的 putIfAbsent 决定复合键是否首次出现。重复时立刻返回,避免调度覆盖、重复计数或借重复请求刷新缓存;首次成功后才维护三个派生投影。
dueSnapshot 用同一分钟内排在所有合法差异键之前的边界键表达半开区间。下界包含边界会收进下界分钟,上界排除边界会排除上界整分钟;按树顺序回查事实并生成字符串,返回列表不再背靠内部 Map。
escalateNext 在严格上界活视图中读取第一项,核对事实状态与完整导航键,再用新 record 替换事实。子视图删除写穿根树,计数从 OPEN 迁移到 ESCALATED;将新 value 写入 access-order 缓存会移动已有键或让已淘汰键重新进入。
精确编译与规定输出
javac --release 21 ReconciliationDifferenceChallenge.java
java ReconciliationDifferenceChallenge
运行输出必须为:
REGISTER first=true
REGISTER second=true
REGISTER third=true
REGISTER fourth=true
DIAG evicted-first=MISS
DIAG access-second=OPEN
REGISTER duplicate=false
BY-KEY merchant-a/D-41=1500
BY-KEY merchant-b/D-41=-800
DIAG before=[merchant-a/D-42, merchant-d/D-9, merchant-b/D-41]
WINDOW before=[merchant-a/D-42@700#S5:OPEN:250, merchant-b/D-41@700#S4:OPEN:-800, merchant-a/D-41@720#S2:OPEN:1500]
COUNTS before=[OPEN=4]
ESCALATE before-700=NONE
ESCALATE before-720=merchant-a/D-42
COUNTS after=[OPEN=3, ESCALATED=1]
SCHEDULE after=[merchant-b/D-41@700#S4, merchant-a/D-41@720#S2, merchant-d/D-9@740#S3]
DIAG after=[merchant-d/D-9, merchant-b/D-41, merchant-a/D-42]
SNAPSHOT unchanged=[merchant-a/D-42@700#S5:OPEN:250, merchant-b/D-41@700#S4:OPEN:-800, merchant-a/D-41@720#S2:OPEN:1500]
两个商户的 D-41 同时存在,证明复合身份没有串商户;正负差额保留原符号。同为 700 分钟时 S5 先于 S4;严格上界 720 排除截止恰为 720 的差异。第一条虽被缓存淘汰仍能从事实表查到;命中第二条和升级第三条分别改变访问热度;旧快照继续保留升级前的 OPEN。
时间与空间复杂度
register 的事实表条件写入期望 O(1),调度树插入 O(log n),计数与缓存更新为常数或期望 O(1),整次为 O(log n)。
dueSnapshot 的边界定位与 k 项投影为 O(log n + k),稳定字符串快照额外空间为 O(k)。
escalateNext 的候选定位和树删除为 O(log n),事实替换、计数迁移和缓存更新为常数或期望 O(1),整次为 O(log n)。
deltaOf 与诊断命中的期望时间为 O(1);scheduleOrder 遍历剩余候选为 O(n);固定容量缓存输出与固定枚举摘要按 O(1) 理解。
事实表与升级树的总空间为 O(n),固定状态表和有界缓存为 O(1)。这些结论不等于最坏碰撞、并发安全或跨 Map 事务保证。
边界复核
不同商户的同号差异并存,正负差额均为 value;完全相同复合键重复登记返回 false,且不能刷新缓存热度。
[720,720) 返回空快照;下界大于上界必须在读取或写入业务状态前失败。
escalateNext(700) 返回 NONE;严格上界 720 不包含截止恰为 720 的差异。
分钟和严重级别相同时,商户与单号仍补足全序;比较为 0 不能误覆盖不同差异。
快照列表不可增删,升级不会改写旧字符串;导航活视图则跟随根树删除。
缓存未命中不代表事实缺失;淘汰不删除事实或候选,升级已淘汰键时允许它重新进入缓存。
生产环境需要覆盖四张 Map 的共同所有权、锁、事务或补偿边界;fail-fast 与单 Map 条件 API 都不能替代它。
4. 今日变式前的最短自检
能否说明正负差额为何属于 value,而商户和单号为何组成稳定事实 key?
能否从截止分钟、严重级别、商户和单号推出全序,不借助 HashMap 或 HashSet 的遇见顺序?
能否逐步推演容量为 2 的 access-order 缓存,包括已淘汰 C 在升级时重新进入并淘汰 B?
能否区分严格上界 900、范围上界 901、旧活视图和旧不可变快照的结果?
能否比较 TreeMap 的 O(log n) 候选定位与未知节点位置时 LinkedList 的 O(n) 查找删除?
能否明确承认没有有效作答,所以 challenge.map 仍是 covered_unverified,今天必须继续 Map 而不能进入 Set?
如有一项含糊,先换一组租户策略键、版本生效时间和重排动作重新推演,再开始今天的限流策略变式。这里的完整参考答案始终只用于教学核对,不会被记录为大大的答案证据。
返回今日索引
返回今日索引
核心讲解:多租户限流策略版本切换中的 Map 合同与重排
Day 17|directionSession 15|challenge.map|checkpoint。建议用时 31~32 分钟。Day 16 没有有效作答,因此本课用于继续闯关取证:challenge.map 仍是 covered_unverified,方向仍为 active;阅读参考过程本身不等于通过。
1. 为什么需要:策略事实稳定,激活顺序却会改变
网关控制台允许不同租户都定义名为 api-core 的限流策略,同一租户也会同时保存多个候选版本。平台既要按“租户 + API + 版本”精确找到事实,又要按切换分钟和优先级挑选下一条待激活策略,还要保存最近预览的三个策略以及 SCHEDULED/ACTIVE 数量。真正困难的不是选出四种 Map,而是处理计划改变:运营人员可能把某个已提交版本从 15:00 提前到 14:30,并把优先级从 2 调成 4。
如果把可变的切换分钟直接放进事实键,改时间就等于改身份;如果把一个可变对象直接作为 TreeMap 键,字段改变后对象留在原树路径,比较器却认为它应在另一条路径,查找、删除和迭代会互相矛盾。可靠做法是把两个空间分开:事实键终身稳定,排序键只是当前事实的一份不可变投影。投影字段一旦变化,必须显式删除旧投影、替换不可变事实 value、插入新投影。
这也解释了今天为什么不能进入 Set:Map 闯关尚未获得“至少 80 分、Java 21 编码成功、无红线”的证据。第 14 次会话应有的阶段复习不能越过未通过的 Map 门禁。
2. 前置知识:先把九项 Map 能力放回各自边界
Map 的核心是键到值的映射合同。若允许 null value,get(key)==null 不能区分“缺键”和“存在但值为 null”;本题在边界拒绝 null 键和值,让 null 唯一表示缺失。
equals/hashCode 决定 HashMap 中的业务同一性。PolicyKey(tenantId, apiCode, version) 三个字段都不可变并共同参与相等判断,才能避免跨租户或跨版本覆盖。
HashMap 用桶定位事实,正常分布下精确访问期望 O(1),但扩容、碰撞链和树桶意味着单次并非严格 O(1),遇见顺序也不是业务合同。
TreeMap 用 Comparator 定义排序键的同一性与全序。比较为 0 就会被当成同一个树键,所以分钟、优先级、租户、策略编码都必须进入比较。
putIfAbsent、compute、merge 只约束当前 Map 的当前调用;映射函数还应避免修改同一 Map。它们不会自动覆盖另一张树、计数表或数据库。
keySet/values/entrySet 和 subMap/headMap 是背靠根容器的视图。Collections.unmodifiableMap 只是只读包装;跨边界需要历史快照时应复制数据,例如投影成字符串后用 toList() 固化。
insertion-order LinkedHashMap 表示登记先后,access-order LinkedHashMap 表示成功访问热度。容量限制是缓存策略,不是事实删除策略。
EnumMap 适合封闭状态域,按枚举声明顺序遇见键;WeakHashMap 受键可达性和 GC 影响,IdentityHashMap 用 ==,都不适合作为稳定业务事实表。
随堂检查 1:两个租户都有 api-core/v1,同一租户又有 v2,能否只用 apiCode 做事实键,再依靠 TreeMap 补出租户和版本?
**即时答案:**不能。事实表会先把多条策略覆盖成一条,后续索引无法恢复被覆盖事实。身份必须是不可变的 (tenantId, apiCode, version) 复合键。
3. 定义:四张 Map、两个键空间和一个共同不变量
设稳定事实键为 PolicyKey(tenantId, apiCode, version);设激活排序键为 ActivationKey(activationMinute, priority, tenantId, apiCode, version),比较顺序是切换分钟升序、优先级降序、租户升序、API 升序、版本升序。合法键上 compareTo==0 恰好意味着五个组件都相同。
四张 Map 的职责如下:
HashMap<PolicyKey, PolicyVersion> byKey 是唯一事实来源。value 是不可变 record,保存切换分钟、优先级、限流值和状态;版本属于稳定事实键。
TreeMap<ActivationKey, PolicyKey> activationSchedule 只保存 SCHEDULED 候选,是可重建的排序投影。
容量为 3 的 access-order LinkedHashMap<PolicyKey, PolicyVersion> previews 是诊断预览缓存;命中或已有键更新会移到尾部,淘汰只影响缓存。
EnumMap<PolicyState, Integer> stateCounts 是状态计数投影,枚举声明顺序为 SCHEDULED, ACTIVE。
核心不变量是:每个待激活事实必须恰好对应一条由当前 value 计算出的 TreeMap 项;每条 TreeMap 项必须能回查同一个事实键;计数等于事实状态的聚合;缓存可以缺项,但缓存中的 value 不应冒充事实来源。四个容器一起满足不变量,任何单个容器“写成功”都不能证明整次版本切换成功。
4. 心智模型:主账、日程卡、三席预览台、状态牌
把 byKey 想成主账:tenant-a/search/v7 无论何时切换,身份都不改变;若另有 v8,它是另一行事实。TreeMap 像按时间排序的日程卡,每张卡抄写当前切换分钟和优先级;改计划不是在卡片入树后拿笔涂字段,而是取出旧卡、生成新事实 value、放入一张新卡。预览缓存只有三个座位,谁最近成功预览或被更新,谁坐到末席;被挤走只代表不再缓存。EnumMap 是状态牌,只汇总主账状态。
因此一次重排的逻辑顺序是:先从主账取得旧 value,并由它重建旧 ActivationKey;确认旧日程卡存在且指向同一事实键;构造新的不可变 value;在同一外层一致性边界内删除旧卡、替换主账、插入新卡,再更新预览投影。若先改主账再从新 value 计算“旧键”,删除会落空,旧卡将成为幽灵候选。
随堂检查 2:旧计划是 900#P2,新计划是 870#P4。应该用哪个键调用 activationSchedule.remove?删除返回 null 又意味着什么?
**即时答案:**必须用旧 value 计算出的 900#P2 完整键。返回 null 表示事实与派生索引已经失配,不能若无其事地再插新键;应在共同原子边界内失败、回滚或触发重建/补偿。
5. 完整数值与状态推演:提前切换怎样让树真正重排
假设四条待切换策略为:
A:tenant-a/search/v7,切换 900 分钟,P2,100 次/秒。
B:tenant-b/search/v3,切换 880 分钟,P5,50 次/秒。
C:tenant-a/report/v11,切换 880 分钟,P1,20 次/秒。
D:tenant-c/pay/v2,切换 930 分钟,P3,200 次/秒。
四次首次登记后,主账大小为 4,状态计数为 {SCHEDULED=4}。TreeMap 比较顺序为 [B@880#P5, C@880#P1, A@900#P2, D@930#P3]:B、C 同分钟时优先级高的 B 在前,身份尾字段又保证任意同分钟同优先级策略仍可并存。若预览缓存依次放入 A、B、C,随后命中 B,再放入 D,热度变化是 [A,B,C]→[A,C,B]→[C,B,D];A 被淘汰却仍存在于主账和日程树。
此时取得覆盖 B 到旧 A 的导航活视图,并固化字符串快照,两者最初都是 [B,C,A]。现在运营保持 A 的事实身份 tenant-a/search/v7 不变,把计划改为 870 分钟、P4;限额仍为 100 次/秒。正确转换逐步为:
以稳定 PolicyKey 从主账读出旧 A,重建旧键 A@900#P2。
从树中删除旧键,返回值必须是 A;树暂为 [B,C,D]。
新建 value A(870,P4,100,SCHEDULED),替换主账旧 value。相等事实键更新不增加 HashMap 的 size;版本没有被偷偷改成另一条身份。
从新 value 生成 A@870#P4 并插树,树变为 [A,B,C,D];这才是真正的重排。
用新 value 更新预览。A 原先已被淘汰,所以插入末尾并挤走 C,缓存成为 [B,D,A];状态仍是 SCHEDULED=4。
重排后,旧导航活视图变为 [B,C]:旧 A 已删除,新 A 又落在视图下界之前。旧字符串快照仍为 [B,C,A@900#P2],因为它拥有复制时刻的数据。接着执行“只激活分钟严格小于 880 的首项”,A@870 被选中,B、C@880 都因严格上界而排除。A 从树删除,主账以 ACTIVE 新 value 替换,计数变为 {SCHEDULED=3, ACTIVE=1},缓存更新已有 A 仍把它置于尾部 [B,D,A]。最终树为 [B,C,D]。
复杂度也由整次操作推导:重排含一次 TreeMap 删除和一次插入,为 O(log n),HashMap 与缓存访问通常为期望 O(1),EnumMap 为固定键域常数规模,因此整次仍由树操作主导为 O(log n)。复制包含 k 项的窗口是 O(log n+k) 时间和 O(k) 额外空间。四张 Map 的常数次调用不会神奇地变成事务。
反例推演同样重要:若先把主账 A 替换成 870/P4,再用“当前 A”计算要删除的键,就会尝试删除尚未存在的 A@870#P4;随后插入它,树同时留下 A@900#P2 与 A@870#P4。下一次领取旧卡会回查到新事实,导航键与事实不符。这个错误不是排序偶然性,而是不变量已经被破坏。
6. 源码映射:从固定入口解释可观察结果
固定基线是 Java 21、OpenJDK jdk-21+35。阅读源码遵循“问题 → 入口类/方法 → 关键字段 → 主调用链 → 扩展点 → 调试练习”,私有实现只用于解释这一版本的 API 现象,不能升级为业务合同。
6.1 HashMap:相等键为何替换,扩容时碰撞节点为何可能分开
**问题:**等值的新 PolicyKey 为什么返回旧 value 且 size 不增?
入口类/方法: HashMap.get/put/remove;关键字段: table、size、threshold、loadFactor、modCount。
主调用链: get→getNode→桶首/链/树匹配;put→putVal→相等键替换或新增;新增后超过阈值再进入 resize;remove→removeNode→摘除节点。
**扩展点:**容量 8、阈值 6、size 5 时,新增第 6 个键不扩容;相等键替换仍是 6;再新增第 7 个才超过阈值并扩到 16。散列值低位索引同为 3、但一个带旧容量位 8 的两节点,在新表中可留在 3 或移动到 11。桶达到树化请求条件但表容量小于 64 时,固定实现优先扩容而不是立即树化。
**调试练习:**断点观察相等键更新前后 size/modCount,再观察新增节点触发 resize;不要从 table 迭代结果推导业务顺序。
6.2 TreeMap:为什么只能“删旧键再插新键”
**问题:**比较字段变化时,怎样保持红黑树搜索路径与全序一致?
入口类/方法: TreeMap.put/remove/subMap/headMap;关键字段: root、size、comparator、modCount 与范围端点。
主调用链: put→沿 comparator 逐节点下降→compare==0 替换或新增→插入平衡;remove→按旧键比较定位→删除节点→删除平衡;范围方法返回带边界检查、写穿根树的导航视图。
**扩展点:**优先级降序可用 Integer.compare(other.priority, priority),而不能用相减;尾部租户与策略编码防止不同策略比较为 0。已入树键必须不可变。
**调试练习:**在删除旧 A 前后检查 remove 返回值和树序列;故意用新键删除一次,观察旧项为何仍能迭代却无法代表当前事实。
6.3 LinkedHashMap、EnumMap 与视图:顺序和所有权从哪里来
**问题:**为什么成功预览会改缓存顺序,状态表又为何按枚举声明顺序输出?
入口类/方法: LinkedHashMap.get/put、EnumMap.get/put/merge、Map 的集合视图;**关键字段:**前者的 head、tail、accessOrder,后者的 keyType、keyUniverse、vals、size。
**主调用链:**访问顺序表中 get→getNode→afterNodeAccess→命中节点移到尾部;插入后进入 afterNodeInsertion,子类可用 removeEldestEntry 淘汰最老项。EnumMap 先校验枚举类型,再由 ordinal 定位数组槽。迭代器以 expectedModCount 对照 modCount,只能尽力发现结构修改误用。
扩展点: entrySet 与导航子图是活视图;只读包装仍随根表变化;复制成独立列表才获得时间点所有权。
**调试练习:**在 access-order 表命中中间键,观察 modCount、首尾链及旧迭代器;得到异常也不能宣称容器线程安全。
官方源码直链:HashMap.java 、LinkedHashMap.java 、TreeMap.java 、EnumMap.java 。API 合同见 Java SE 21 Map 与 NavigableMap 。
7. 真实后端应用:配置发布、崩溃恢复与并发边界
真实限流平台通常把数据库或配置中心作为持久事实源,内存 Map 是单进程读优化投影。从 v7 发布 v8 时,两者应是两个不同 PolicyKey;数据库还可对“当前生效版本”做期望版本条件更新。今天的重排只修改某个尚未激活版本的计划字段,不改它的身份。若只依次调用四张普通 Map,线程可能在“旧树项已删、主账尚未替换”时读到空窗,进程也可能在“主账已换、树尚未插入”时崩溃。
可选边界取决于系统需求:低并发单分片可由同一线程串行拥有整个聚合;单进程并发可用覆盖四张 Map 的共同锁,读路径也遵守同一规则;读多写少可构建包含四个不可变投影的新聚合并一次交换引用;需要跨进程恢复则让数据库事务与版本号承担事实原子性,内存索引可由事件日志或主账重建。把 byKey 换成 ConcurrentHashMap 只能改善该表的特定单键操作,不会保护 TreeMap、EnumMap、预览缓存,更不会让跨策略切换自动原子。
随堂检查 3:对 byKey.compute(policyKey, ...) 加上 ConcurrentHashMap,能否保证旧调度项删除、事实替换、新调度项插入和计数变化一起成功?
**即时答案:**不能。单键 compute 的原子边界只属于那张 Map 的一个键;必须再选择共同锁、单线程所有者、不可变聚合交换或持久化事务,并为投影失配准备重建或补偿。
8. 错误示例:把身份、排序、缓存和事务混成一个概念
factKey = apiCode // 漏 tenantId/version,跨事实覆盖
treeKey = mutablePolicy // 入树后修改 minute/priority
compare = activationMinuteOnly // 同分钟策略 compare==0,静默替换
facts.put(key, newValue)
schedule.remove(keyFrom(newValue)) // 用新投影删旧项,留下幽灵键
report = unmodifiableMap(schedule) // 只读包装仍随根树变化
cache.removeEldestEntry -> facts.remove(...) // 缓存淘汰反向删除事实
catch ConcurrentModificationException // 把 fail-fast 当并发控制
另一个隐蔽错误是用 LinkedList 保存时间顺序,然后声称“中间删除 O(1)”。只有已经持有目标节点或 ListIterator 位置时局部链接修改才是 O(1);从业务键查找未知位置仍需 O(n)。本题用 TreeMap 是为了有界导航和完整排序合同,不是为了追求某次样例更短。
9. 正确示例:独立合同演示,而非三个 TODO 的答案
下面程序只手工展示等值事实、树重排、活视图、稳定快照、容量 3 的访问顺序缓存和 EnumMap 计数。它没有 reschedule、activationSnapshot、activateNext 服务方法,不处理输入命令,也没有给出今日三个 TODO 的实现;闯关仍需大大独立完成外层不变量检查、严格边界选择和失败处理。
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 PolicyProjectionContractsDemo {
private enum PolicyState {
SCHEDULED,
ACTIVE
}
private record PolicyKey(
String tenantId,
String policyCode,
long version) {
PolicyKey {
Objects.requireNonNull(tenantId, "tenantId");
Objects.requireNonNull(policyCode, "policyCode");
if (tenantId.isBlank() || policyCode.isBlank() || version <= 0) {
throw new IllegalArgumentException("invalid policy identity");
}
}
String label() {
return tenantId + "/" + policyCode + "/v" + version;
}
}
private record PolicyVersion(
PolicyKey key,
long activationMinute,
int priority,
int permitsPerSecond,
PolicyState state) {
PolicyVersion {
Objects.requireNonNull(key, "key");
Objects.requireNonNull(state, "state");
if (activationMinute < 0
|| priority < 1 || priority > 9
|| permitsPerSecond <= 0) {
throw new IllegalArgumentException("invalid policy version");
}
}
}
private record ActivationKey(
long activationMinute,
int priority,
String tenantId,
String policyCode,
long version) implements Comparable<ActivationKey> {
static ActivationKey from(PolicyVersion policy) {
return new ActivationKey(
policy.activationMinute(),
policy.priority(),
policy.key().tenantId(),
policy.key().policyCode(),
policy.key().version());
}
@Override
public int compareTo(ActivationKey other) {
int byMinute = Long.compare(
activationMinute, other.activationMinute);
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 byPolicy = policyCode.compareTo(other.policyCode);
return byPolicy != 0
? byPolicy
: Long.compare(version, other.version);
}
String label() {
return tenantId + "/" + policyCode + "/v" + version
+ "@" + activationMinute + "#P" + priority;
}
}
public static void main(String[] args) {
PolicyKey a = new PolicyKey("tenant-a", "search", 7);
PolicyKey b = new PolicyKey("tenant-b", "search", 3);
PolicyKey c = new PolicyKey("tenant-a", "report", 11);
PolicyKey d = new PolicyKey("tenant-c", "pay", 2);
PolicyVersion oldA = new PolicyVersion(
a, 900, 2, 100, PolicyState.SCHEDULED);
PolicyVersion policyB = new PolicyVersion(
b, 880, 5, 50, PolicyState.SCHEDULED);
PolicyVersion policyC = new PolicyVersion(
c, 880, 1, 20, PolicyState.SCHEDULED);
PolicyVersion policyD = new PolicyVersion(
d, 930, 3, 200, PolicyState.SCHEDULED);
Map<PolicyKey, PolicyVersion> facts = new HashMap<>();
facts.put(a, oldA);
facts.put(b, policyB);
facts.put(c, policyC);
facts.put(d, policyD);
NavigableMap<ActivationKey, PolicyKey> schedule = new TreeMap<>();
schedule.put(ActivationKey.from(oldA), a);
schedule.put(ActivationKey.from(policyB), b);
schedule.put(ActivationKey.from(policyC), c);
schedule.put(ActivationKey.from(policyD), d);
ActivationKey oldAKey = ActivationKey.from(oldA);
ActivationKey bKey = ActivationKey.from(policyB);
NavigableMap<ActivationKey, PolicyKey> liveWindow =
schedule.subMap(bKey, true, oldAKey, true);
List<String> snapshot = liveWindow.keySet().stream()
.map(ActivationKey::label)
.toList();
LinkedHashMap<PolicyKey, PolicyVersion> previews =
new LinkedHashMap<>(4, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<PolicyKey, PolicyVersion> eldest) {
return size() > 3;
}
};
previews.put(a, oldA);
previews.put(b, policyB);
previews.put(c, policyC);
previews.get(b);
previews.put(d, policyD);
EnumMap<PolicyState, Integer> counts =
new EnumMap<>(PolicyState.class);
counts.put(PolicyState.SCHEDULED, 4);
System.out.println("facts-size=" + facts.size());
System.out.println("schedule-before=" + labels(schedule));
System.out.println("window-before=" + labels(liveWindow));
System.out.println("cache-before=" + keyLabels(previews));
PolicyKey removed = schedule.remove(oldAKey);
PolicyVersion newA = new PolicyVersion(
a, 870, 4, 100, PolicyState.SCHEDULED);
facts.put(new PolicyKey("tenant-a", "search", 7), newA);
ActivationKey newAKey = ActivationKey.from(newA);
schedule.put(newAKey, a);
previews.put(a, newA);
System.out.println("reschedule-remove=" + removed.label());
System.out.println("schedule-after-reschedule=" + labels(schedule));
System.out.println("live-after-reschedule=" + labels(liveWindow));
System.out.println("snapshot-stable=" + snapshot);
System.out.println("cache-after-reschedule=" + keyLabels(previews));
System.out.println("counts-before=" + counts);
schedule.remove(newAKey);
PolicyVersion activeA = new PolicyVersion(
a, 870, 4, 100, PolicyState.ACTIVE);
facts.put(a, activeA);
counts.merge(PolicyState.SCHEDULED, -1, Integer::sum);
counts.merge(PolicyState.ACTIVE, 1, Integer::sum);
previews.put(a, activeA);
System.out.println("schedule-after-activate=" + labels(schedule));
System.out.println("state-a=" + facts.get(a).state());
System.out.println("counts-after=" + counts);
System.out.println("cache-after-activate=" + keyLabels(previews));
}
private static List<String> labels(
Map<ActivationKey, PolicyKey> schedule) {
return schedule.keySet().stream().map(ActivationKey::label).toList();
}
private static List<String> keyLabels(
Map<PolicyKey, PolicyVersion> policies) {
return policies.keySet().stream().map(PolicyKey::label).toList();
}
}
预期输出:
facts-size=4
schedule-before=[tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1, tenant-a/search/v7@900#P2, tenant-c/pay/v2@930#P3]
window-before=[tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1, tenant-a/search/v7@900#P2]
cache-before=[tenant-a/report/v11, tenant-b/search/v3, tenant-c/pay/v2]
reschedule-remove=tenant-a/search/v7
schedule-after-reschedule=[tenant-a/search/v7@870#P4, tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1, tenant-c/pay/v2@930#P3]
live-after-reschedule=[tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1]
snapshot-stable=[tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1, tenant-a/search/v7@900#P2]
cache-after-reschedule=[tenant-b/search/v3, tenant-c/pay/v2, tenant-a/search/v7]
counts-before={SCHEDULED=4}
schedule-after-activate=[tenant-b/search/v3@880#P5, tenant-a/report/v11@880#P1, tenant-c/pay/v2@930#P3]
state-a=ACTIVE
counts-after={SCHEDULED=3, ACTIVE=1}
cache-after-activate=[tenant-b/search/v3, tenant-c/pay/v2, tenant-a/search/v7]
10. 边界总结:闯关时必须守住的六条红线
HashMap、HashSet 没有稳定业务顺序;激活顺序来自完整 TreeMap 比较器,预览热度来自明确的 access-order LinkedHashMap。
事实键的 equals/hashCode 必须使用同一组不可变身份字段;Comparator 必须形成全序,已经入树的比较字段不得改变。可变计划必须“删旧键、换不可变 value、插新键”。
活视图会随根容器变化,只读包装也不等于不可变集合;跨边界历史结果要复制,并说明复制的是 value、字符串还是深层对象。
不知道节点位置时,LinkedList 中间查找或删除仍是 O(n),不能把局部改链的 O(1) 冒充完整业务操作成本。
fail-fast 是尽力发现迭代期间结构修改误用,不提供互斥、可见性或线程安全;access-order 的成功读取本身还可能改变结构。
ConcurrentHashMap 不会让跨键、跨集合或跨数据库操作自动原子。版本切换必须由共同锁、单线程所有者、不可变聚合交换或持久化事务覆盖,并核对旧 TreeMap 删除返回值。
普通 put/compute/merge 的返回值是状态转换证据,而不是可忽略的装饰;缓存未命中不等于事实不存在;WeakHashMap 和 IdentityHashMap 也不能替代稳定业务身份。最终必须能同时解释“谁是事实、何时激活、为何重排、看到哪个时刻、哪里保证原子性”。只有提交完整闯关答案、Java 21 编译运行成功、总分至少 80 且无红线,challenge.map 才能从 covered_unverified 变为 mastered 并放行 Set。
返回今日索引
返回今日索引
编码闯关:多租户限流策略版本切换索引
评估项:challenge.map。建议用时:18~20 分钟。本题完整代码、Java 21 编译结果与运行输出必须提交;只写思路不能通过 Map 阶段闯关。
你要为 API 网关完成一个单线程限流策略索引。平台既要按“租户 + API + 版本”定位每份策略事实,又要按“计划激活分钟 + 优先级”决定版本切换顺序,还要维护状态计数和容量受限的预览缓存。与前几次只新增后领取的模型不同,本题增加了真正的重排操作:计划中的策略允许修改激活分钟与优先级,必须删除旧 TreeMap 投影,再用新的不可变 value 建立新投影,不能在树中遗留幽灵键。
1. 业务背景
同一租户、同一 API 可以并存多个版本;不同租户也可以使用相同 API 名和版本号。因此事实键必须完整包含 tenantId、apiCode 和 version,并且进入 Map 后不可改变。激活顺序先按 activateMinute 升序;同一分钟优先级越高越先激活;仍相同时再按租户、API 和版本补足全序。这个顺序来自业务合同,不能使用 HashMap 的偶然遍历顺序代替。
四张 Map 分别承担:
byKey:唯一事实来源,以不可变 PolicyKey 精确定位一个策略版本。
activationSchedule:只保存 SCHEDULED 策略的排序投影,支持重排、半开窗口和严格上界首项。
stateCounts:用 EnumMap 统计封闭状态域 SCHEDULED/ACTIVE。
previewCache:容量为 3 的 access-order LinkedHashMap;提交、重排和激活会写入最新 value,预览命中也会升温,超限只淘汰缓存项。
种子数据通过已经实现的 submit 写入,以便把编码时间集中在重排、快照和激活三个难点。四张 Map 仍不是事务:练习的单线程顺序只能避免并发交错,不能抵抗进程在“删旧排序键、换事实 value、插新排序键”中途退出。生产环境必须让共同锁、单线程事件所有者、不可变聚合状态交换或数据库事务覆盖完整转换,并准备从事实重建派生索引。
2. 约束与验收边界
基线为 Java 21;只使用 JDK 集合,不添加依赖、日志或无法恢复问题的 try/catch。
PolicyKey 是不可变 record;租户和 API 非空白,版本为正数。激活时间、优先级、限额和状态只属于 value。
新提交策略必须为 SCHEDULED;activateMinute 非负,优先级限定 1..9,每分钟配额为正数。
已实现的 submit 对复合键去重;重复提交返回 false,不能修改调度、计数或缓存热度。
reschedule(key,newMinute,newPriority) 仅允许修改存在且仍为 SCHEDULED 的策略;缺失或已激活返回 false 且无副作用。成功时必须移除旧 ActivationKey,构造新的不可变策略 value,再重建排序投影并让缓存项重新进入或升温。
ActivationKey.compareTo 已给出:激活分钟升序、优先级降序、租户/API/版本升序;合法业务键上,比较为 0 与五个组件相等一致。
activationSnapshot(fromInclusive,toExclusive) 必须从 [fromInclusive,toExclusive) 的 TreeMap 活视图投影出不可变字符串快照,不能扫描事实 HashMap。
activateNext(beforeExclusive) 只激活分钟严格小于上界的第一个 SCHEDULED 候选;没有候选返回 NONE。
缓存淘汰不删除事实或调度项;重排已被淘汰的策略会重新放入缓存并可能淘汰另一缓存项。
禁止依赖 HashMap 顺序、破坏键或 Comparator 合同、混淆活视图/只读包装/快照、把 fail-fast 当线程安全,或把跨 Map 操作称为自动事务。
3. 目标拆解与实施顺序
阅读已实现的 submit,先确认事实新增成功后才建立调度、计数和缓存投影;重复分支必须立即结束。
完成重排:验证边界与当前状态,保存旧排序键,移除旧投影,构造并替换不可变 value,插入新排序键,最后更新预览缓存。
完成快照:用两个边界键获得半开导航视图,按当前激活顺序固化所有字段。
完成激活:用严格上界视图选择首项,核对事实和排序投影,再迁移状态、移除调度项、更新计数与缓存。
用 Java 21 编译运行并逐行核对规定输出;提交完整代码、编译证据和输出。
4. 完整可编译的 Java 21 起始代码
保存为 RatePolicySwitchChallenge.java。未补代码时仍能通过编译;直接运行会完成种子提交,然后在第一次重排时以 TODO 1: reschedule 明确失败。起始代码只有 3 个待补位置,当天不提供完整实现。
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) {
// 删除旧排序投影,用新不可变 value 重建事实、调度和缓存。
throw new UnsupportedOperationException("TODO 1: reschedule");
}
private List<String> activationSnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive > toExclusive) {
throw new IllegalArgumentException(
"fromInclusive must be <= toExclusive");
}
// 将半开导航活视图投影为稳定、不可变的版本快照。
throw new UnsupportedOperationException(
"TODO 2: activationSnapshot");
}
private String activateNext(long beforeExclusive) {
if (beforeExclusive < 0) {
throw new IllegalArgumentException(
"beforeExclusive must be >= 0");
}
// 严格上界内激活首项,并迁移事实与派生投影。
throw new UnsupportedOperationException("TODO 3: activateNext");
}
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);
}
}
直接编译运行:
javac --release 21 RatePolicySwitchChallenge.java
java RatePolicySwitchChallenge
5. 三个待补位置的验收条件
位置 1:重排必须同时处理旧投影与新投影
key 为 null、新分钟为负或新优先级不在 1..9 时,在任何 Map 修改前失败。
缺失或状态已经是 ACTIVE 时返回 false,调度、计数和缓存均无副作用。
保存当前 ActivationKey 后,先以“旧排序键 + 当前事实键”条件删除旧投影;删除失败表示索引不一致,应明确失败而不是继续写入。
构造包含新分钟、新优先级的不可变 RatePolicy,条件替换事实 value,再插入新排序键并更新预览缓存。重排不改变 SCHEDULED 计数。
整段顺序依赖题设单线程所有权;生产中外层原子边界必须覆盖删旧、换事实、建新和缓存更新,不能把单次条件删除称作事务。
位置 2:半开激活窗口转稳定快照
通过两个 ActivationKey.boundary 和四参数 subMap 表达 [fromInclusive,toExclusive),不能遍历 HashMap 再排序。
按范围视图当前顺序回查事实,核对它仍为 SCHEDULED 且 ActivationKey.from(policy) 与视图键一致;索引失配应明确失败。
将每项投影为 snapshotLine() 并返回不可修改列表;后续激活或重排不能改变旧字符串内容。
[x,x) 合法且为空;下界大于上界必须在业务读取前失败。
位置 3:严格上界内激活第一项
用 headMap(ActivationKey.boundary(beforeExclusive), false) 建立严格上界活视图;无候选返回 NONE,所有 Map 不变。
回查首项事实,确认状态仍为 SCHEDULED 且完整排序键一致;不一致时明确失败。
以新的 ACTIVE record 条件替换事实,从活视图删除旧调度项;SCHEDULED 减一并在归零时移除,再合并增加 ACTIVE。
用激活后的 value 更新预览缓存;已有项应升温,已淘汰项可重新进入,但淘汰只作用于缓存。返回被激活的完整策略键标签。
6. 三级提示
一级提示:重排先保存旧键,不能只改 value
TreeMap 不会观察事实 value 的字段变化。先从旧事实构造旧 ActivationKey,再创建新事实和新排序键;成功路径中必须能证明旧键不再存在、新键恰好存在。
二级提示:边界键为什么使用极高优先级
同一分钟高优先级排前。边界键的优先级高于所有合法策略,因此排在该分钟真实键之前:包含下界边界能收进下界整分钟,排除上界边界能排除上界整分钟。
三级提示:三个顺序分别来自三个合同
激活顺序来自 ActivationKey 比较器,缓存热度来自 access-order,事实 HashMap 没有业务顺序。重排 A 会让它从旧树位置移动到新位置,也会让被淘汰的 A 回到缓存尾部;两个变化的原因不同。
7. 精确预期输出
补全三个待补位置后,实际输出必须逐行一致:
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]
验收时还要解释:第四次提交为何只淘汰 A 的缓存而不删事实;重复提交为何不能让 A 回到缓存;重排 A 为何从 520#P2 移到 510#P4 且计数不变;同为 500 时 C 为何先于 B;严格上界 510 为何排除重排后的 A;旧快照为何仍显示 C 为 SCHEDULED。精确输出不授权依赖任何 HashMap 遇见顺序。
8. 复杂度要求
byKey 精确查询、条件新增与条件替换的期望时间为 O(1);碰撞、扩容和树化使“每次严格 O(1)”不成立。
activationSchedule 单项插入、删除和边界导航为 O(log n);重排包含一次删除和一次插入,仍为 O(log n)。
窗口边界定位加 k 项固化为 O(log n + k),返回快照额外空间为 O(k)。
stateCounts 键域固定;previewCache 命中、已有键移动、插入及一次最老项淘汰均为常数或期望 O(1),缓存空间上限为 O(1)。
事实表与调度树总空间为 O(n)。提交、重排和成功激活均由 TreeMap 步骤主导为 O(log n),但复杂度结论不等于跨 Map 原子性。
9. 边界用例
补全后至少自行验证:
同租户同 API 的不同版本、以及不同租户的同 API/版本均可并存;事实键不会因重排而改变。
重复提交不增加 SCHEDULED,不覆盖排序项,也不能刷新已淘汰的预览缓存项。
缺失或 ACTIVE 策略重排返回 false 且无副作用;非法新分钟或优先级在任何写入前失败。
重排后旧 ActivationKey 不再存在,新键按分钟、优先级和全部身份字段参与全序;计数保持不变。
[510,510) 返回空快照;下界大于上界不读取或修改业务状态。
activateNext(500) 返回 NONE;严格上界 510 不包含激活分钟恰为 510 的 A。
激活后旧导航视图反映 C 被删除,旧字符串快照保持 C 的 SCHEDULED 状态;快照列表不可增删。
缓存重排、命中与淘汰不改变事实存在性;生产环境必须给重排和激活完整流程提供共同事务边界。
10. 闯关提交与自检
11. Java 21 与固定源码资料
返回今日索引
返回今日索引
Map 阶段闯关题:多租户限流策略版本切换索引
建议用时:约 8~9 分钟。请优先在 互动页面 作答,聊天提交仅作补充。当天不提供答案。 第 1~3、5 题提交结论与最短必要推理;第 4 题必须提交完整 Java 21 代码、编译证据和规定输出。
题号
固定维度
知识点 ID
分值
可能触发的红线
1
契约与选型
ds.map.contract、ds.map.equality、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
25
假设 HashMap/HashSet 顺序稳定;破坏 equals/hashCode 或 Comparator;混淆事实身份与可变排序字段
2
复杂度与状态推演
ds.map.hashmap-structure、ds.map.hashmap-put-get-remove-source、ds.map.hashmap-resize-treeify-iterator-source、ds.map.treemap-source
12
把期望复杂度当严格保证;认为未知节点位置时 LinkedList 中间操作天然 O(1);把 fail-fast 当线程安全
3
复杂度与状态推演
ds.map.contract、ds.map.compute-merge-views、ds.map.linkedhashmap-source、ds.map.treemap-source
13
混淆活视图、只读包装与快照;只改事实而遗留旧排序键;把缓存或多 Map 更新当事务
4
编码
ds.map.contract、ds.map.equality、ds.map.compute-merge-views、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
30
重排遗漏旧键、比较器或范围错误、计数误迁移;把单 Map 条件操作扩张成跨 Map 原子性
5
源码讲解
ds.map.hashmap-structure、ds.map.hashmap-put-get-remove-source、ds.map.hashmap-resize-treeify-iterator-source、ds.map.linkedhashmap-source、ds.map.treemap-source、ds.map.specialized
20
猜测源码、把实现当公共合同、把 fail-fast 当线程安全;误认为 ConcurrentHashMap 跨键或跨集合自动原子
合计
契约与选型 25;复杂度与状态推演 25;编码 30;源码讲解 20
覆盖 Map 9 个知识项
100
闯关放行必须同时满足:总分至少 80/100、第 4 题 Java 21 代码成功编译运行且产生规定输出、六类红线均未命中。只达到分数、只交代码片段、编译失败或命中任一红线,都不能进入 Set;应依据有效证据补强并重闯。
1. 契约与方案设计:版本身份、排序投影与所有权(25 分)
限流平台要求按“租户 + API + 版本”精确定位策略,按激活分钟和优先级切换版本,统计有限状态,并维护容量为 3 的预览访问缓存。用最多 8 条写出四张 Map 的选型和事实/派生关系,同时定义:PolicyKey 的相等与 null 边界、可变字段为什么不能进入事实键、ActivationKey 的全序、access-order 与淘汰、重排旧/新投影、导航视图和对外快照所有权。解释为什么 HashMap/HashSet 顺序不能排激活队列,以及 WeakHashMap、IdentityHashMap 不适合持久策略事实。
2. 复杂度与结构推演:重排不是一次 HashMap 替换(12 分)
设事实表有 n 个策略版本,窗口命中 k 个。给出精确查询、提交、重排、首项激活、范围转快照、枚举计数与有界预览缓存操作的时间复杂度和空间复杂度。解释重排为何包含 TreeMap 的旧键删除与新键插入、仍为 O(log n);说明碰撞、扩容与 OpenJDK 21 树化容量门槛为何否定 HashMap“每次严格 O(1)”。再比较未知候选位置时用 LinkedList 查找/删除与 TreeMap 导航的成本,并指出 fail-fast 没有提供的线程安全性质。
3. 状态推演与代码分析:重排、缓存重新进入与严格上界(13 分)
预览缓存容量为 2。依次提交 A=tenant-a/orders/v1@640#P2、B=tenant-b/orders/v1@620#P1、C=tenant-a/pay/v2@620#P4;随后把已被缓存淘汰的 A 重排为 630#P5,再提交 D=tenant-c/report/v1@660#P3。接着取得 [620,641) 的导航活视图并投影成不可变字符串快照,最后执行 activateNext(630)。
逐步写出调度树和预览缓存从每次提交、A 重排、D 提交到激活后的顺序;写出被激活键、最终 SCHEDULED/ACTIVE 计数、事实表中 A 的新 value、旧 A 排序键是否存在、旧活视图当前内容和旧快照内容。说明同分钟 P4/P1 顺序、严格上界 630 为什么排除 A、激活一个已被缓存淘汰的 C 如何重新进入并触发淘汰,以及为何“删旧键 + 换事实 + 建新键 + 缓存更新”仍不是自动事务。不得依据 HashMap 遍历顺序。
4. 必交编码:完成限流策略重排与激活(30 分)
补全 编码练习 的 3 个待补位置,提交完整 RatePolicySwitchChallenge.java、javac --release 21 RatePolicySwitchChallenge.java 的成功结果,以及 java RatePolicySwitchChallenge 的完整且逐行一致输出。再用不超过 6 句话说明重复提交、重排旧键清理、半开快照、严格激活上界、缓存重新进入和跨 Map 原子性边界。
评分拆分:重排与旧/新投影不变量 10 分;范围活视图转稳定快照 7 分;首项激活、计数迁移与缓存热度 7 分;Java 21 编译、规定输出和边界说明 6 分。缺少完整代码、编译失败或输出不符时,编码维度不能视为通过,整个 Map 闯关也不得放行。
5. 源码追踪:由固定入口解释重排后的现象(20 分)
固定版本为 OpenJDK jdk-21+35。用 5 行表格作答,每行包含“问题、公开入口、关键字段、最多 4 个箭头节点的主路径、可观察结果或扩展点”,分别覆盖:HashMap 精确查询以及新键/同键 value 替换/删除;阈值、扩容拆分、树化容量条件和迭代器结构修改检测;LinkedHashMap access-order 的命中、已有键更新与最老项钩子;TreeMap 的比较定位、删除旧排序键、插入新键、subMap/headMap 视图;EnumMap 枚举槽位与计数迁移。
每行 4 分。必须从源码路径回到本题的返回值、size、重排顺序、范围、缓存和状态结果;不能把方法名清单当答案,不能猜红黑树具体形状,也不能把内部阈值、fail-fast、普通 Map 或 ConcurrentHashMap 单键能力扩张成业务顺序、线程安全或跨键/跨 Map 事务。
返回今日索引
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) {
// 删除旧排序投影,用新不可变 value 重建事实、调度和缓存。
throw new UnsupportedOperationException("TODO 1: reschedule");
}
private List<String> activationSnapshot(
long fromInclusive,
long toExclusive) {
if (fromInclusive > toExclusive) {
throw new IllegalArgumentException(
"fromInclusive must be <= toExclusive");
}
// 将半开导航活视图投影为稳定、不可变的版本快照。
throw new UnsupportedOperationException(
"TODO 2: activationSnapshot");
}
private String activateNext(long beforeExclusive) {
if (beforeExclusive < 0) {
throw new IllegalArgumentException(
"beforeExclusive must be >= 0");
}
// 严格上界内激活首项,并迁移事实与派生投影。
throw new UnsupportedOperationException("TODO 3: activateNext");
}
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);
}
}
互动保存需要 JavaScript。请启用 JavaScript,并通过本地启动器或已部署的学习地址访问此页面。