1. 引言
本文将深入解析 Hi/Lo 算法,它是一种常用于数据库主键生成的策略,尤其在 ORM 框架(如 Hibernate)中被广泛采用。
我们会先介绍算法的核心思想,再通过 Hibernate 的实际示例演示其工作流程,最后分析它的适用场景、优点与潜在“踩坑”点。
目标是让你不仅“知道它是什么”,还能在项目中判断:✅ 什么时候该用,❌ 什么时候该避坑。
2. Hi/Lo 算法概述
2.1 基本概念
Hi/Lo 算法的核心目标是:在不频繁访问数据库的前提下,安全地生成唯一的主键 ID。
它通过三个关键变量协作完成:
incrementSize
:每批次可生成的 ID 数量,相当于“步长”。这个值一旦设定应保持不变,否则多实例环境下容易冲突。high
:来自数据库序列(sequence)的值,保证全局唯一。low
:当前批次内的递增计数器,范围是[0, incrementSize)
。
最终生成的 ID 公式为:
✅ (high - 1) * incrementSize + low + 1
生成范围是:[(high - 1) * incrementSize + 1, high * incrementSize)
举个例子,如果 high=2
,incrementSize=3
,那么可用 ID 范围是 [4, 6]
,即 4、5、6。
⚠️ 关键点:只有当 low >= incrementSize
时,才会去数据库取下一个 high
值。这意味着大部分 ID 都是在内存中生成的,减少了数据库压力。
2.2 算法伪代码
生成下一个 ID 的步骤如下:
- 如果
low >= incrementSize
:- 从数据库获取新的
high
值 - 重置
low = 0
- 从数据库获取新的
- 计算 ID:
(high - 1) * incrementSize + low + 1
low++
- 返回 ID
简单粗暴地说:先“批发”一批号段,再“零售”分配。
3. 实战示例(基于 Hibernate)
我们以 Hibernate 为例,看看 Hi/Lo 是如何配置和工作的。
实体定义
@Entity
public class RestaurantOrder {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hilo_sequence_generator")
@GenericGenerator(
name = "hilo_sequence_generator",
strategy = "sequence",
parameters = {
@Parameter(name = "sequence_name", value = "hilo_sequence"),
@Parameter(name = "initial_value", value = "1"),
@Parameter(name = "increment_size", value = "3"),
@Parameter(name = "optimizer", value = "hilo")
}
)
private Long id;
}
📌 关键配置说明:
参数 | 说明 |
---|---|
strategy = "sequence" |
使用数据库序列 |
optimizer = "hilo" |
启用 Hi/Lo 优化器 |
increment_size = 3 |
每次“批发”3个ID |
持久化操作
public void persist() {
Transaction transaction = session.beginTransaction();
for (int i = 0; i < 9; i++) {
session.persist(new RestaurantOrder());
session.flush(); // 强制触发ID生成
}
transaction.commit();
}
我们插入 9 条记录,incrementSize=3
,理论上只需要 3 次数据库调用来获取 high
值。
日志输出验证
Hibernate: call next value for hilo_sequence
org.hibernate.id.enhanced.SequenceStructure - Sequence value obtained: 1
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 1
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 2
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 3
Hibernate: call next value for hilo_sequence
org.hibernate.id.enhanced.SequenceStructure - Sequence value obtained: 2
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 4
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 5
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 6
Hibernate: call next value for hilo_sequence
org.hibernate.id.enhanced.SequenceStructure - Sequence value obtained: 3
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 7
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 8
org.hibernate.event.internal.AbstractSaveEventListener - Generated identifier: 9
✅ 完美印证:
- 第一次取
high=1
→ ID: 1~3 - 第二次取
high=2
→ ID: 4~6 - 第三次取
high=3
→ ID: 7~9
整个过程仅 3 次数据库交互,高效且可控。
4. 优缺点分析
✅ 优点
- 减少数据库往返次数:
incrementSize
越大,性能越高,尤其适合高并发场景。 - 适合弱网络环境:应用本地生成 ID,对数据库依赖低。
- ID 趋于连续:相比 UUID,更利于数据库索引优化(B+树插入效率更高)。
❌ 缺点(踩坑预警)
- ID 不连续且有空洞:如果应用重启,当前批次未用完的 ID 会丢失,造成“断号”。
- 多系统写入冲突风险:如果有其他服务(如脚本、第三方系统)直接往同一表插数据,并使用了自增或随机 ID,可能撞上 Hi/Lo 当前占用的号段,导致主键冲突。
- 难以预测下一个 ID:不适合需要“预知 ID”的业务场景(比如生成订单号前缀)。
📌 建议使用场景:
- 单一应用写入
- 高并发、低延迟要求
- ID 只用于主键,不参与业务逻辑
🚫 不建议使用场景:
- 多个异构系统共写一张表
- 需要严格连续 ID(如财务流水号)
- 分布式 ID 已有统一方案(如 Snowflake)
5. 总结
Hi/Lo 算法是一种经典的主键生成策略,通过“号段 + 内存分配”的方式,显著降低数据库压力,提升插入性能。
在 Hibernate 中配置简单,只需设置 optimizer = "hilo"
和合适的 increment_size
即可。
但也要注意它的局限性,特别是在多系统协作或对 ID 连续性有要求的场景下,容易“踩坑”。
示例代码已整理至 GitHub:https://github.com/baeldung/tutorials/tree/master/persistence-modules/hibernate5