自定义序列生成器之单体架构实现
主键 ID VS 业务 ID
在数据库设计中,除了主键 ID,一般还需要一个具有唯一索引的业务 ID。二者承担的职责不一样,它们共同满足了我们对于 技术实现 和 业务需求 的双重目标
1. 职责分离原则
主键 ID | 业务唯一标识 ID | |
作用 | 保证数据库层面的唯一性 | 保证业务层面的唯一性 |
目标 | 保证数据存储和关联的可靠性 | 满足业务规则和外部交互需求 |
特点 | 无意义、自增/随机、不可变 | 有具体业务含义、可读、可暴露 |
eg:
- 商品表的主键 ID 可能是 1、2、3,但商品编码(业务唯一标识)可能是
00012517271821
。前四位是所处的地区码,中间是随机生成的数字,最后四位是新增这个商品的用户 ID 后四位 - 订单表的主键 ID 可能是 1、2、3,但订单编码可能是时间戳拼上今天订单的序号:
202505220012
2. 使用场景分析
场景一:防止暴露内部信息
- 问题:直接暴露自增主键
ID
,可能泄露业务规模(如用户量、订单量),甚至被恶意遍历数据 - 解决:使用无规律的业务
ID
(如UUID、哈希值)对外暴露,隐藏自增主键
场景二:分库分表需求
- 问题:如果需要分库分表,主键
ID
就无法保证全局唯一性 - 解决:通过业务
ID
实现全局唯一(雪花算法生成的分布式 ID、使用自定义序列生成器生成的 ID)
场景三:业务标识符的灵活性
- 问题:业务唯一标识可能需要动态规则(如订单号包含日期、地区码),而自增主键无法满足
- 解决:业务唯一标识
ID
按业务规则生成,主键ID
保持默认策略
3. 技术实现对比
主键 ID | 业务唯一标识 ID | |
数据类型 | 通常为 | 可能是 |
唯一性范围 | 表内唯一 | 全局唯一(跨表、跨系统) |
生成方式 | 自增/随机 | 程序生成(UUID、雪花算法、业务规则拼接) |
修改性 | 不可变(与数据生命周期绑定) | 可能允许修改(如用户重设唯一用户名) |
建议:
- 主键
ID
始终存在:作为数据库的“技术锚点”,用于外键关联、索引优化 - 业务唯一标识
ID
按需设计
-
- 若无需业务唯一标识,可省略
- 若需暴露或业务规则复杂,必加,并为其添加唯一索引
- 查询优化
-
- 内部关联用
ID
(更快) - 对外接口用业务
ID
(更安全)
- 内部关联用
4. 何时不需要用业务 ID ?
- 纯内部工具表,无暴露需求
- 业务标识符可直接复用主键(如简单的配置表)
为什么需要自定义序列生成器?
前面有说过业务ID
一般是具有具体业务含义的,我们需要支持根据动态规则来生成具有不同业务属性的业务ID
注意:
自定义序列生成器一般用来生成业务 ID ,但也可以用来生成主键 ID。具体实现方式是由多个维度所决定的。例如:公司觉得主键 ID 使用雪花算法生成的 64 位长整型数字比较占用内存,但是又不想新增一个具备实际业务含义的字段,那就可以选择使用自定义序列生成器生成具备业务属性的主键 ID(合二为一)
下面,我将分别实现 单体架构 和 分布式架构 下的序列生成器。它们最大的区别在于分布式架构下的序列生成器可以保证序列在多个不同的数据库之间也不会出现重复的问题,保证全局唯一性
单体架构实现
实现单体架构的序列生成器较为简单,只要想明白两个注意点:
- 由于要支持动态规则,所以需要用一张表来存储不同的业务生成序列的对应规则
- 我们需要保证业务 ID 唯一,所以每次要记录生成的最后一次数值,确保下次生成的值具有顺序且不重复
想明白了以上两点,我们就来尝试实现吧
实现步骤:
- 定义一张表用来配置不同业务的序列生成规则模板
CREATE TABLE `sequence_rule` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增列',`module_id` varchar(50) NOT NULL COMMENT '模块ID',`rule` varchar(100) NOT NULL COMMENT '序列规则',`cuid` int(11) NOT NULL COMMENT '当前流水号',`pref` varchar(50) NOT NULL COMMENT '规则前缀',PRIMARY KEY (`id`),UNIQUE KEY `idx_module_id` (`module_id`)
) COMMENT='序列规则配置';
注意:模块 ID 要单独建立唯一索引,保证唯一性
- 定义一张表用来记录不同序列对应生成的值
CREATE TABLE `sequence_record` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`sequence_key` varchar(64) NOT NULL COMMENT '序列编码',`sequence_value` bigint(20) DEFAULT NULL COMMENT '序列值',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='序列ID记录表';
- 定义入口方法
VoucherIdManager.generateIds()
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public List<String> generateIds(ModuleEnum moduleEnum, Long length) {if (moduleEnum == null ||length == null) {throw new BizException(200, "缺失必传参数");}return this.buildIds(moduleEnum, length);
}
- 核心逻辑为
buildIds()
/*** 构建 ID** @param moduleEnum 模块枚举* @param length 集合长度* @return List<String>*/
private List<String> buildIds(ModuleEnum moduleEnum, Long length) {List<String> ids = new ArrayList<>();// 1.获取序列规则SequenceRule sequenceRule = sequenceRuleService.getByModuleEnum(moduleEnum);String rule = sequenceRule.getRule().toUpperCase();// 2.生成 ID 前缀// ID 规则为: CO[yy][mm][dd][ID000000] 则第二步会生成 CO20230501 这一串前缀String idPref = this.generateIdPref(rule);log.info("idPref -> [{}]", idPref);// 3.生成唯一值Matcher matcher = SEQUENCE.matcher(rule);if (matcher.find()) {// 如果匹配上了,获取 0 的个数 (0 的个数就意味着要生成的随机数的长度)int zeroLength = matcher.end() - matcher.start() - 4;for (int i = 0; i < length; i++) {Long nextSequence = sequenceManager.getNextSequence(idPref);ids.add(idPref + String.format("%0" + zeroLength + "d", nextSequence));}} else {throw new BizException(200, "序列规则配置错误");}return ids;
}
- 定义一个类,将数据库中对应序列的属性保存到内存(此处也可替换成 Redis)
private class SequenceHolder {private String key;/*** 当前序列号,初始化是为 0*/private AtomicLong sequenceValue;/*** 数据库保存的序列号*/private long dbValue;/*** 步长,用来判断序列号是否还在给定的步长范围内*/private long step;public SequenceHolder(long step) {this.step = step;}public long nextValue() {if (sequenceValue == null) {// 初始化this.init();}long sequence = sequenceValue.incrementAndGet();if (sequence > step) {// 意味着分配给它的序列号已经用完,需要重新分配this.nextRound();return this.nextValue();} else {return dbValue + sequence;}}private synchronized void nextRound() {if (sequenceValue.get() > step) {// 重新生成下一个序列号dbValue = SequenceManager.this.nextValue(key, step) - step;sequenceValue = new AtomicLong(0);}}private synchronized void init() {if (sequenceValue != null) {return;}dbValue = SequenceManager.this.nextValue(key, step) - step;sequenceValue = new AtomicLong(0);}}
步长 step 的作用是什么?
步长的意思就是一次返回序列号的长度。例如:step=100,则会修改数据库中对应序列的可用值为当前值 + 100,意味着这段区间已经分配给了当前服务。只要 sequenceValue 没有超过这个步长,则可以安全的使用分配给它的这一段区间。如果超过了,则需要重新获取一个新的区间,此区间长度为 step
- 实现序列生成逻辑
/*** @Description 序列生成器* @Author Mr.Zhang* @Date 2025/5/25 19:04* @Version 1.0*/
@Slf4j
@Component
public class SequenceManager {@Autowiredprivate SequenceRecordService sequenceRecordService;private static final Map<String, SequenceHolder> holder = new HashMap<>();/*** 获取下一个序列 确保唯一性** @param identity Key* @return*/@Transactional(propagation = Propagation.NOT_SUPPORTED)public long getNextSequence(String identity) {SequenceHolder sequenceHolder = holder.get(identity);if (sequenceHolder == null) {synchronized (holder) {sequenceHolder = holder.get(identity);if (sequenceHolder == null) {sequenceHolder = new SequenceHolder(1); // 默认为 1sequenceHolder.setKey(identity);sequenceHolder.init();holder.put(identity, sequenceHolder);}}}return sequenceHolder.nextValue();}/*** 获取下一个序列 确保唯一性** @param sequenceKey Key* @return*/private long nextValue(String sequenceKey, long step) {for (int i = 0; i < 10; i++) {SequenceRecord sequenceRecord = sequenceRecordService.querySequence(sequenceKey);int effectRow = sequenceRecordService.nextValue(sequenceRecord.getSequenceValue() + step, sequenceRecord.getSequenceValue(), sequenceKey);if (effectRow == 1) {return sequenceRecord.getSequenceValue() + step; // 返回下一个可用值}}throw new BizException(200, "获取序列失败");}
}
单体架构的核心代码就是这些。最主要的思路其实是保证序列生成的唯一性。此实现采用步长 + 乐观锁的方式确保不同的服务拿到的是不同的序列值
单体架构实现完整代码已上传到 github 上,感兴趣的朋友可以配合我的讲解看看具体实现代码
GitHub - nowtostudeyday/sequence-generate: 序列生成器。支持单体架构和分布式架构下的序列生成。支持自定义序列前缀,保证全局唯一性
欢迎 star~~
相关文章:
自定义序列生成器之单体架构实现
主键 ID VS 业务 ID 在数据库设计中,除了主键 ID,一般还需要一个具有唯一索引的业务 ID。二者承担的职责不一样,它们共同满足了我们对于 技术实现 和 业务需求 的双重目标 1. 职责分离原则 主键 ID 业务唯一标识 ID 作用 保证数据库层面…...

电阻电容的选型
一、电阻选型 1.1安装方式 贴片电阻体积小,适用于SMT生产;功率小;易拆解插件电阻体积大;功率大;不易脱落 1.2阻值 电阻的阻值是离散的,其标称阻值根据精度分为E6、E12、E24、E48、E96、E192六大系列&am…...

12.springCloud AlibabaSentinel实现熔断与限流
目录 一、Sentinel简介 1.官网 2.Sentinel 是什么 3.Sentinel 的历史 4.Sentinel 基本概念 资源 规则 5.Sentinel 功能和设计理念 (1).流量控制 什么是流量控制 流量控制设计理念 (2).断降级 什么是熔断降级 熔断降级设计理念 (3).系统自适应保护 6.主要工作机制…...
Cookie 和 Session:Web 身份验证的核心机制
文章目录 一、Cookie:客户端存储的小数据块**核心特性****典型应用场景**二、Session:服务器端的会话存储**核心特性****典型应用场景**三、Cookie vs Session:核心区别对比四、最佳实践与扩展 一、Cookie:客户端存储的小数据块 …...

vSOME/IP与ETAS DSOME/IP通信的问题解决方案
✅ 一、服务版本不匹配导致 Handover 问题 —— 需要更新 VSOMEIP 代码逻辑 📌 问题描述: 在 SOME/IP 通信中,发布者(offer)与订阅者(subscribe)之间存在服务版本不一致的问题,导致 Handover(切换)失败。 ✅ 解决方案: 需要在 offer_service 和 subscribe 接口中…...
修改vscode切换上一个/下一个标签页快捷键
装了vim后一直没找到切tab页的快捷键 Code>Preferences>Keyboard Shortcuts on macOS 搜索这2个选项 我设置成了commandh 向前切换,commandl向后切换,贴合vim的方向设置 workbench.action.previousEditor commandh workbench.action.nextEdit…...
三大中文wordpress原创主题汉主题
汉主题 汉主题是一款极具特色的 WordPress 主题,由国内专业团队精心打造,专为中文用户设计。其设计灵感源自博大精深的汉文化,将传统文化元素与现代网页设计理念巧妙融合,呈现出独特而典雅的风格。无论是用于个人博客展示文学创作…...

软考-系统架构设计师-第十五章 信息系统架构设计理论与实践
信息系统架构设计理论与实践 15.2 信息系统架构风格和分类15.3 信息系统常用的架构模型15.4 企业信息系统总体框架15.5 信息系统架构设计方法 15.2 信息系统架构风格和分类 信息系统架构风格 数据流体系结构风格:批处理、管道-过滤器调用/返回体系结构风格&#x…...
Redis缓存-数据淘汰策略
数据淘汰策略就是,当redis内存满的时候,此时在向redis添加新的key,那么redis会按照某一种规则将内存中的数据删掉,这种删除数据的规则成为内存的淘汰策略。 redis支持8中淘汰策略 1.noeviction,这种是redis默认的情况…...
52. N 皇后 II【 力扣(LeetCode) 】
文章目录 零、原题链接一、题目描述二、测试用例三、解题思路四、参考代码 零、原题链接 52. N 皇后 II 一、题目描述 n 皇后问题 研究的是如何将 n 个皇后放置在 n n 的棋盘上,并且使皇后彼此之间不能相互攻击。【补充:不能互相攻击就是要求一个皇后的…...

MySQL 8 完整安装指南(Ubuntu 22.04)
MySQL 8 完整安装指南(Ubuntu 22.04) 本教程详细说明如何在 Ubuntu 22.04 上安装和配置 MySQL 8,包含安全优化及远程访问设置。 1️⃣ 添加 MySQL 官方 APT 仓库 官网仓库下载地址:MySQL APT 仓库下载页 下载仓库配置包&#…...
C++ 标准输入输出 -- <iostream>
<iostream>库是 C++ 标准库中用于输入输出操作的头文件。 <iostream> 定义了几个常用的流类和操作符,允许程序与标准输入输出设备(如键盘和屏幕)进行交互。 以下是<iostream>库的详细使用说明,包括其主要类和常见用法示例。 主要类 std::istream:用于…...
记一次sql按经纬度计算距离
具体代码: ROUND函数在mysql可以用来计算经纬度,代码如下: SELECTa.store_name_sfa as storeName,a.storeid_sfa as store_id,a.link_man_sfa as link_man,a.link_phon_sfa as link_phone,a.photo as image_url,a.district,a.street,ROUND(6…...

安卓jetpack compose学习笔记-UI基础学习
哲学知识应该用哲学的方式学习,技术知识也应该用技术的方式学习。没必要用哲学的态度来学习技术。 学完安卓技术能做事就ok了,安卓技术肯定是有哲学的,但是在初学阶段没必要讨论什么安卓哲学。 学习一们复杂技术的路径有很多,这里…...
线性回归用于分类
线性回归本身是一种用于回归问题的技术,即预测一个连续的目标变量值。然而,线性回归也可以被改造或结合其他技术来用于分类问题,尽管这不是其最直接或最常见的用途。以下是几种将线性回归应用于分类问题的方法或相关概念: 阈值划分…...
解锁电商新势能:商城系统自动 SaaS 多开功能深度解析
在电商行业加速向精细化、多元化运营转型的当下,传统的商城系统部署模式已难以满足企业快速拓展业务的需求。此时,商城系统自动 SaaS 多开功能横空出世,以智能、高效、灵活的特性,成为众多电商企业突破发展瓶颈的关键利器。这一功…...

蓝桥杯_DS18B20温度传感器---新手入门级别超级详细解析
目录 一、引言 DS18B20的原理图 单总线简介: 编辑暂存器简介: DS18B20的温度转换与读取流程 二、代码配置 maic文件 疑问 关于不同格式化输出符号的使用 为什么要rd_temperature()/16.0? onewire.h文件 这个配置为什么要先读lo…...

C++中锁与原子操作的区别及取舍策略
文章目录 锁与原子操作的基本概念锁(Lock)原子操作(Atomic Operations) 锁与原子操作的区别1. **功能**2. **性能**3. **复杂性**4. **适用场景** 锁与原子操作的取舍策略1. **简单变量操作**2. **复杂共享资源**3. **性能敏感场景…...
ESP32对接巴法云实现配网
目录 序言准备工作巴法云注册与使用Arduino准备 开发开始配网 序言 本文部分内容摘抄原创作者巴法云-做优秀的物联网平台 代码有部分修改并测试运行正常 巴法云支持免费用户通过开发对接实现各智能音箱设备语音控制智能家居设备,并有自己的App进行配网和控制&…...
《深度剖析:基于Meta的GameFormer构建自博弈AI游戏代理》
自博弈AI游戏代理,是一种具备自主学习和自我提升能力的人工智能系统。它打破了传统AI依赖预设规则和固定策略的局限,能够在游戏过程中不断与自身进行对战,通过反复博弈来积累经验、优化策略,从而实现智能水平的持续提升 。这种独特…...

C++语法系列之类型转换
前言 类型转换是经常存在的情况,类型转换分为隐式类型转化 和 显式类型转化 隐式类型转化:编译器在编译阶段自动进行,能转就转,不能转就编译失败 double i 3.3; int b i; //隐式类型转化 double -> intC搞出来了四种强制类…...
Qwen3 技术报告解读一
📘 Qwen3 技术报告解读:通义千问系列新成员的技术亮点与能力分析 一、论文写了什么? 本文来自阿里通义实验室发布的 《Qwen3 Technical Report》,介绍了其最新一代大语言模型 Qwen3 的技术架构、训练方法以及在多个关键任务上的…...

详解开漏输出和推挽输出
开漏输出和推挽输出 以上是 GPIO 配置为输出时的内部示意图,我们要关注的其实就是这两个 MOS 管的开关状态,可以组合出四种状态: 两个 MOS 管都关闭时,输出处于一个浮空状态,此时他对其他点的电阻是无穷大的ÿ…...

【八股消消乐】索引失效与优化方法总结
😊你好,我是小航,一个正在变秃、变强的文艺倾年。 🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、具身智能、机器学习、深度学习、力扣算法等相关知识点ÿ…...

一步一步配置 Ubuntu Server 的 NodeJS 服务器详细实录——4. 配置服务器终端环境 zsh , oh my zsh, vim
前言 通过前面几篇文章,我们顺利的 安装了 ubuntu server 服务器,并且配置好了 ssh 免密登录服务器,也安装好了 服务器常用软件安装,接下来,我们要仔细的配置一下我们的终端环境,让服务器的终端更加好用。 一般情况下…...

数据安全合规体系构建的“三道防线“
引言 "三道防线"模型架构图 #mermaid-svg-wbeppAbwa3Vb3nL2 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-wbeppAbwa3Vb3nL2 .error-icon{fill:#552222;}#mermaid-svg-wbeppAbwa3Vb3nL2 .error-text{fi…...

【Spring底层分析】Spring AOP基本使用+万字底层源码阅读分析
一、AOP基本使用 三步: 将业务逻辑组件和切面类都加入到容器中,告诉Spring哪个是切面类(Aspect)在切面类上的每一个通知方法上标注通知注解,告诉Spring何时(Before、After、Around……)何地运…...
Python数据分析及可视化中常用的6个库及函数(二)
Python数据分析及可视化中常用的6个库及函数(二) 摘要:以下是Python数据分析及可视化常用的6个库的详细介绍,包括它们的概述以及每个库中最常用的10个函数(如果某些库常用函数不足10个,则列出所有常用函数)。每个函数都附带功能描述、用法说明和使用示例。这些库…...

新德通科技:以创新驱动光通信一体化发展,赋能全球智能互联
在数字经济与AI技术高速发展的今天,光通信作为信息传输的核心基础设施,正迎来前所未有的升级浪潮。深圳新德通科技有限公司(以下简称“新德通科技”)凭借其深厚的技术积累与一体化产品布局,成为行业内的中坚力量。本文…...
Selenium的底层原理
Selenium 底层主要依赖于 WebDriver 协议(即 W3C WebDriver 规范,早期也有 JSON Wire Protocol)来实现对浏览器的远程控制,其核心架构可以分为以下几层: Selenium 客户端(Client Library) 支持多…...