版权声明:
本文为博主原创文章,未经博主允许不得转载。关注公众技术汇(ID: jishuhui_2015)可联系到作者。

一、需求介绍

1、分布式环境下,保证每个序列号(sequence)是全系统唯一的;

2、序列号可排序,满足单调递增的规律;

3、特定场景下,能生成无规则(或者看不出规则)的序列号;

4、生成的序列号尽量短

5、序列号可进行二次混淆,提供可扩展的interface,业务方自定义实现。

二、方案设计

为了满足上述需求,发号器必须能够支持不同的生成策略,最好是还能支持自定义的生成策略,这就对系统本身的可扩展性提出了要求。

目前,发号器设计了两种比较通用的基础策略,各有优缺点,但结合起来,能达到优势互补的目的。

1、segment
第一种策略称之为『分段』(segment),下文将对其进行详细阐述:

整个segment发号器有两个重要的角色:Redis和MongoDB,理论上MongoDB是可以被MySQL或其他DB产品所替代的。

segment发号器所产生的号码满足单调递增的规律,短时间内产生的号码不会有过长的问题(可根据实际需要,设置初始值,比如 100)。

Redis数据结构(Hash类型)

key: <string>,表示业务主键/名称
value: {cur: <long>,表示当前序列号max: <long>,表示这个号段最大的可用序列号
}

取号的大部分操作都集中在Redis,为了保证序列号递增的原子性,取号的功能可以用Lua脚本实现。

--[[由于RedisTemplate设置的HashValueSerializer是GenericToStringSerializer,故此处的HASH结构中的VALUE都是string类型,需要使用tonumber函数转换成数字类型。
]]
local max = redis.pcall("HGET", KEYS[1], "max")  --获取一段序列号的max
local cur = redis.pcall("HGET", KEYS[1], "cur")  --获取当前发号位置
if tonumber(cur) >= tonumber(max) then  --没有超过这段序列号的上限local step = ARGV[1]if (step == nil) then  --没有传入step参数step = redis.pcall("HGET", KEYS[1], "step")  --获取这段序列号的step配置参数值endredis.pcall("HSET", KEYS[1], "max", tonumber(max) + tonumber(step))  --调整max参数值,扩展上限
end
return redis.pcall("HINCRBY", KEYS[1], "cur", 1)  --触发HINCRBY操作,对cur自增,并返回自增后的值

注意:在redis执行lua script期间,redis处于BUSY状态,这个时候对redis的任何形式的访问都会抛出JedisBusyException异常,所以lua script中的处理逻辑不得太复杂。

值得一提的是,即使切换到一个新的database,或者开启新线程执行lua script,都将会遇到同样的问题,毕竟redis是单进程单线程的。

如果不幸遇到上述问题,需要使用redis-cli客户端连上redis-server,向其发送SCRIPT KILL命令,即可终止脚本执行,

如果想避免上述问题,也可以直接使用Springboot提供的RedisTemplate,能支持绝不大部分redis command。

MongoDB数据结构

{bizTag: <string>,  表示业务主键/名称max: <long>,  表示这个号段最大的可用序列号step: <int>, 每次分段的步长timestamp: <long>,  更新数据的时间戳(毫秒)
}

MongoDB部分主要是对号段的分配进行管理,一个号段不能多发,也可以根据发号情况,适当放缩号段步长(step)。

到此为止,segment发号器的雏形已经形成了。

一个比较突出的问题是在两个号段衔接的时间点,当一个segment派发完了后,会对MongoDB和Redis中的数据中的max扩容,I/O消耗比正常发号要稍多,会遇到“尖刺”,如下示意图:

为了消除“尖刺”,可以使用双Buffer模型。示意图如下:

这个模型的核心思想就是“预分配”。可以设置一个阈值(threshold),比如20%,当Buffer-1里面的号段已经消耗了20%,那么立刻根据Buffer-1的max和step,开辟Buffer-2。

当Buffer-1完全消耗了,可以无缝衔接Buffer-2,。如果Buffer-2的消耗也达到阈值了,又可以开辟Buffer-1,如此往复。

接下来,我们来讨论一下异常/故障情况

① Redis宕机。因为大部分发号工作都是依靠Redis完成的,所以发生了这种情况是非常糟糕的。如果想有效降低此风险,最行之有效的办法是对Redis进行集群化,通常是1主2从,这样可以挺住非常高的QPS了。

当然也有退而求其次的办法,就是利用上述提到的双Buffer模型。不依赖Redis取号,直接通过程序控制,利用机器内存。所以当需要重启发号服务之前,要确保依赖的组件是运行良好的,不然号段就丢失了。

② 要不要持久化的问题。这个问题主要是针对Redis,如果没有记录下当前的取号进度,那么随着Redis的宕机,取号现场就变得难以恢复了;如果每次都记录取号进度,那么这种I/O高密度型的作业会对服务性能

造成一定影响,并且随着取号的时间延长,恢复取号现场就变得越来越慢了,甚至到最后是无法忍受的。除了对Redis做高可用之外,引入MongoDB也是出于对Redis持久化功能辅助的考虑。

个人建议:如果Redis已经集群化了,而且还开启了双Buffer的策略,以及MongoDB的加持,可以不用再开启Redis的持久化了。

如果考虑到极端情况下,Redis还是宕机了,我们可以使用MongoDB里面存下来的max,就max+1赋值给cur(避免上个号段取完,正好宕机了)。

③ MongoDB宕机。这个问题不是很严重,只要将step适当拉长一些(至少取号能支撑20分钟),利用Redis还在正常取号的时间来抢救MongoDB。不过,考虑到实际可能没这么快恢复mongo服务,可以在程序中采取

一些容错措施,比如号段用完了,mongo服务无法到达,直接关闭取号通道,直到MongoDB能正常使用;或者程序给一个默认的step,让MongoDB中的max延长到max+step*n(可能取了N个号段MongoDB才恢复过来),

这样取号服务也可以继续。依靠程序本身继续服务,那么需要有相关的log,这样才有利于恢复MongoDB中的数据。

④ 取号服务宕机。这个没什么好说的,只能尽快恢复服务运行了。

⑤ Redis,MongoDB都宕机了。这种情况已经很极端了,只能利用双Buffer策略,以及程序默认的设置进行工作了,同样要有相关的log,以便恢复Redis和MongoDB。

⑥ 都宕机了。我有一句mmp不知当讲不当讲……

2、snowflake

第二种策略是Twitter出品,算法思想比较巧妙,实现的难度也不大。

以上示意图描述了一个序列号的二进制组成结构。

第一位不用,恒为0,即表示正整数;

接下来的41位表示时间戳,精确到毫秒。为了节约空间,可以将此时间戳定义为距离某个时间点所经历的毫秒数(Java默认是1970-01-01 00:00:00);

再后来的10位用来标识工作机器,如果出现了跨IDC的情况,可以将这10位一分为二,一部分用于标识IDC,一部分用于标识服务器;

最后12位是序列号,自增长。

snowflake的核心思想是64bit的合理分配,但不必要严格按照上图所示的分法。

如果在机器较少的情况下,可以适当缩短机器id的长度,留出来给序列号。

当然,snowflake的算法将会面临两个挑战:

① 机器id的指定。这个问题在分布式的环境下会比较突出,通常的解决方案是利用Redis或者Zookeeper进行机器注册,确保注册上去的机器id是唯一的。为了解决

强依赖Redis或者Zookeeper的问题,可以将机器id写入本地文件系统。

② 机器id的生成规则。这个问题会有一些纠结,因为机器id的生成大致要满足三个条件:a. int类型(10bit)纯数字,b. 相对稳定,c. 与其他机器要有所区别。至于优雅美观,都是其次了。对于机器id的存储,可以使用HASH结构,KEY的规则是“application-name.port.ip”,其中ip是通过算法转换成了一段长整型的纯数字,VALUE则是机器id,

服务id,机房id,其中,可以通过服务id和机房id反推出机器id。

假设服务id(workerId)占8bit,机房id(rackId)占2bit,从1开始,workerId=00000001,rackId=01,machineId=00000000101

如果用Redis存储,其表现形式如下:

如果存储在文件中(建议properties文件),则文件名是sequence-client:8112:3232235742.properties,文件内容如下:

如果发号服务上线,直接按照“application-name.port.ip”的规则取其内容。

③ 时钟回拨。因为snowflake对系统时间是很依赖的,所以对于时钟的波动是很敏感的,尤其是时钟回拨,很有可能就会出现重复发号的情况。时钟回拨问题解决策略通常是直接拒绝发号,直到时钟正常,必要时进行告警。

三、程序设计

整个发号过程可以分成三个层次:

1、策略层(strategy layer):这个层面决定的是发号方法/算法,涵盖了上述所讲的segment和snowflake两种方式,当然,用户也可以自己扩展实现其他发号策略。

最顶上定义Sequence实际上就是发号的结果。bizType是对发号业务场景的定义,比如订单号,用户ID,邀请好友的分享码。

发号策略的init接口是发号前的初始化工作,而generate接口就是调用发号器的主入口了。

当然,考虑到各种异常情况,加入了拒绝发号的处理器(SequenceRejectedHandler),默认实现只是记录日志,用户可根据需求去实现该处理器,然后用set方法设置发号策略的拒绝处理器。

2、插件层(plugin layer):此处的插件可以理解是一种拦截器,贯穿SequenceStrategy的发号全周期。引入插件后,无疑是丰富了整个发号的操作过程,用户可以从中干预到发号的整个流程,以便达到其他的目的,比如:记录发号历史,统计发号速率,发号二次混淆等。

可以看出,插件被设计成『注册式』的,发号策略只有注册了相关插件之后,插件才能生效,

当然,一个插件能被多个发号策略所注册,一个发号策略也能同时注册多个插件,所以两者是多对多的关系,PluginManager的出现就是解决插件的注册管理问题。

从SequencePlugin的定义中可以发现,插件是有优先级(Order)的,通过getOrder()可以获得,在这套发号系统里,Order值越小,表示该插件越优先执行。此外,插件有三个重要的操作:

before,表示发号之前的处理。若返回了false,那么该插件后面的操作都失效了,否则继续执行发号流程。

after,表示发号之后的处理。

doException,表示插件发生异常的处理方法。

3、持久层(persistence layer):这个层面指代的是上述所提的MongoDB部分,如果不需要持久化的支持,可以不实现此接口,那么整个发号器就变成纯内存管理的了。

PersistRepository定义了基本的CRUD方法,其中persistId可以理解成上述提到的BizType。

一切的持久化对象都是从PersistModel开始的,上图中的Segment、PersistDocument都是为了实现分段发号器而定义的。

四、总结

这篇文章详细阐述了分布式发号器系统的设计,旨在能做出一个可扩展,易维护的发号系统。业界比较知名的发号算法似乎也不多,整个发号系统不一定就按照笔者所做的设计,还是要立足于具体的业务需求。

分布式发号器架构设计相关推荐

  1. 分布式发号器——Vesta

    1.基于时间戳 比如流水号规则如下:XX-YYYYMMDD-N位随机数,这也是企业级应用开发常用的规则.此流水号对人比较友好,可识别性高,但容量受后面随机数的限制,且数据量越大,生成时难度越高.前三部 ...

  2. 如何设计一个分布式环境下全局唯一的发号器

    一.如何设计一个分布式环境下全局唯一的发号器 1.UUID 常见的方式.可以利用数据库也可以利用程序生成,一般来说全球唯一. 优点: 简单,代码方便. 生成ID性能非常好,基本不会有性能问题. 全球唯 ...

  3. java 发号器_发号器设计

    需求背景 高效生成趋势有序的全局唯一ID,兼顾有序性.高性能.可扩展等因素. 主要应用于一下场景: 信息安全,如果id连续,那么就容易被恶意用户猜测 分库分表需要有一个唯一ID来标识一条数据 常见方案 ...

  4. 为什么要有ID发号器、原理是什么以及如何实现?

    一.前言 上一篇文章<如何将一个长URL转换为一个短URL?>中谈到如何将长地址URL转换为短地址URL,其中谈到了一个比较理想的解决方案就是使用发号器生成一个唯一的整数ID,然后转换为6 ...

  5. 深入理解美团 Leaf 发号器开源方案

    之前我们有聊过「如何设计一个分布式 ID 发号器」,其中有讲过 4 种解决方案,分别是: UUID 类雪花算法 数据库自增主键 Redis 原子自增 美团以第 2.3 种解决方案为基础,开发出了分布式 ...

  6. mysql id发号器_ID发号器介绍

    ID发号器介绍 php swoole 1.发号器介绍 什么是发号器? 全局唯一ID生成器,主要用于分库分表唯一ID,分布式系统数据的唯一标识. 是否需要发号器? 1)是否需要全局唯一. 分布式系统应该 ...

  7. 7种主流案例,告诉你调度器架构设计通用法则(干货!)

    女主宣言 今天小编为大家转载一篇来自DBAplus社群的干货文章,希望能够帮助大家对关于调度器的理解.作者张晨,Strikingly数据平台工程师,算法.分布式系统和函数式编程爱好者.Shanghai ...

  8. 语音卡开发系列: LightweightCTI架构设计(作者:东成西就)

    //评注:这是一位朋友的关于语音卡开发的开源框架,历时约一年.初步在.net实现,并有实用案例.后期希望开源并借助大家的力量完成c++,delphi的版本.其目的是使开发爱好者从某个台阶起步,更快的进 ...

  9. hbase 二进制数据写入_分布式数据库HBase的架构设计详解(有彩蛋)

    原标题:分布式数据库HBase的架构设计详解(有彩蛋) 本文根据DBAplus社群第99期线上分享整理而成,文末还有好书送哦~ 讲师介绍 陈鸿威 云财经大数据CTO 曾任百度高级工程师,现主持设计开发 ...

最新文章

  1. React:入门计数器
  2. UISwitch,UISegmentedControl及UISlider的初步学习
  3. 陕西师范大学 渝粤教育 《学前儿童数学教育》作业
  4. java 对话框 显示图片_Java对话框上显示图片
  5. Git的GUI工具sourcetree的使用
  6. SAP License:BASIS 事务代码速记
  7. 蓝桥杯 基础练习 查找整数
  8. Java中Dom解析xml文档
  9. idea导入一个工程后只显示pom文件_P1搭建第一个springboot应用
  10. dmx512如何帧同步_DMX512协议的前世今生 数据格式及传输简介
  11. CSS - 样式以及样式权重
  12. 安装教程 | 在谷歌安装印象笔记插件(实用,在谷歌浏览器添加印象笔记插件,整理遇到bug,收藏解决bug的文章)
  13. 〔王鹰教程五〕和弦的分类记忆法
  14. SM4算法大文件加密与字符串加密
  15. scrapy爬取知乎问题实战
  16. 垃圾分类的正确姿势?用 OpenCV 人工智能图像识别技术来进行
  17. 转载:SRT传输库评估报告(V1.0.0)
  18. C语言之memset函数
  19. import()函数和import语句
  20. 计算机组成原理实验 内容,计算机组成原理实验报告

热门文章

  1. PHP导入(百万级)Excel表格数据
  2. 重新配置Tomcat
  3. [TEST] 此文用以测试样式中的各种标签
  4. 初识dubbo(随笔)
  5. 人和人最大的差别不是技术上的高低,而是你的人生观价值观
  6. Apple Watch 的传感器
  7. 头脑风暴法利用计算机让大家进行思想交流,个人与团队管理期末复习
  8. 使用husky配置git代码提交规范
  9. 日历 fullCalendar 整合农历
  10. FlexRay™ 协议控制器 (E-Ray)-04