06 加载票机制
前几章反复提到了 "加载票" 这个词。本章将它单独展开,从每一个 Ticket 的诞生到消亡,讲清楚 "票" 到底是什么、怎么用、怎么传播。
1 什么是加载票
加载票(Ticket)是一个用于标明区块加载原因的对象。它的核心数据结构非常简单:
一张票回答三个问题:为什么加载(type)、加载到什么程度(level)、加载哪里(argument)。
2 加载票类型全表
1.20.1 中一共有 8 种加载票类型:
2.1 各类型详解
START:MinecraftServer.prepareStartRegion() 在出生点周围以 radius=11(level = 22)添加。这张票在服务端启动后立即生效,并在生成完成后一直存在 —— 确保出生点区块永不卸载。
DRAGON:EnderDragonFight.tick() 在龙战期间以 radius=9(level = 24)加载末地主岛。战斗结束后移除。
PLAYER:这是最常见的票。由 NearbyChunkTicketUpdater 自动管理 —— 当玩家移动时,进入视距的区块获得 PLAYER 票,离开视距的区块失去。它不是 "每个玩家一张票",而是 "每个在视距内的区块各一张 PLAYER 票"。
FORCED:/forceload 通过 ChunkTicketManager.setChunkForced() 添加,level 固定为 ENTITY_TICKING(31),永不过期。只有当手动执行 /forceload remove 时才被移除。
LIGHT:光照系统在需要计算某个区块的光照时添加。这是一张临时票 —— 光照计算完成后自动过期。
PORTAL:实体穿过下界传送门时,PortalForcer.getPortalRect() 在目标维度添加。radius=3 意味着目标传送门周围 3 区块都被加载,level = 30(强加载)。300 gt(15 秒)的宽限期确保实体有足够时间穿过并站稳。
POST_TELEPORT:/tp 等传送操作后添加。radius 通常为 0 或 1,5 gt 后过期 —— 只保证传送落地的区块在短时间内可用。
UNKNOWN:当代码通过 World.getChunk() 等方式获取区块时自动添加。radius=0,1 gt 后过期。它的作用是防止区块在获取后立即被卸载—— 给了调用者至少 1 gt 的时间来完成对区块的操作。
3 票的添加与移除
3.1 添加票
radius 被转换为 level = 33 - radius。因此:
radius=0→ level=33(FULL)radius=1→ level=32(BLOCK_TICKING)radius=2→ level=31(ENTITY_TICKING)radius=11→ level = 22(远超强加载,足够覆盖出生点区域)
添加后,票被存入 ticketsByPosition—— 一个以 long 坐标索引的 SortedArraySet<ChunkTicket>。同一区块的所有票按 level 升序排列,getLevel() 返回集合中第一个(即 level 最小的,也就是最 "强" 的)票的 level。
3.2 移除票
移除票后,如果某区块的票集合变为空,该区块从 ticketsByPosition 中移除。然后 distanceFromTicketTracker 重新计算该区块的 level—— 当没有任何票时,level 变为 INACCESSIBLE + 1。
4 票的传播
一张票加到中心区块后,周围区块如何确定自己的加载等级?TicketDistanceLevelPropagator 负责这个计算。
传播规则严格遵循切比雪夫距离:
即:每向外走一个区块(包括对角线),level +1。
例如,一张 level = 31(强加载)的票加在 (0, 0):
35 34 33 34 35 ← 向外 2 格 = level 33
34 33 32 33 34
33 32 [31] 32 33 ← 中心 = level 31
34 33 32 33 34
35 34 33 34 35
TicketDistanceLevelPropagator 继承自 ChunkPosDistanceLevelPropagator,使用经典的 level 传播算法 ——propagateLevel 向 8 个邻居广播,recalculateLevel 取所有邻居的 min(level+1)。
5 三个追踪器的分工
ChunkTicketManager 维护了四个追踪器,各自负责一种 level 的计算:
6 票的过期与回收
ChunkTicketManager.purge() 每 tick 被调用一次,遍历所有票:
过期的判定基于 tickCreated 和票类型:
POST_TELEPORT:age - tickCreated >= 5UNKNOWN:age - tickCreated >= 1PORTAL:age - tickCreated >= 300PLAYER、FORCED、START、DRAGON:isExpired()始终返回false—— 不休眠
7 小结
- 8 种加载票类型覆盖了启动、玩家、传送门、指令、光照等各种加载原因。
- 票的核心属性是 level,同一区块取最低 level;level 通过切比雪夫距离向外传播。
- 票的过期时间因类型而异:
POST_TELEPORT(5gt)到永不过期(FORCED)。 - 三个追踪器各自负责 level 的传播和计算,共同驱动 ChunkHolder 的状态变化。
- 票系统不是引用计数,而是等级传播系统 —— 加一张 level = 31 的票比加三张 level = 33 的票更 "强"。
8 代码走读
8.1 ChunkTicket 比较器:SortedArraySet 与 "最强票优先"
ChunkTicket 实现 Comparable,按 level 升序排列 ——level 越小,票越 "强",排在 SortedArraySet 的最前面。ticketsByPosition 中每区块的 ticket 集合就是一个 SortedArraySet,getLevel() 直接取第一个元素的 level 作为该区块的综合加载等级。
为什么第二排序键用 identityHashCode 而不是 type 本身的比较? 因为 ChunkTicketType 没有实现 Comparable—— 设计者认为不同类型的票之间不应该有序,它们只是以不同的理由要求加载同一个区块。identityHashCode 是一个稳定的、不依赖字段值的 hash(由 JVM 为每个对象分配),保证了同一 JVM 运行期间相同 type 的票排序一致,且不需要 type 实现任何比较接口。
8.2 isExpired:基于超时而非引用计数的过期机制
为什么用超时而不是引用计数? 因为加载票的持有者(玩家位置、传送门逻辑、光照系统)可能在多个不同的代码路径中被添加和移除 —— 没有统一的 "计数器"。更重要的是,有些持有者可能 "悄悄地消失了" 而没有机会调用删除(如光照任务在异步线程上完成后线程直接回收)。超时机制保证了即使持有者忘记清理,票也会在过期后自动回收,避免死票堆积。
expiryTicks = 0 的设计意义: 用 0 表示 "永不过期",而不是 -1 或 Long.MAX_VALUE。isExpired 的条件是 l != 0L && ...—— 如果 expiryTicks = 0,l != 0L 为 false,整个表达式短路为 false。这是一个经典的哨兵值模式—— 不需要额外的 boolean 字段来标识 "是否过期",零值本身就是哨兵。
8.3 传播算法的设计
ChunkPosDistanceLevelPropagator 继承自 LevelPropagator—— 这是一个通用的数值传播框架,用于光照传播等场景。区块管理系统复用了这个框架来实现加载等级的传播。
ChunkPos.MARKER 是什么? 它是一个特殊的 long 值(1875066, 1875066 的打包),用作传播的虚拟源点。当 TicketDistanceLevelPropagator.setLevel() 被调用时,它不直接更新目标区块的 level—— 而是更新 ChunkPos.MARKER 到目标区块的传播路径。这样设计的原因:可能有多个区块都有 ticket(多个玩家在不同的块),如果直接更新每个区块的 level,在 ticket 变化时需要回退所有传播。通过一个虚拟 MARKER,所有 ticket 的 level 都被当作 "从 MARKER 到目标区块的传播起点",删除时只需去掉这条路径,不影响其他 ticket 的传播。
传播范围由 LevelPropagator 的构造函数参数控制:new LevelPropagator(maxLevel + 1, 16, 256)——16 是每轮处理的最大更新数,256 是 pending update 队列的容量。这两个参数限制了传播的性能开销:不能在单次 applyPendingUpdates 中处理太多区块(会阻塞 tick),也不能积压太多未处理的更新(会浪费内存)。
9 光照抑制现象
光照抑制器的效率、START 与 PLAYER 票的执行时序差异,以及光照任务积压如何阻塞区块进入 FULL,已独立为现象章节:见 光照抑制现象。
10 参考
- Discovering Minecraft(CC0 协议)
net.minecraft.server.world.ChunkTicketManagernet.minecraft.server.world.ChunkTicketnet.minecraft.server.world.ChunkTicketTypenet.minecraft.world.ChunkPosDistanceLevelPropagatornet.minecraft.world.SimulationDistanceLevelPropagator
