业务背景
系统的业务单据基本都会带有一个单据编号字段,编号规则如下:
- 一般以租户维度下唯一,即允许不同租户的相同单据类型出现单据编号重复。
- 编号形式一般为:固定前缀 + 日期 + 自然位数 。
- 部分单据编号的形式要求包含随机数,用于打乱自然增长序号。
- 部分单据编号的前缀不是唯一的,可能会根据用户输入的前缀决定。
实现原理
关于业务背景的需求,其实主要麻烦的是自然位数的实现,特别是在考虑多种附加场景下时,例如:自然位数尽可能从1递增、系统是分布式服务、不同租户下序列号隔离。
因此,一般需要借助其他工具实现,例如:
- 数据库的序列(Sequence),数据库序列是一种数据库对象,用于生成唯一的递增或递减序列值。
- Redis 的 INCR 命令,用来原子地递增一个键的值,使用 Redis 的字符串类型来存储递增的序列值。
- 分布式ID生成器,例如Snowflake算法,可以生成全局唯一的递增ID。
- 分布式缓存和计数器,使用分布式缓存(例如Memcached)和计数器,通过原子递增操作实现递增序列。
在工作中,我使用的是 MySQL 数据库的唯一索引特性 + InnoDB引擎的事务特性,通过事务和唯一索引的锁机制,实现自然位数的自然递增。
数据表设计
数据表主要是依赖一张表,用于存储自然位数。
1 2 3 4 5 6 7 8 9 10 11 12
| create table sys_business_sn ( id bigint auto_increment comment '主键id' primary key, tenant_id bigint default 0 not null comment '租户id', business_type int not null comment '业务类型', business_date date not null comment '业务日期', business_sn int not null comment '业务编号 (一般根据 业务类型+业务日期 唯一)', row_create_time datetime default CURRENT_TIMESTAMP not null comment '数据创建时间', row_update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '数据更新时间', constraint uniq_tenant_type_date unique (tenant_id, business_type, business_date) ) comment '系统业务编号表';
|
其中 tenant_id 表示租户id,实现租户隔离;business_type 表示业务类型,指不同业务单据对应的自然位数是隔离的;business_date 表示业务日期,指不同日期下的自然位数是隔离的;business_sn 表示自然位数,它通过 租户id + 业务类型 + 业务日期 实现唯一,通过 uniq_tenant_type_date 唯一索引建立关系。
至于如何通过这张表获取单据编号(其实主要是获取业务编号),则依赖下面的 sql 语句:
1 2 3
| insert into sys_business_sn(tenant_id, business_type, business_date, business_sn) values (#{tenantId}, #{businessType}, #{businessDate}, 1) on duplicate key update business_sn = business_sn + 1;
|
这句 sql 语句采用 inset into xxx values xxx on duplicate key update xxx
语法。当“租户id + 业务类型 + 业务日期”拼接的值不存在对应的行数据时,默认插入一条数据,业务编号初始为1;当存在对应的行数据时,则将业务编号自增加1。
代码设计
通过数据表设计,在 Java 代码设计一个 DAO 层,用于生成指定租户、业务类型、业务日期的单据编号。
getBusinessSnByType
方法用于获取指定的自然位数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public Integer getBusinessSnByType(Integer businessType, Date date, Long companyId) { String realTenantId; if (companyId == null) { Long tenantId = UserUtils.getTenantId(); if (tenantId == null) { throw new IllegalStateException("token信息异常"); } realTenantId = tenantId.toString(); } else { realTenantId = companyId.toString(); } String dateStr = BUSINESS_DATE_FORMAT.format(date);
DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); transactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition); baseMapper.incrementBusinessSn(realTenantId, businessType, dateStr);
SysBusinessSn entity = lambdaQuery() .eq(SysBusinessSn::getTenantId, realTenantId) .eq(SysBusinessSn::getBusinessType, businessType) .eq(SysBusinessSn::getBusinessDate, dateStr) .select(SysBusinessSn::getBusinessSn) .last("limit 1") .one();
transactionManager.commit(transactionStatus);
return entity.getBusinessSn(); }
|
上面代码的关键在于 TransactionStatus
控制数据库事务的开始和提交,在事务中做数据更新和数据查询操作,根据 InnoDB 的ACID原则,拿取更新后的业务编号。
在这个方法的上层方法(generateDefaultFullBusinessSnByCustomized
),则是拼接最终的单据编号:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public String generateDefaultFullBusinessSnByCustomized(SysBusinessSnEnum sysBusinessSnEnum, String dateFormat, DecimalFormat decimalFormat, Long companyId) { Date date = new Date(); StringBuilder sb = new StringBuilder(sysBusinessSnEnum.getPrefix());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat); sb.append(simpleDateFormat.format(date));
if (sysBusinessSnEnum.getRandomLen() > 0) { String randomNumbers = RandomUtil.randomNumbers(sysBusinessSnEnum.getRandomLen()); sb.append(randomNumbers); }
Integer sn = this.getBusinessSnByType(sysBusinessSnEnum.getCode(), date, companyId);
return sb.append(decimalFormat.format(sn)).toString(); }
|
完整的代码示例地址:https://github.com/wangfarui/work-report/tree/main/business-sn
总结
因为实现原理借助的工具不同,所以优缺点有很多都是显而易见的,是代码很难解决的。先说优点:
- 有效保障分布式服务下业务编号递增规则。
- 可以基于数据表了解到各租户下各单据类型的编号使用状况。
缺点:
- 因为数据库的原因,所以如果数据库奔溃等原因,就会影响所有涉及到业务的正常流程。
- 一般这个表会跟业务表共存在一个数据库,会影响数据库的吞吐量、连接数等。
- 业务编号可能不保持连续递增,由于业务系统的业务异常等原因。