高可用高性能可扩展的单号生成方案 您所在的位置:网站首页 序列号qj开头 高可用高性能可扩展的单号生成方案

高可用高性能可扩展的单号生成方案

2023-10-16 01:16| 来源: 网络整理| 查看: 265

在业务开发中经常会遇到各种单号生成, 例如快递单号、服务单号、订单号等等。 这些单号生成往往是业务逻辑处理的第一步, 单号生成出问题,必然导致业务走不下去;另外有多少业务量就会至少有多少的单号生成需求。所以单号生成必须高可用,必须高性能。 另外业务不同需要的单号规则可能也不相同, 所以单号服务还必须具备足够的扩展性。

一、单号定义

在进入正题之前我们先给单号下个定义, 看几个常见的单号形式。

单号是一个数字和字符组成的序列, 它要满足两个条件: 一个是唯一, 保证唯一才可以作为业务标识; 另一个是符合业务需要的规则。 例如下面三个单号:

2017030400001 这个单号由两个部分序列号日期20170304+定长5位补0数字00001。 010-6541-00001 此单号分三部分, 中间用减号连接, 第一部分为区号, 第二部分为作业单位号码, 第三部分为作业单位产生作业的序号。 QJ000001 则是由字符QJ开头后面跟随数字序列的单号。

二、单号数字序列部分的生成

上述单号定义中的数字部分通常是一个自增的数字序列。 我们可以通过数据库的自增列、 数据库的列+1方式、 redis或者memcached的INCR指令来生成这种数字的序列。 这四种方式都可以生成序列, 但各自有各自的好处。

1. 数据库自增列的方式

是通过数据库的内部机制生成的, 在普通PC上每秒约可以生成4000个数字序列, 它的好处是每一个数字序列都会保留一条记录, 记录生成使用时间, 缺点是吞吐量一般, 会占用一定的数据库资源, 如下是一种推荐的表结构:

CREATE TABLE `xx_code_sequence` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `generate_time` timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULTCHARSET=utf8;

此表有两列, id列为bigint类型的自增长字段,作为数字序列的值, generate_time时间戳字段可以记录每一个单号的生成时间。生成数字序列的方式用sql说明如下:

begin trans; insert into `xx_code_sequence`(generate_time)values(current_timestamp); select last_insert_id(); commit;

说明:

表名格式xx_code_sequence,sto_code_0分为三部分sto为ownerKey, code固定不变,0表示表的序号,可以有多个下标不同表来支持更高的并发,共有几个表需要在开始确认了,确认的依据是需要满足的并发请求。表的个数必须是2的n次方,例如1, 2, 4, 8,16; `id` 即序列的部分值,是通过mysql的自增特性生成的,最终的序列值是id和表序号共同组成的,假定有4个表,序号分别为0,1,2,3;那么序列值为 idtimeoutThresholdInMilliseconds; if(!hasError&&isTimeout){ onTimeout(currentPartitionIndex,innerGen,usedTime); } LOGGER.trace("gen usedTime {}",usedTime); } }while(true); return sequence; }

使用时配置bean使用即可, 如下spring bean xml配置:

四、高性能实现

单号生成只是业务操作的第一个步骤, 业务操作往往是复杂耗时的, 我们必须保证单号生成的性能, 使其几乎不会影响业务时间。

上述介绍的四种序列生成方式都是跨网络通过中间件获得的序列号,要进一步优化其性能,我们需要将序列放在离CPU更近的地方――内存中。我们使用如下两种方式将数字序列放到CPU更近的地方:

将内部序列值向左移位n位, 然后序列的最右n位在内存生成,一次生成2的n次方个数字序列, 然后放在内存队列中; 异步提前生成:实时计算序列号方法被调用的速度, 然后在异步线程(池)中生成最近x ms需要的序列,放入内存队列中备用

这两种方式并不一定都需要, 置放入内存队列中的数字序列越多,重启时丢失的也会越多。

其内部结构图示如下:

高性能序列使用的bean配置如下:

通过设定memoryBitLength,指定序列的最右的memoryBitLength位在内存中生成以提高生成的效率。 需要注意memoryBitLength值越大则在内存中的序列条数越多, 性能越高, 如果发生重启时丢失的序列也会越多, 要根据情况来设置。 支持异步生成序列值, 异步生成的速度会根据序列值消费速度自适应。

五、关于可扩展性

单号规则多种多样, 不能每增加一种规则就增加一个需求, 我们需要相对灵活的扩展性。 上述介绍了多种单号数字序列的生成方式, 和数字序列生成的高可用和高性能实现, 他们都实现了同一个接口:

/** * 根据序列业务类型生成新序列的接口 * * 生成序列是大致递增的 * * Created by zhaoyukai on 2016/8/8. */ public interface SequenceGen { /** * 生成序列 * @param ownerKey 序列业务key * @return 新序列值 */ long gen(String ownerKey); }

有了这个统一的数字序列生成接口, 我们可以扩展多种不同的数字序列生成方式。 或者实现不同的高可用、高性能机制。

另外在本文的开头我们介绍了多种不同的单号生成规则, 要灵活满足这些不同的规则, 我们使用表达式来表达单号的组合规则, 通过将表达式解析成不同的Expression来实现不同单号部分的生成。 下面我们看一个单号表达式的示例, 如下是一个spring bean配置:

SmartSNGen类负责根据表达式生成不同规则的单号,其构造函数第一个参数值:

@{ownerKey, value=SN}-@{bean, ref=sequence}-

@{com.jd.coo.sa.sn.expression.CheckSumExpression} 即为表达式, 该表达式分为五个部分:

@{ownerKey, value=SN} 在表达式生成的上下文中写入key为ownerKey值为SN的参数 “-“ 表示静态表达式“-” @{bean, ref=sequence} 指定引用id为sequence的spring bean来生成表达式的一部分 “-“表示静态表达式”-“ @{com.jd.coo.sa.sn.expression.CheckSumExpression} 表示要创建指定类com.jd.coo.sa.sn.expression.CheckSumExpression的实例来生成表达式的一部分

该bean的interpreter属性指定了表达式的解释器,该解释器会将表达式值转换为实现了Expression接口的对象,通过该对象可以计算出单号的值。

表达式解释器查找表达式中的“@{”和“}”对,将其内部的表达式解析为动态表达式,将其他部分的表达式解析为静态表达式。动态表达式分为三种类型:

spring配置文件中的bean引用表达式 指定上下文参数的表达式 指定自定义类型的表达式

第3种表达式留出任意扩展自定义表达式的扩展点。

Expression接口定义如下:

import com.jd.coo.sa.sn.GenContext; /** * SmartSNGen表达式接口 * * Created by zhaoyukai on 2016/10/18. */ public interface Expression { /** * 计算表达式的值 * @param context 表达式计算上下文, 表达式可以根据需要将计算中间值存储到上下文中, 以便在表达式之间共享数据 * @return 表达式计算值 */ Object eval(GenContext context); /** * 计算优先级, 优先级越高越先执行, 如果表达式需要依赖其他表达式的值, 则要在依赖表达式计算之后执行 * @return 执行顺序 */ ExecuteOrder executeOrder(); /** * 该表达式的最大字符串长度值 * * @return 最大长度值 */ int maxLength(); }

通过实现此接口即可实现任何自定义的单号生成逻辑。如下是自定义的单号校验位生成表达式示例:

public class CheckSumExpressionimplements Expression { public Object eval(GenContext context) { Long newId = (Long) context.get("sequence"); if (newId == null) { throw newRuntimeException("sequence can not be null when calculate checksum"); } return newId * 9 % 31 % 10; } public ExecuteOrder executeOrder() { return ExecuteOrder.AfterNormal; } public int maxLength() { return 1; } }

总结

本文提到了多种单号数字序列生成方式,还介绍了高可用、高性能以及扩展性的实现方式。

要根据场景, 并发量, 单号类型数量选择数字序列生成方式; 不要裸奔, 要使用高可用+高性能序列生成器, 保证单号生成方式的可用性和性能; 底层序列要从物理上做隔离, 否则出现硬件故障高可用机制也会时效; 使用了多个底层序列生成方式时生成的序列是大致自增, 不能保证完全自增, 这是设计使然, 如果要保证完全自增, 则会出现单点, 在完全自增和单点的选择上, 我们选择了大致自增+非单点; 高性能序列生成的性能可以通过调节其memoryBitLength属性来提高, 但要根据业务实际情况来做选择,memoryBitLength属性值越高在内存生成的序列数越多,性能越高, 但在进程停止时丢失的序列也会越多。


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有