如何构建一个分布式系统中的流水ID生成器?

更新于
2026-10-02 22:31:06
67阅读来源:SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2376个文字,预计阅读时间需要10分钟。

如何构建一个分布式系统中的流水ID生成器?

流水编号在开发过程中可能用到的功能包括:销售订单号、采购订单号、日志编号、凭证号等,为确保唯一性,建议使用自增长或GUID值,或通过雪花算法计算。

流水编号

日常在我们开发的过程中可能会用到编号的功能,如销售订单号,采购订单号,日志编号,凭证号...等等,为了保证唯一有些表的主键要么用自增长,要么用GUID值,或通过雪花ID算法生成。这此方式基本都能产生唯一的ID,但如果在分布式环境下产生流水ID,以上这几种方式可能就不太好用,如有以下场景

  1. 工作流的流水编号

工作流的编号通常会是以下格式 如2022060200001-2022060299999 到了第二天时尾数又要生00001开始编,这种编号规则有一个好处就是非常直观的通过工作流编号就可以看出来这是哪一天申请的流程,一天大概有多少流水码。那么如果我们用常规编号规则其实是比较难完成此需求的。

  1. 采购订单号

如在实际业务需求中 标准采购订单要用5000000000-5999999999 这个号进行进行编码,委外采购订单用4000000000-4999999999号段进行编码

  1. XX业务受理单编号

在办里某业务时根据业务类型使用不同的编号 如财务收款用的流水号是 SK10000000-SK99999999,支出用的是ZC10000000-ZC99999999

根据以上的业务场景如果使用常规的编号是比较难实现的(要保证分布式环境编号不重复且按流水编码),那么HiSql 提供了比较方便的解决方案

该版本还未正式更新nuget 需要使用请下载源码

  • HiSql 源码(github) github.com/tansar/HiSql
  • HiSql 源码(gitee) gitee.com/tansar/HiSql
怎样开启编号规则

//如果需要使用编号那么一定要开启此功能 HiSql.Global.SnroOn = true; //开启编号后进行初始化 sqlClient.CodeFirst.InstallHisql();//仅需执行一次 如果使用的低版本的HiSql 升级引用包手需要重新初始化安装

初始安装完成后会生成表Hi_Snro 这个编号配置表

编号配置介绍

Hi_Snro 表详细说明

字段 描述 备注 SNRO 编号规则名称 主键 字符串(10) SNUM 子编号 主键 整数 IsSnow 是否雪花ID bool true:表示雪花id false:表示自定义流水编号 SnowTick 雪花ID时间戳 int64 IsSnow 为true时配置 大于0 都可以,其它的都可以不用配置 StartNum 编号开始值 字符串(20) 当IsNumber 为true 时编号只能纯数字 为false时 可以以0-9 A-Z 混编 EndNum 编号结束值 字符串(20) 当编号超过此时时将会抛出异常号段池已满 CurrNum 当前编号值 字符串(20) 第一次配置时值要等于StartNum CurrAllNum 当前完整编号 字符串(40) 不需要配置,产生流水时会自动生成 Length 编号长度 StartNum 和EndNum 的长度 两者的长度要一至否则会报错 IsNumber 是否纯数字编号 当为true 编号按0-9的10进制数字编号,当为false时按0-9 A-Z 36进度数字和字母混合编号 IsHasPre 是否有前辍 可以按到年月日时分秒作为前辍 在PreType 配置 PreType 前辍编号类型 详细请见 PreType 前置编号类型配置 FixPreChar 固定前辍 固定一个字符串 每个生成的码前面都加上这个值 PreChar 当前前辍 不需要配置 在编号的过程中会将 FixPreChar 和 PreType 存在此昝 CacheSpace 号段缓存 编号使用越频繁这个值配置越大 常的建议配置为10 值越大编号性能越好 CurrCacheSpace 当前缓存池使用的数量 不需要配置 Descript 编号描述 备注一下当前编号规则 PreType 前置编号类型配置

在PreType 是一个枚举类

字段 值 备注 PreType.None 0 表示无前置 PreType.Y 1 表示前置为日期格式[yyyy] 即当前年份如2022 PreType.Y2 12 表示前置为日期格式[yy] 即当前年份后两位如22 PreType.YM 2 表示前置为日期格式[yyyyMM] 即当前年月如202206 PreType.Y2M 22 表示前置为日期格式[yyMM] 即当前年月如2206 PreType.YMD 3 表示前置为日期格式[yyyyMMdd] 即当前年月日如20220602 PreType.Y2MD 32 表示前置为日期格式[yyMMdd] 即当前年月日如220602 PreType.YMDH 4 表示前置为日期格式[yyyyMMddHH] 即当前年月日时如2022060216 PreType.Y2MDH 42 表示前置为日期格式[yyMMddHH] 即当前年月日时如22060216 PreType.YMDHm 5 表示前置为日期格式[yyyyMMddHHmm] 即当前年月日时分如202206021630 PreType.Y2MDHm 52 表示前置为日期格式[yyMMddHHmm] 即当前年月日时分如2206021630 PreType.YMDHms 6 表示前置为日期格式[yyyyMMddHHmmss] 即当前年月日时分秒如20220602163020 PreType.Y2MDHms 62 表示前置为日期格式[yyMMddHHmmss] 即当前年月日时分秒如220602163020

根据以上配置规则将数据配置到表中

配置样例

Hi_Snro 的配置可以自行做个配置界面进行配置,以下是通过程序进行配置保存.

List<Hi_Snro> list = new List<Hi_Snro>(); ///工作流编号配置 ///按天产生流水号 如2205061000000-2205069999999 之间 list.Add( new Hi_Snro { SNRO = "WFNO", SNUM = 1, IsSnow = false, SnowTick = 0, StartNum = "1000000", EndNum = "9999999", Length = 7, CurrNum = "1000000", IsNumber = true, PreType=PreType.Y2MD, FixPreChar="", IsHasPre = true, CacheSpace = 5, Descript = "工作流编号" }); ///生成销售订单编码 每分钟从0开始编号 如20220602145800001-20220602145899999 list.Add(new Hi_Snro{ SNRO = "SALENO", SNUM = 1, IsSnow = false, SnowTick = 0, StartNum = "10000", EndNum = "99999", Length = 5, CurrNum = "10000", IsNumber = true, PreType = PreType.YMDHm, FixPreChar = "", IsHasPre = true, CacheSpace = 10, Descript = "销售订单号流水" }); ///生成另外一种销售订单编码 年的是取后两位 按每秒顺序生成 如22060214581200001-22060214581299999 list.Add(new Hi_Snro { SNRO = "SALENO", SNUM = 2, IsSnow = false, SnowTick = 0, StartNum = "10000", EndNum = "99999", Length = 5, CurrNum = "10000", IsNumber = true, PreType = PreType.Y2MDHms, FixPreChar = "", IsHasPre = true, CacheSpace = 10, Descript = "销售订单号流水" }); ///通过雪花ID生成 list.Add( new Hi_Snro { SNRO = "Order", SNUM = 1, IsSnow=true, SnowTick=145444, StartNum = "", EndNum = "",Length=7, CurrNum = "", IsNumber = true, IsHasPre = false, CacheSpace = 10, Descript = "订单号雪花ID" }); //保存配置到表中 sqlClient.Modi("Hi_Snro", list).ExecCommand(); 编号生成代码环境配置

建议把SeriNumber 做成一个局静态变量

public static SeriNumber number = null; //定义个全局变更的流水号对象

设置连接

//sqlClient 为数据库连接对象 number = new SeriNumber(sqlClient);

如果应用进行了分布式布署 请一定要启用以下代码

HiSql.Global.RedisOn = true;//开启redis缓存 HiSql.Global.RedisOptions = new RedisOptions { Host = "172.16.80.178", PassWord = "qwe123", Port = 6379, CacheRegion = "HRM", Database = 2 }; HiSql.Global.NumberOptions.MultiMode= true; //如果部署了分布式且也要生成雪花ID 一定要配置以下当前应用的ID 多个应用间只要不重复即可0-31之间 HiSql.Global.NumberOptions.WorkId=1;

通过以上配置则分布式编号环境配置完成,下面就可以进行编号测试了

编号测试

为了测试编号是否有重复我这里建了一个测试记录表H_nlog ,编号生成完成后可以分析该表的数据看是否有重复数据,(测试环境 为SqlServer) sql代码如下

如何构建一个分布式系统中的流水ID生成器?

SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[H_nlog]( [Nid] [int] IDENTITY(1,1) NOT NULL, [Numbers] [varchar](50) NULL, [CreateTime] [datetime] NULL, [CreateName] [nvarchar](50) NULL, [ModiTime] [datetime] NULL, [ModiName] [nvarchar](50) NULL, CONSTRAINT [PK_H_nlog] PRIMARY KEY CLUSTERED ( [Nid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_CreateName] DEFAULT ('') FOR [CreateName] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_ModiTime] DEFAULT (getdate()) FOR [ModiTime] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_ModiName] DEFAULT ('') FOR [ModiName] GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'CreateTime' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'CreateName' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'ModiTime' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'ModiName' GO

根据编号规则WFNO 子编号1 生成编号

List<object> lst = new List<object>(); for (int i = 0; i < 1000; i++) { var num = number.NewNumber("WFNO", 1); lst.Add(new { Numbers = num }); Console.WriteLine(num); } sqlClient.Insert("H_nlog", lst).ExecCommand();

测试结果

启用了分布式多个应用同时对同一个编号规则产生编号不会产生重复编号。

本文共计2376个文字,预计阅读时间需要10分钟。

如何构建一个分布式系统中的流水ID生成器?

流水编号在开发过程中可能用到的功能包括:销售订单号、采购订单号、日志编号、凭证号等,为确保唯一性,建议使用自增长或GUID值,或通过雪花算法计算。

流水编号

日常在我们开发的过程中可能会用到编号的功能,如销售订单号,采购订单号,日志编号,凭证号...等等,为了保证唯一有些表的主键要么用自增长,要么用GUID值,或通过雪花ID算法生成。这此方式基本都能产生唯一的ID,但如果在分布式环境下产生流水ID,以上这几种方式可能就不太好用,如有以下场景

  1. 工作流的流水编号

工作流的编号通常会是以下格式 如2022060200001-2022060299999 到了第二天时尾数又要生00001开始编,这种编号规则有一个好处就是非常直观的通过工作流编号就可以看出来这是哪一天申请的流程,一天大概有多少流水码。那么如果我们用常规编号规则其实是比较难完成此需求的。

  1. 采购订单号

如在实际业务需求中 标准采购订单要用5000000000-5999999999 这个号进行进行编码,委外采购订单用4000000000-4999999999号段进行编码

  1. XX业务受理单编号

在办里某业务时根据业务类型使用不同的编号 如财务收款用的流水号是 SK10000000-SK99999999,支出用的是ZC10000000-ZC99999999

根据以上的业务场景如果使用常规的编号是比较难实现的(要保证分布式环境编号不重复且按流水编码),那么HiSql 提供了比较方便的解决方案

该版本还未正式更新nuget 需要使用请下载源码

  • HiSql 源码(github) github.com/tansar/HiSql
  • HiSql 源码(gitee) gitee.com/tansar/HiSql
怎样开启编号规则

//如果需要使用编号那么一定要开启此功能 HiSql.Global.SnroOn = true; //开启编号后进行初始化 sqlClient.CodeFirst.InstallHisql();//仅需执行一次 如果使用的低版本的HiSql 升级引用包手需要重新初始化安装

初始安装完成后会生成表Hi_Snro 这个编号配置表

编号配置介绍

Hi_Snro 表详细说明

字段 描述 备注 SNRO 编号规则名称 主键 字符串(10) SNUM 子编号 主键 整数 IsSnow 是否雪花ID bool true:表示雪花id false:表示自定义流水编号 SnowTick 雪花ID时间戳 int64 IsSnow 为true时配置 大于0 都可以,其它的都可以不用配置 StartNum 编号开始值 字符串(20) 当IsNumber 为true 时编号只能纯数字 为false时 可以以0-9 A-Z 混编 EndNum 编号结束值 字符串(20) 当编号超过此时时将会抛出异常号段池已满 CurrNum 当前编号值 字符串(20) 第一次配置时值要等于StartNum CurrAllNum 当前完整编号 字符串(40) 不需要配置,产生流水时会自动生成 Length 编号长度 StartNum 和EndNum 的长度 两者的长度要一至否则会报错 IsNumber 是否纯数字编号 当为true 编号按0-9的10进制数字编号,当为false时按0-9 A-Z 36进度数字和字母混合编号 IsHasPre 是否有前辍 可以按到年月日时分秒作为前辍 在PreType 配置 PreType 前辍编号类型 详细请见 PreType 前置编号类型配置 FixPreChar 固定前辍 固定一个字符串 每个生成的码前面都加上这个值 PreChar 当前前辍 不需要配置 在编号的过程中会将 FixPreChar 和 PreType 存在此昝 CacheSpace 号段缓存 编号使用越频繁这个值配置越大 常的建议配置为10 值越大编号性能越好 CurrCacheSpace 当前缓存池使用的数量 不需要配置 Descript 编号描述 备注一下当前编号规则 PreType 前置编号类型配置

在PreType 是一个枚举类

字段 值 备注 PreType.None 0 表示无前置 PreType.Y 1 表示前置为日期格式[yyyy] 即当前年份如2022 PreType.Y2 12 表示前置为日期格式[yy] 即当前年份后两位如22 PreType.YM 2 表示前置为日期格式[yyyyMM] 即当前年月如202206 PreType.Y2M 22 表示前置为日期格式[yyMM] 即当前年月如2206 PreType.YMD 3 表示前置为日期格式[yyyyMMdd] 即当前年月日如20220602 PreType.Y2MD 32 表示前置为日期格式[yyMMdd] 即当前年月日如220602 PreType.YMDH 4 表示前置为日期格式[yyyyMMddHH] 即当前年月日时如2022060216 PreType.Y2MDH 42 表示前置为日期格式[yyMMddHH] 即当前年月日时如22060216 PreType.YMDHm 5 表示前置为日期格式[yyyyMMddHHmm] 即当前年月日时分如202206021630 PreType.Y2MDHm 52 表示前置为日期格式[yyMMddHHmm] 即当前年月日时分如2206021630 PreType.YMDHms 6 表示前置为日期格式[yyyyMMddHHmmss] 即当前年月日时分秒如20220602163020 PreType.Y2MDHms 62 表示前置为日期格式[yyMMddHHmmss] 即当前年月日时分秒如220602163020

根据以上配置规则将数据配置到表中

配置样例

Hi_Snro 的配置可以自行做个配置界面进行配置,以下是通过程序进行配置保存.

List<Hi_Snro> list = new List<Hi_Snro>(); ///工作流编号配置 ///按天产生流水号 如2205061000000-2205069999999 之间 list.Add( new Hi_Snro { SNRO = "WFNO", SNUM = 1, IsSnow = false, SnowTick = 0, StartNum = "1000000", EndNum = "9999999", Length = 7, CurrNum = "1000000", IsNumber = true, PreType=PreType.Y2MD, FixPreChar="", IsHasPre = true, CacheSpace = 5, Descript = "工作流编号" }); ///生成销售订单编码 每分钟从0开始编号 如20220602145800001-20220602145899999 list.Add(new Hi_Snro{ SNRO = "SALENO", SNUM = 1, IsSnow = false, SnowTick = 0, StartNum = "10000", EndNum = "99999", Length = 5, CurrNum = "10000", IsNumber = true, PreType = PreType.YMDHm, FixPreChar = "", IsHasPre = true, CacheSpace = 10, Descript = "销售订单号流水" }); ///生成另外一种销售订单编码 年的是取后两位 按每秒顺序生成 如22060214581200001-22060214581299999 list.Add(new Hi_Snro { SNRO = "SALENO", SNUM = 2, IsSnow = false, SnowTick = 0, StartNum = "10000", EndNum = "99999", Length = 5, CurrNum = "10000", IsNumber = true, PreType = PreType.Y2MDHms, FixPreChar = "", IsHasPre = true, CacheSpace = 10, Descript = "销售订单号流水" }); ///通过雪花ID生成 list.Add( new Hi_Snro { SNRO = "Order", SNUM = 1, IsSnow=true, SnowTick=145444, StartNum = "", EndNum = "",Length=7, CurrNum = "", IsNumber = true, IsHasPre = false, CacheSpace = 10, Descript = "订单号雪花ID" }); //保存配置到表中 sqlClient.Modi("Hi_Snro", list).ExecCommand(); 编号生成代码环境配置

建议把SeriNumber 做成一个局静态变量

public static SeriNumber number = null; //定义个全局变更的流水号对象

设置连接

//sqlClient 为数据库连接对象 number = new SeriNumber(sqlClient);

如果应用进行了分布式布署 请一定要启用以下代码

HiSql.Global.RedisOn = true;//开启redis缓存 HiSql.Global.RedisOptions = new RedisOptions { Host = "172.16.80.178", PassWord = "qwe123", Port = 6379, CacheRegion = "HRM", Database = 2 }; HiSql.Global.NumberOptions.MultiMode= true; //如果部署了分布式且也要生成雪花ID 一定要配置以下当前应用的ID 多个应用间只要不重复即可0-31之间 HiSql.Global.NumberOptions.WorkId=1;

通过以上配置则分布式编号环境配置完成,下面就可以进行编号测试了

编号测试

为了测试编号是否有重复我这里建了一个测试记录表H_nlog ,编号生成完成后可以分析该表的数据看是否有重复数据,(测试环境 为SqlServer) sql代码如下

如何构建一个分布式系统中的流水ID生成器?

SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE TABLE [dbo].[H_nlog]( [Nid] [int] IDENTITY(1,1) NOT NULL, [Numbers] [varchar](50) NULL, [CreateTime] [datetime] NULL, [CreateName] [nvarchar](50) NULL, [ModiTime] [datetime] NULL, [ModiName] [nvarchar](50) NULL, CONSTRAINT [PK_H_nlog] PRIMARY KEY CLUSTERED ( [Nid] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_CreateTime] DEFAULT (getdate()) FOR [CreateTime] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_CreateName] DEFAULT ('') FOR [CreateName] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_ModiTime] DEFAULT (getdate()) FOR [ModiTime] GO ALTER TABLE [dbo].[H_nlog] ADD CONSTRAINT [DF_H_nlog_ModiName] DEFAULT ('') FOR [ModiName] GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'CreateTime' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'创建人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'CreateName' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改时间' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'ModiTime' GO EXEC sys.sp_addextendedproperty @name=N'MS_Description', @value=N'修改人' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'H_nlog', @level2type=N'COLUMN',@level2name=N'ModiName' GO

根据编号规则WFNO 子编号1 生成编号

List<object> lst = new List<object>(); for (int i = 0; i < 1000; i++) { var num = number.NewNumber("WFNO", 1); lst.Add(new { Numbers = num }); Console.WriteLine(num); } sqlClient.Insert("H_nlog", lst).ExecCommand();

测试结果

启用了分布式多个应用同时对同一个编号规则产生编号不会产生重复编号。