TSID(Time-Sorted Unique Identifier,时间排序唯一标识符)是一种既能保证全局唯一性、又能按时间自然排序的 64 位整数 ID 生成方案。Hypersistence TSID 库让 Java 开发者可以像用 UUID 一样简单地生成这种"带时间戳的身份证",既支持机器友好的 Long 类型,也支持人类可读的 13 位 Base-32 字符串,还能在多节点分布式环境下避免 ID 冲突。
为什么你的数据库主键需要"时间感"
想象一下,你在开发一个高并发的订单系统。用数据库自增 ID?单库没问题,分库分表后就会"撞车"。用 UUID?虽然不会重复,但完全随机,按时间查询订单时数据库索引会哭晕在厕所,而且 36 个字符的长度让索引文件膨胀得像气球。
TSID 就是来解决这个痛点的。它把 64 位整数切成三块:42 位存毫秒级时间戳、10 位存机器节点 ID、12 位存同一毫秒内的自增序列号。这意味着什么?首先,ID 天然按时间排序,你查"最近 24 小时的订单"时,数据库可以直接利用主键索引顺序扫描,性能拉满。其次,即使同一台机器在一毫秒内生成 4096 个 ID(2 的 12 次方),也不会重复。如果你有 1024 台服务器(2 的 10 次方),每台都配置不同的节点 ID,它们生成的 ID 永远不会打架。
Hypersistence TSID 这个库把这一切封装得极其简单。你不用去理解位运算,不用去算时间戳偏移量,几行代码就能开始生成 TSID。而且它是线程安全的,一个工厂实例可以安全地在整个应用生命周期复用。
解剖一个 TSID:它到底长什么样
让我们看一个真实的例子。假设你生成了一个 TSID,转换成 Long 是 38352658567418867。把它写成二进制,你会看到清晰的"三段式"结构:
最左边的 12 位是计数器(Counter),比如 000000001000,表示这是这一毫秒内生成的第 8 个 ID(从 0 开始)。中间的 10 位是节点 ID(Node ID),比如 1000100001,换算成十进制是 529,表示这是编号 529 的服务器生成的。剩下的 42 位是时间戳,存储的是从 Unix 纪元(1970-01-01)开始的毫秒数,比如 1692990591987,对应 2023 年 8 月 25 日 19:09:51.987 UTC。
这个设计最妙的地方在于"可配置性"。如果你的系统只有 4 台服务器,你可以把节点 ID 压缩到 2 位,把剩下的 20 位都给计数器,这样每毫秒能生成超过 100 万个唯一 ID。反过来,如果你有上千台服务器,可以把节点 ID 扩展到 20 位,计数器留 2 位,每毫秒保证 4 个唯一 ID就够了。这种灵活性让 TSID 能适应从初创公司到互联网大厂的各种规模。
三行代码上手:从依赖到生成
用 Maven 引入 Hypersistence TSID 只需要在 pom.xml 里加一段依赖,版本 2.1.4 是目前最新的稳定版。
<dependency> |
引入后,最简单的用法是直接拿单例工厂开干:TSID.Factory tsidFactory = TSID.Factory.INSTANCE;
然后调用 tsidFactory.generate()
就能得到一个新的 TSID 对象。
如果你想要更多控制,可以用建造者模式自定义工厂。比如 TSID.Factory.builder().withNode(42).build() 就把当前节点的 ID 设为 42。这个节点值也可以通过系统属性 tsid.node 或环境变量 TSID_NODE 来设置,这样你在 Kubernetes 里部署 100 个 Pod 时,每个 Pod 可以通过环境变量注入不同的节点 ID,完全不用改代码。
还有个"极速模式" TSID.fast(),它为了性能会忽略节点配置,计数器也不会随时间重置,而是无限递增。如果你在一个单节点、对 ID 的"语义"没有严格要求、但每秒要生成几百万 ID 的场景,这个模式能让你的代码飞起来。
序列化的艺术:Long 还是 String?
生成 TSID 后,你得决定怎么存、怎么传。
调用 TSID 的toLong() 会得到一个 64 位长整数,比如 809100737063473402。这玩意存数据库最省空间,做索引效率最高。但有个坑:如果你要把这个 ID 传给前端 JavaScript,JSON 数字类型在 JS 里会被当成 IEEE 754 双精度浮点数,精度会丢!超过 2 的 53 次方的整数,JS 就认不准了。
这时候 toString() 就派上用场了。它会返回一个 13 字符的 Base-32 编码字符串,比如 "0PEM0TNJ2SM7T"。这种格式人类能读、能抄、能念,而且完全避免了 JavaScript 的精度问题。你可以安全地把它放在 URL 里、JSON 里、甚至打印在快递单上。
反向解析也很简单。TSID.from(809100737063473402L)
或TSID.from("0PEM0TNJ2SM7T")
都能还原成原始的 TSID 对象。
更酷的是,你可以从 TSID 里提取时间戳:tsid.getInstant() 直接拿到 Java 8 的 Instant 对象,tsid.getUnixMilliseconds() 拿到毫秒数。
这意味着你不需要额外存"创建时间"字段,ID 本身就携带了这个信息。当然,节点 ID 和计数器是提取不出来的,因为 TSID 对象本身不存储位分配信息,这是设计上的取舍。
什么时候该用 TSID,什么时候不该用
TSID 最适合的场景是分布式系统的数据库主键,特别是当你需要按时间范围查询、排序时。比如电商订单、支付流水、日志记录、消息队列的消息 ID。它比 UUID 短(64 位 vs 128 位),比自增 ID 灵活(支持分库分表),还能顺便当时间戳用。
但如果你需要绝对的安全性,比如防止用户通过 ID 猜测出系统生成 ID 的速率或业务量,TSID 就不合适了,因为它的时间戳部分是暴露的。这时候你可能需要雪花算法(Snowflake)的变种,或者直接用完全随机的 UUID。
另外,如果你的系统对时钟同步要求极高,要注意 TSID 依赖系统时钟。如果服务器时钟回拨,可能会生成重复的 ID。虽然 Hypersistence TSID 内部有一些防护机制,但最好还是配合 NTP 服务保证时钟准确性。