[{"data":1,"prerenderedAt":19},["ShallowReactive",2],{"article-主键生成策略":3},{"id":4,"title":5,"slug":5,"description":6,"content":7,"coverImage":8,"tags":9,"category":6,"categoryName":6,"draft":10,"reviewStatus":11,"viewCount":12,"readingTime":13,"author":14,"createdAt":17,"updatedAt":18,"series":6},"846619960467629050","主键生成策略",null,"\n# 关系型数据库主键生成策略\n\n> 主键是关系型数据库表的行唯一标识。选择合适的主键生成策略直接影响写入性能、索引效率、以及分布式扩展能力。\n\n---\n\n## 1. 主键选型的核心考量\n\n### 1.1 评估维度\n\n| 维度 | 说明 | 为什么重要 |\n|------|------|-----------|\n| **有序性** | ID 是否趋势递增 | 有序 ID 减少 B+ 树页分裂，写入性能更高 |\n| **全局唯一** | 分布式环境下是否保证唯一 | 分库分表后单库自增不再唯一 |\n| **存储效率** | ID 占用字节大小 | 主键作为二级索引的叶子节点值，越短越省空间 |\n| **插入性能** | 批量插入是否支持 | IDENTITY 模式下 JDBC 批量插入失效 |\n| **可读性** | ID 是否携带业务信息 | 订单号、流水号需要语义化 |\n| **依赖程度** | 是否依赖外部组件 | 雪花算法纯本地生成，号段模式依赖数据库 |\n\n### 1.2 核心结论\n\n> **InnoDB 聚簇索引决定了主键必须是有序、短小的整型**。UUID 和无序雪花 ID 直接做主键会导致频繁页分裂，写入性能急剧下降。\n\n---\n\n## 2. 数据库原生机制\n\n### 2.1 MySQL — AUTO_INCREMENT\n\nMySQL 通过 `AUTO_INCREMENT` 属性实现自增主键，是 InnoDB 下最常用的方案。\n\n```sql\nCREATE TABLE t_user (\n    id BIGINT PRIMARY KEY AUTO_INCREMENT,\n    username VARCHAR(50) NOT NULL\n);\n\n-- 查看当前自增值\nSHOW TABLE STATUS LIKE 't_user';\n\n-- 手动设置起始值\nALTER TABLE t_user AUTO_INCREMENT = 10000;\n```\n\n**工作原理**：\n\n- InnoDB 为每张含有 `AUTO_INCREMENT` 列的表维护一个计数器，存储在内存中\n- MySQL 8.0 前，自增值持久化在 redo log 中（重启后可能回退）；8.0+ 改为写入数据字典，**重启不丢失**\n- 每次插入时自增值 +1，即使事务回滚，已分配的值也不会复用（存在空洞）\n\n**InnoDB 锁机制**：\n\n| 插入类型 | 锁策略 | 并发影响 |\n|---------|--------|---------|\n| 简单 INSERT（已知行数） | 轻量级互斥锁，分配后立即释放 | 高并发下无瓶颈 |\n| 批量 INSERT（未知行数） | `AUTO-INC` 表级锁，语句结束释放 | 并发性能差 |\n| `innodb_autoinc_lock_mode = 2` | 全部使用轻量级锁（交错模式） | 性能最好，但 binlog 为 STATEMENT 时主从不安全 |\n\n**局限性**：\n\n- 分库分表后无法保证全局唯一\n- 批量插入需要逐条获取 ID，JDBC `rewriteBatchedStatements` 无法优化 INSERT\n- 自增值在主从切换后可能冲突\n\n### 2.2 PostgreSQL — SEQUENCE \u002F SERIAL\n\nPostgreSQL 使用 **SEQUENCE**（序列对象）生成自增值，`SERIAL` \u002F `BIGSERIAL` 是语法糖。\n\n```sql\n-- 方式一：SERIAL 语法糖（自动创建序列 + 设置默认值）\nCREATE TABLE t_user (\n    id BIGSERIAL PRIMARY KEY,\n    username VARCHAR(50) NOT NULL\n);\n-- 等价于：\n-- CREATE SEQUENCE t_user_id_seq;\n-- id BIGINT PRIMARY KEY DEFAULT nextval('t_user_id_seq');\n\n-- 方式二：手动创建序列（更灵活）\nCREATE SEQUENCE t_user_seq START WITH 10000 INCREMENT BY 1;\n\nCREATE TABLE t_user (\n    id BIGINT PRIMARY KEY DEFAULT nextval('t_user_seq'),\n    username VARCHAR(50) NOT NULL\n);\n\n-- 批量分配：预取一批 ID\nSELECT nextval('t_user_seq') FROM generate_series(1, 100);\n```\n\n**相比 MySQL AUTO_INCREMENT 的优势**：\n\n| 特性 | MySQL AUTO_INCREMENT | PostgreSQL SEQUENCE |\n|------|---------------------|---------------------|\n| 预分配 | 不支持 | `ALTER SEQUENCE ... CACHE 1000` |\n| 步长调整 | 不支持（固定+1） | `INCREMENT BY N` 自定义步长 |\n| 起始值 | ALTER TABLE 修改 | `START WITH N` |\n| 循环使用 | 不支持 | `CYCLE` \u002F `NO CYCLE` |\n| 批量获取 | 不支持 | `nextval()` + `generate_series()` |\n| 与表耦合 | 是 | 否（独立对象，可多表共享） |\n\n**PostgreSQL 10+ 的 IDENTITY 列**：\n\n```sql\n-- 推荐写法（取代 SERIAL）\nCREATE TABLE t_user (\n    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n    username VARCHAR(50) NOT NULL\n);\n\n-- 优势：防止手动插入覆盖序列值\n-- GENERATED ALWAYS：禁止手动插入\n-- GENERATED BY DEFAULT：允许手动插入时覆盖\n```\n\n### 2.3 Oracle — SEQUENCE\n\nOracle 的 SEQUENCE 是功能最丰富的实现，也是 Oracle 数据库主键生成的标准方案。\n\n```sql\n-- 创建序列\nCREATE SEQUENCE t_user_seq\n    START WITH 1\n    INCREMENT BY 1\n    CACHE 500          -- 预缓存500个值，减少递归调用\n    NOORDER            -- 不保证顺序（RAC 环境下提升性能）\n    NOCYCLE;\n\n-- 使用序列\nINSERT INTO t_user (id, username) VALUES (t_user_seq.NEXTVAL, '张三');\n\n-- 获取当前会话最后一次 NEXTVAL 的值\nSELECT t_user_seq.CURRVAL FROM dual;\n```\n\n**RAC 集群下的特殊处理**：\n\n| 参数 | 说明 | 性能影响 |\n|------|------|---------|\n| `ORDER` | 保证全局有序（跨节点） | 性能较低，需要跨节点协调 |\n| `NOORDER` | 各节点独立分配，不保证全局有序 | 性能高，推荐用于非业务排序场景 |\n| `CACHE` | 每个节点缓存一段值 | 值越大，性能越好，但重启浪费越多 |\n\n### 2.4 三大数据库对比\n\n| 特性 | MySQL | PostgreSQL | Oracle |\n|------|-------|------------|--------|\n| 自增方式 | `AUTO_INCREMENT` | `SEQUENCE` \u002F `SERIAL` \u002F `IDENTITY` | `SEQUENCE` |\n| 预分配缓存 | 不支持 | `CACHE N` | `CACHE N` |\n| 步长自定义 | 不支持 | `INCREMENT BY N` | `INCREMENT BY N` |\n| 批量获取 ID | 不支持 | `generate_series` + `nextval` | 不原生支持 |\n| 集群支持 | 主从复制 | 流复制 \u002F 逻辑复制 | RAC（`ORDER`\u002F`NOORDER`） |\n| 有序性保证 | 单实例严格递增 | 单实例严格递增 | `ORDER` 模式下全局有序 |\n\n---\n\n## 3. UUID\n\n### 3.1 生成方式\n\n```java\n\u002F\u002F Java 原生 UUID（版本4，随机）\nUUID uuid = UUID.randomUUID();        \u002F\u002F 36字符：550e8400-e29b-41d4-a716-446655440000\nString id = uuid.toString().replace(\"-\", \"\");  \u002F\u002F 32字符：550e8400e29b41d4a716446655440000\n\n\u002F\u002F MySQL 内置 UUID\nSELECT UUID();  -- 550e8400-e29b-41d4-a716-446655440000\n\n-- MySQL 8.0+：有序 UUID（替换时间戳字节顺序）\nSELECT UUID_TO_BIN(UUID(), TRUE);  -- 16字节二进制，有序\n```\n\n### 3.2 UUID v7（2023年 RFC 9562）\n\nUUID v7 解决了 UUID v4 无序的问题，以毫秒级时间戳开头，**趋势递增**：\n\n```\nUUID v7 结构（128位）：\n| 48位毫秒时间戳 | 4位版本(0111) | 12位随机 | 2位变体 | 62位随机 |\n```\n\n```sql\n-- PostgreSQL 17+ 原生支持\nSELECT gen_random_uuid();  -- UUID v4\n-- UUID v7 需扩展或自定义函数\n\n-- MySQL 8.0 中生成有序 UUID\nSELECT UUID_TO_BIN(UUID(), TRUE) AS ordered_uuid;\n```\n\n### 3.3 为什么 UUID 不适合做 InnoDB 主键\n\n```\n问题链：\nUUID 无序 → 每次插入落到 B+ 树随机位置\n  → 频繁页分裂（Page Split）\n    → 大量随机 I\u002FO\n      → 写入性能严重下降\n\n此外：\nUUID 36字符 → 二级索引叶子节点存储主键值\n  → 索引膨胀（36字节 vs BIGINT 8字节）\n    → 内存利用率低，磁盘占用大\n```\n\n**性能对比数据**（参考 Percona 基准测试）：\n\n| 主键类型 | 插入 QPS | 表大小 | 二级索引大小 |\n|---------|----------|--------|-------------|\n| BIGINT AUTO_INCREMENT | ~25,000 | 100MB | 50MB |\n| BINARY(16) UUID | ~12,000 | 120MB | 80MB |\n| CHAR(36) UUID | ~8,000 | 160MB | 120MB |\n\n---\n\n## 4. 雪花算法（Snowflake）\n\n### 4.1 结构设计\n\n```\nSnowflake ID 结构（64位 = 8字节 = Java long）：\n\n 0 | 00000000 00000000 00000000 00000000 00000000 0 | 00000 00000 | 000000000000\n │  │                   41位时间戳                   │  10位机器ID  │   12位序列号  │\n符号位                                                │(5位数据中心+5位机器)│\n\n- 每毫秒可生成 4096 个 ID（单节点）\n- 可用 69 年（从纪元开始计算）\n- 支持 1024 个节点（32 个数据中心 × 32 台机器）\n```\n\n### 4.2 Java 实现\n\n```java\npublic class SnowflakeIdGenerator {\n    private final long epoch = 1609459200000L; \u002F\u002F 2021-01-01 起始纪元\n    private final long workerIdBits = 5L;\n    private final long datacenterIdBits = 5L;\n    private final long sequenceBits = 12L;\n\n    private final long maxWorkerId = ~(-1L \u003C\u003C workerIdBits);        \u002F\u002F 31\n    private final long maxDatacenterId = ~(-1L \u003C\u003C datacenterIdBits); \u002F\u002F 31\n    private final long sequenceMask = ~(-1L \u003C\u003C sequenceBits);        \u002F\u002F 4095\n\n    private final long workerIdShift = sequenceBits;                           \u002F\u002F 12\n    private final long datacenterIdShift = sequenceBits + workerIdBits;         \u002F\u002F 17\n    private final long timestampShift = sequenceBits + workerIdBits\n                                        + datacenterIdBits;                    \u002F\u002F 22\n\n    private final long workerId;\n    private final long datacenterId;\n    private long sequence = 0L;\n    private long lastTimestamp = -1L;\n\n    public SnowflakeIdGenerator(long workerId, long datacenterId) {\n        if (workerId > maxWorkerId || workerId \u003C 0) {\n            throw new IllegalArgumentException(\"worker Id error\");\n        }\n        if (datacenterId > maxDatacenterId || datacenterId \u003C 0) {\n            throw new IllegalArgumentException(\"datacenter Id error\");\n        }\n        this.workerId = workerId;\n        this.datacenterId = datacenterId;\n    }\n\n    public synchronized long nextId() {\n        long timestamp = System.currentTimeMillis();\n\n        \u002F\u002F 时钟回拨检测\n        if (timestamp \u003C lastTimestamp) {\n            throw new RuntimeException(\"Clock moved backwards\");\n        }\n\n        \u002F\u002F 同一毫秒内，序列号自增\n        if (lastTimestamp == timestamp) {\n            sequence = (sequence + 1) & sequenceMask;\n            if (sequence == 0) {\n                timestamp = tilNextMillis(lastTimestamp);\n            }\n        } else {\n            sequence = 0L;\n        }\n\n        lastTimestamp = timestamp;\n\n        return ((timestamp - epoch) \u003C\u003C timestampShift)\n                | (datacenterId \u003C\u003C datacenterIdShift)\n                | (workerId \u003C\u003C workerIdShift)\n                | sequence;\n    }\n\n    private long tilNextMillis(long lastTimestamp) {\n        long timestamp = System.currentTimeMillis();\n        while (timestamp \u003C= lastTimestamp) {\n            timestamp = System.currentTimeMillis();\n        }\n        return timestamp;\n    }\n}\n```\n\n### 4.3 时钟回拨问题\n\n**核心风险**：NTP 时间同步可能导致系统时钟回退，产生重复 ID。\n\n| 策略 | 做法 | 适用场景 |\n|------|------|---------|\n| 抛异常 | 回拨超过阈值直接报错 | 对唯一性要求极高 |\n| 等待追上 | 小幅回拨（\u003C50ms）时自旋等待 | 生产环境推荐 |\n| 备用 workerId | 回拨时切换到预分配的备用机器 ID | 高可用场景 |\n| 百度 UidGenerator | 改用未来时间填补，环形缓冲区 | 时钟回拨频繁的环境 |\n\n### 4.4 雪花 ID 与数据库主键\n\n> **关键问题**：雪花 ID 做主键好吗？\n\n雪花 ID 是**趋势递增**的（同一毫秒内严格递增，跨毫秒也递增），相比 UUID 好很多，但不如纯自增 ID 严格有序。对 InnoDB 聚簇索引来说：\n\n- **可以接受**：趋势递增，页分裂远少于 UUID\n- **非最优**：不如纯自增 ID 的顺序写入效率\n- **折中方案**：主键用 `BIGINT AUTO_INCREMENT`，雪花 ID 存业务字段（如 `order_no`）\n\n---\n\n## 5. 号段模式\n\n### 5.1 核心思想\n\n从数据库**批量预分配**一段 ID，应用在内存中消费，用完再取下一段。\n\n```\n号段模式工作流：\n\n  应用启动\n    ↓\n  从数据库获取号段 [1, 1000]（step=1000）\n    ↓\n  内存中分配：1, 2, 3, ..., 999\n    ↓\n  消耗到 10% 时，异步预加载下一段 [1001, 2000]\n    ↓\n  当前号段用完 → 无缝切换到备用号段\n```\n\n### 5.2 号段表设计\n\n```sql\nCREATE TABLE id_segment (\n    id BIGINT AUTO_INCREMENT PRIMARY KEY,\n    biz_tag VARCHAR(64) NOT NULL COMMENT '业务标识（如 order、user）',\n    max_id BIGINT NOT NULL DEFAULT 1 COMMENT '当前最大已分配 ID',\n    step INT NOT NULL COMMENT '步长（每次预分配的 ID 数量）',\n    version BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',\n    description VARCHAR(256) COMMENT '描述',\n    update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n                 ON UPDATE CURRENT_TIMESTAMP,\n    UNIQUE KEY uk_biz_tag (biz_tag)\n) ENGINE=InnoDB;\n```\n\n```sql\n-- 获取号段（乐观锁 + CAS 更新）\nUPDATE id_segment\nSET max_id = max_id + step,\n    version = version + 1\nWHERE biz_tag = 'order' AND version = #{currentVersion};\n\n-- 获取更新后的 max_id\nSELECT max_id, step FROM id_segment WHERE biz_tag = 'order';\n-- 号段范围：[max_id - step + 1, max_id]\n```\n\n### 5.3 双 Buffer 优化\n\n```\n  ┌──────────────┐     ┌──────────────┐\n  │  当前号段      │     │  备用号段      │\n  │  [1, 1000]    │     │  [1001, 2000] │\n  │  消耗中...     │     │  已预加载 ✓    │\n  └──────────────┘     └──────────────┘\n\n当前号段消耗到 10% → 触发异步加载备用号段\n当前号段用完 → 原子切换到备用号段 → 旧备用变为当前，开始加载新备用\n```\n\n**优势**：数据库短暂不可用时，应用仍可从缓存号段中分配 ID，提供缓冲期。\n\n---\n\n## 6. TSID（Time-Sorted ID）\n\n### 6.1 什么是 TSID\n\nTSID（Time-Sorted ID）是一种 64 位时间排序的唯一标识符，由 [Fabio Lima](https:\u002F\u002Fgithub.com\u002Ff4b6a3\u002Ftsid-creator) 设计，[Vlad Mihalcea](https:\u002F\u002Fgithub.com\u002Fvladmihalcea\u002Fhypersistence-tsid) 提供 Hibernate 集成。它结合了雪花算法的紧凑性和 ULID 的简洁性，是 Snowflake 的现代演进方案。\n\n**与雪花算法的核心区别**：\n\n| 维度 | 雪花算法 | TSID |\n|------|---------|------|\n| 机器 ID | **必须预分配** workerId \u002F datacenterId | 可随机生成，无需协调 |\n| 字符串表示 | 纯数字（如 `1541815603606036480`） | Crockford Base32 编码，13 字符（如 `0HJBZJM5V3YC8`） |\n| 单调性 | 同毫秒内序列号递增 | 同毫秒内计数器递增，**可跨越毫秒保持单调** |\n| 时钟回拨 | 直接抛异常 | 允许时间组件领先系统时钟 1ms+ 来维持单调性 |\n| 数据库存储 | `BIGINT` | `BIGINT`（一致） |\n\n### 6.2 结构设计\n\n```\nTSID 结构（64位 = Java long）：\n\n adjustable\n\u003C---------->\n----------┼----------┼----------┼----------\n time (42 bits)       node       counter\n                      (10 bits)  (12 bits)\n\n- time：毫秒时间戳，自 2020-01-01 起算\n  42 位 → 有符号 long 可用 ~69 年，无符号 ~139 年\n- node：节点标识，0~20 位可调（默认 10 位，支持 1024 节点）\n- counter：同一毫秒内的计数器，初始值随机\n  node bits + counter bits = 22 位（固定）\n  → node=10 时 counter=12，每毫秒 4096 个 ID\n  → node=0 时 counter=22，每毫秒 419 万个 ID（单机模式）\n```\n\n### 6.3 Java 使用\n\n**依赖**（两个主流实现任选其一）：\n\n```xml\n\u003C!-- 方式一：Hypersistence TSID（Vlad Mihalcea，提供 Hibernate 集成） -->\n\u003Cdependency>\n    \u003CgroupId>io.hypersistence\u003C\u002FgroupId>\n    \u003CartifactId>hypersistence-tsid\u003C\u002FartifactId>\n    \u003Cversion>2.1.3\u003C\u002Fversion>\n\u003C\u002Fdependency>\n\n\u003C!-- 方式二：TSID Creator（Fabio Lima，原版实现） -->\n\u003Cdependency>\n    \u003CgroupId>com.github.f4b6a3\u003C\u002FgroupId>\n    \u003CartifactId>tsid-creator\u003C\u002FartifactId>\n    \u003Cversion>5.2.6\u003C\u002Fversion>\n\u003C\u002Fdependency>\n```\n\n**基础用法**：\n\n```java\nimport io.hypersistence.tsid.Tsid;\n\n\u002F\u002F 快速生成（使用静态工厂）\nTsid tsid = Tsid.fast();\n\n\u002F\u002F 获取数值（存入数据库 BIGINT）\nlong id = tsid.toLong();           \u002F\u002F 如 809100737063473402\n\n\u002F\u002F 获取字符串（13字符，Crockford Base32）\nString str = tsid.toString();      \u002F\u002F 如 \"0PEM0TNJ2SM7T\"\n\n\u002F\u002F 从 ID 反推创建时间\nInstant createdAt = tsid.getInstant();\n```\n\n**自定义工厂配置**：\n\n```java\n\u002F\u002F 场景一：指定节点 ID（分布式环境多节点）\nint node = 256;   \u002F\u002F 最大值：2^nodeBits - 1\nTsid.Factory factory = Tsid.Factory.builder()\n        .withNodeBits(10)   \u002F\u002F 节点位：0~20 可调\n        .withNode(node)\n        .build();\n\n\u002F\u002F 场景二：单机模式（无需节点 ID，最大化每毫秒 ID 数）\nTsid.Factory factory = Tsid.Factory.builder()\n        .withNodeBits(0)    \u002F\u002F 无节点位，counter 占 22 位\n        .build();           \u002F\u002F 每毫秒可生成 419 万个 ID\n\n\u002F\u002F 场景三：自定义纪元\nInstant customEpoch = Instant.parse(\"2000-01-01T00:00:00Z\");\nTsid.Factory factory = Tsid.Factory.builder()\n        .withCustomEpoch(customEpoch)\n        .build();\n\n\u002F\u002F 场景四：模拟 Twitter Snowflake 结构\nint datacenter = 1, worker = 1;\nint snowflakeNode = (datacenter \u003C\u003C 5) | worker;\nTsid.Factory factory = Tsid.Factory.builder()\n        .withCustomEpoch(Instant.ofEpochMilli(1288834974657L))\n        .withNode(snowflakeNode)\n        .withRandomFunction(x -> new byte[x])  \u002F\u002F 计数器从 0 开始\n        .build();\n```\n\n### 6.4 JPA \u002F Hibernate 集成\n\n**方式一：`@PrePersist` 回调（最简单）**\n\n```java\n@Entity\n@Table(name = \"t_order\")\npublic class Order {\n\n    @Id\n    private Long id;\n\n    private String orderNo;\n\n    @PrePersist\n    public void prePersist() {\n        if (id == null) {\n            id = Tsid.fast().toLong();\n        }\n    }\n\n    \u002F\u002F 从 ID 反推创建时间（无需额外的 create_time 字段）\n    public Instant getCreatedAt() {\n        return Tsid.from(id).getInstant();\n    }\n}\n```\n\n**方式二：自定义 Hibernate `IdentifierGenerator`（更优雅）**\n\n```java\nimport io.hypersistence.tsid.Tsid;\nimport org.hibernate.engine.spi.SharedSessionContractImplementor;\nimport org.hibernate.id.IdentifierGenerator;\n\npublic class TsidGenerator implements IdentifierGenerator {\n\n    @Override\n    public Long generate(SharedSessionContractImplementor session, Object object) {\n        return Tsid.fast().toLong();\n    }\n}\n\n\u002F\u002F 实体中使用\n@Entity\n@Table(name = \"t_order\")\npublic class Order {\n\n    @Id\n    @GeneratedValue(generator = \"tsid-generator\")\n    @GenericGenerator(name = \"tsid-generator\", strategy = \"com.example.TsidGenerator\")\n    private Long id;\n}\n```\n\n**方式三：Spring Data JPA + `BeforeConvertCallback`（Spring Boot 3+）**\n\n```java\n@Component\npublic class TsidIdListener {\n\n    @BeforeConvert\n    public void assignId(Object entity) {\n        if (entity instanceof BaseEntity base && base.getId() == null) {\n            base.setId(Tsid.fast().toLong());\n        }\n    }\n}\n```\n\n### 6.5 TSID 字符串格式的优势\n\n```\n连续生成的 TSID 字符串序列：\n\n01226N0640J7K\n01226N0640J7M\n01226N0640J7N\n01226N0640J7P\n01226N0640J7Q\n01226N0640J7R\n01226N0640J7S\n01226N0640J7T\n01226N0693HDA  ← 毫秒切换\n01226N0693HDB\n01226N0693HDC\n...\n^^ ^          ^\n│  │          │\n│  │          └── 随机部分（node + counter）\n│  └── 时间戳部分（毫秒）\n└── 时间戳高位\n\n特点：\n- 字符串天然按时间排序（字典序 = 时间序）\n- 仅 13 字符（UUID v4 需要 36 字符）\n- 可安全用于 URL、日志、前端展示\n- 兼容 JavaScript（Number 精度丢失问题 → 使用字符串）\n```\n\n---\n\n## 7. 其他方案\n\n### 7.1 Redis INCR\n\n```bash\n# 原子自增\nINCR order:id          # 返回 1, 2, 3, ...\nINCRBY order:id 1000   # 一次分配 1000 个号段\n\n# 带过期时间（按天重置）\nSET order:20260525:id 0 EX 86400\nINCR order:20260525:id\n```\n\n| 优点 | 缺点 |\n|------|------|\n| 原子操作，高性能 | 依赖 Redis，Redis 故障则不可用 |\n| 灵活控制步长 | 持久化配置不当可能丢失（RDB\u002FAOF） |\n| 天然支持号段预分配 | 需要额外基础设施 |\n\n### 7.2 数据库集群步长偏移\n\n```sql\n-- 两个数据库实例，步长为 2\n-- 数据库 A：起始值 1，步长 2 → 生成 1, 3, 5, 7, ...\nSET @@auto_increment_increment = 2;\nSET @@auto_increment_offset = 1;\n\n-- 数据库 B：起始值 2，步长 2 → 生成 2, 4, 6, 8, ...\nSET @@auto_increment_increment = 2;\nSET @@auto_increment_offset = 2;\n```\n\n**局限**：扩容困难（需要修改步长和起始值），ID 不连续。\n\n---\n\n## 8. ORM 框架集成\n\n### 8.1 JPA \u002F Hibernate 主键策略\n\nJPA 通过 `@GeneratedValue` 注解定义主键生成策略，支持四种标准策略加 Hibernate 扩展：\n\n```java\n\u002F\u002F 策略一：IDENTITY — 使用数据库自增（MySQL AUTO_INCREMENT, PostgreSQL SERIAL）\n@Id\n@GeneratedValue(strategy = GenerationType.IDENTITY)\nprivate Long id;\n\n\u002F\u002F 策略二：SEQUENCE — 使用数据库序列（Oracle, PostgreSQL）\n@Id\n@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = \"user_seq\")\n@SequenceGenerator(name = \"user_seq\",\n                   sequenceName = \"t_user_seq\",\n                   allocationSize = 1,     \u002F\u002F 每次 nextval 调用分配的 ID 数\n                   initialValue = 1)\nprivate Long id;\n\n\u002F\u002F 策略三：TABLE — 用独立表模拟序列（跨数据库兼容，性能差）\n@Id\n@GeneratedValue(strategy = GenerationType.TABLE, generator = \"user_table_gen\")\n@TableGenerator(name = \"user_table_gen\",\n                table = \"id_generator\",\n                pkColumnName = \"gen_name\",\n                pkColumnValue = \"user_id\",\n                valueColumnName = \"gen_value\",\n                allocationSize = 50)\nprivate Long id;\n\n\u002F\u002F 策略四：UUID（Hibernate 扩展，JPA 标准不内置）\n@Id\n@GeneratedValue(generator = \"uuid2\")\n@GenericGenerator(name = \"uuid2\", strategy = \"uuid2\")\n@Column(columnDefinition = \"VARCHAR(36)\")\nprivate String id;\n```\n\n**allocationSize 与性能**：\n\n| allocationSize | 行为 | 性能影响 |\n|---------------|------|---------|\n| 1（默认） | 每次插入都调用 `nextval` | 高频数据库访问 |\n| 50 | 预分配 50 个 ID 到内存 | 减少数据库访问，**推荐设置** |\n\n> **重要**：`allocationSize` 必须与数据库序列的 `INCREMENT BY` 值一致，否则会导致主键冲突。\n\n```java\n\u002F\u002F 正确的配合使用\n\u002F\u002F 数据库端：CREATE SEQUENCE t_user_seq INCREMENT BY 50;\n\u002F\u002F JPA 端：\n@SequenceGenerator(name = \"user_seq\",\n                   sequenceName = \"t_user_seq\",\n                   allocationSize = 50)  \u002F\u002F 与 INCREMENT BY 一致\n```\n\n**JPA 主键策略对比**：\n\n| 策略 | 数据库支持 | 批量插入 | 性能 | 分布式 |\n|------|-----------|---------|------|-------|\n| IDENTITY | MySQL, PostgreSQL | 不支持（需逐条获取） | 一般 | 不支持 |\n| SEQUENCE | Oracle, PostgreSQL | 支持（预分配） | 好 | 不支持 |\n| TABLE | 所有 | 支持 | 较差（锁竞争） | 不支持 |\n| UUID | 所有 | 支持 | 较差（字符串索引） | 支持 |\n\n> **IDENTITY 与批量插入的矛盾**：Hibernate 使用 `IDENTITY` 策略时，无法使用 JDBC 批量插入。因为 INSERT 后必须立即获取生成的 ID 才能管理实体状态，导致每条 INSERT 单独执行。这是选择 SEQUENCE 的重要理由。\n\n### 8.2 MyBatis-Plus 主键策略\n\nMyBatis-Plus 通过 `@TableId` 注解和全局配置控制主键生成：\n\n```java\n@Data\n@TableName(\"t_user\")\npublic class User {\n\n    \u002F\u002F 雪花算法（默认策略）\n    @TableId(value = \"id\", type = IdType.ASSIGN_ID)\n    private Long id;\n}\n```\n\n**IdType 枚举**：\n\n| IdType | 生成方式 | 说明 |\n|--------|---------|------|\n| `AUTO` | 数据库自增 | 对应 MySQL `AUTO_INCREMENT` |\n| `ASSIGN_ID` | 雪花算法（默认） | 分布式唯一 ID，64位 long |\n| `ASSIGN_UUID` | UUID | 去掉横线的 32 位 UUID |\n| `INPUT` | 手动输入 | 需要在插入前手动设置 ID |\n| `NONE` | 跟随全局配置 | 由 `idType` 全局设置决定 |\n\n```yaml\n# application.yml 全局配置\nmybatis-plus:\n  global-config:\n    db-config:\n      id-type: assign_id          # 全局主键策略（默认雪花算法）\n```\n\n### 8.3 框架选型建议\n\n| 场景 | 推荐方案 | ORM 配置 |\n|------|---------|---------|\n| 单库 MySQL + JPA | `GenerationType.IDENTITY` | `@GeneratedValue(strategy = IDENTITY)` |\n| 单库 Oracle\u002FPG + JPA | `GenerationType.SEQUENCE` + allocationSize | `@SequenceGenerator(allocationSize = 50)` |\n| 分布式 + MyBatis-Plus | 雪花算法（默认） | `IdType.ASSIGN_ID` |\n| 分布式 + JPA | TSID 自定义生成器 | `@GenericGenerator` + `TsidGenerator`（见第 6 章） |\n| 订单号等业务 ID | 号段模式（Leaf） | 应用层独立服务 |\n\n---\n\n## 9. 方案总览与选型决策\n\n### 9.1 全方案对比\n\n| 方案 | 有序性 | 全局唯一 | 性能 | 存储效率 | 批量插入 | 依赖 | 适用场景 |\n|------|--------|---------|------|---------|---------|------|---------|\n| AUTO_INCREMENT | 严格递增 | 单库唯一 | 高 | 8 字节 | 差 | 无 | 单库单表 |\n| SEQUENCE | 严格递增 | 单库唯一 | 高（可预分配） | 8 字节 | 好 | 无 | Oracle\u002FPG 单库 |\n| UUID v4 | 无序 | 全局唯一 | 高 | 36 字节 | 好 | 无 | 临时 ID、非索引字段 |\n| UUID v7 | 趋势递增 | 全局唯一 | 高 | 36 字节 | 好 | 无 | 兼容已有 UUID 方案 |\n| 雪花算法 | 趋势递增 | 全局唯一 | 极高 | 8 字节 | 好 | 时钟 | 分布式系统经典方案 |\n| TSID | 趋势递增 | 全局唯一 | 极高 | 8 字节 \u002F 13字符 | 好 | 时钟 | 雪花算法的现代替代 |\n| 号段模式 | 严格递增 | 全局唯一 | 高 | 8 字节 | 好 | 数据库 | 电商订单、高可用要求 |\n| Redis INCR | 严格递增 | 全局唯一 | 高 | 8 字节 | 好 | Redis | 已有 Redis 基础设施 |\n\n### 9.2 选型决策树\n\n```\n开始\n  ↓\n是否分库分表 \u002F 微服务？\n  ├── 否 → 单库自增（MySQL AUTO_INCREMENT \u002F PG SEQUENCE）\n  └── 是\n        ↓\n      是否需要严格递增？\n        ├── 否 → TSID（推荐）\u002F 雪花算法（无需协调、高性能）\n        └── 是\n              ↓\n            是否对高可用有极高要求？\n              ├── 否 → Redis INCR\n              └── 是 → 号段模式（Leaf-segment）\n```\n\n### 9.3 主键 vs 业务 ID 分离\n\n> 生产环境中常见的最佳实践：**主键与业务 ID 分离**。\n\n```\nt_order 表：\n  id          BIGINT AUTO_INCREMENT PRIMARY KEY   -- 聚簇索引主键，有序写入\n  order_no    BIGINT NOT NULL UNIQUE              -- 雪花算法生成的业务订单号\n  ...\n\n好处：\n  1. 主键有序 → InnoDB 写入性能最优\n  2. 业务 ID 全局唯一 → 分库分表、跨服务引用时使用\n  3. 主键短小 → 二级索引紧凑，内存利用率高\n```\n![](https:\u002F\u002Fimg.233144.xyz\u002Fxv-site\u002Fbfdc9f40-08f1-406b-876c-a4f71ac1a5fd.gif)\n","https:\u002F\u002Fimg.233144.xyz\u002Fxv-site\u002Fb155c022-25f8-4b36-8ac4-a8a78864d473.jpg",[],false,"APPROVED",14,44,{"id":15,"username":16},"1","sa","2026-05-25T13:25:28.014014","2026-05-28T23:27:24.848533",1780059956182]