大家好,我是树哥。
在复杂的分布式系统中,往往需要对大量的数据和消息进行唯一标识,例如:分库分表的 ID 主键、分布式追踪的请求 ID 等等。于是,设计「分布式 ID 发号器」就成为了一个非常常见的系统设计问题。今天我将带大家一起学习一下,如何设计一个分布式 ID 发号器。
对于业务系统而言,对于全局唯一 ID 一般有如下几个需求:
对于上面两个需求来说,第一点是所有系统都要求的。而第二点则并不是所有系统都需要,例如分布式追踪的请求 ID 就可以不需要单调递增。而那些需要存到数据库里作为 ID 逐渐的场景,可能就需要保证全局唯一 ID 是单调递增的。
此外,我们可能还需要考虑安全方面的问题。如果一个全局唯一 ID 是顺序递增的,那么有可能会造成业务信息的泄露。例如订单 ID 每次递增 1,那么竞争对手直接通过订单 ID 就可以知道我们每天的订单数,这对于业务来说是不可接受的。
对于上述的诉求,现在市面上有非常多的唯一 ID 解决方案,其中最为常见的方案有如下 4 种:
UUID 全称叫 Universally Unique Identifier,即全局唯一标识符,它是 Java 中自带的 API。 一个标准的 UUID 包含 32 个 16 进制的数字,以中横线作为分隔符分为 5 段,每段的长度分别为 8 字符、4 字符、4 字符、4 字符、12 字符,大小为 36 个字符,如下图所示。一个简单的 UUID 示例:630e4100-e29b-33d4-a635-246652140000。
对于 UUID 这种唯一 ID 解决方案,优点是没有外部依赖,纯本地生成,因此其性能非常高。但缺点也是非常明显的:
因此,对于 UUID 而言,其比较适用于非数据库 ID 存储的情况,例如生成一个本地的分布式追踪请求 ID。
雪花算法(SnowFlake)是 Twitter 开源的分布式 ID 生成算法,其思路是用 64 位来表示一个 ID,并将 64 位分割成 4 个部分,如下图所示。
雪花算法的优点是:
雪花算法几乎可以是非常完美了,但它有一个致命的缺点 —— 强依赖机器时间。 如果机器上的系统时间回拨,即时间较正常的时间慢,那么就可能会出现发号重复的情况。对于这种情况,我们可以在本地维护一个文件,写入上次的时间戳,随后与当前时间戳比较。如果当前时间戳小于上次时间戳,说明系统时间出了问题,应该及时处理。
整体而言,雪花算法不仅长度更短,而且还具有业务含义,在数据库存储的场景下还能提高写入性能,因此雪花算法生成分布式唯一 ID 受到了大家的欢迎。 现在许多国内大厂的开源发号器的实现,都是在雪花算法的基础上做改进,例如:百度开源的 UidGenerator、美团开源的 Leaf 等等。这些类雪花算法的核心都是将 64 位进行更合理的划分,从而使得其更适合自身场景。
说起唯一 ID,我们自然会想起数据库的自增主键,因为它就是唯一的。
对于并发量低的情况下,我们可以直接部署 1 台机器,每次获取 ID 的时候就往数据库表插入一条数据,随后返回主键 ID。这种方式的好处是非常简单,实现成本低。此外,生成的唯一 ID 也是单调自增的,可以满足数据库写入性能的要求。
但其缺点也非常明显,即其强依赖数据库。当数据库异常的时候,会造成整个系统不可用。即使做了高可用切换,主从切换时数据同步不一致时,仍然可能造成重复发号。另外,由于是单机部署,因此其性能瓶颈限制在单台 MySQL 机器的读写性能上,注定无法承担起高并发的业务场景。
对于上面说到的性能问题,我们可以通过集群部署来解决。而集群部署之后的 ID 冲突问题,我们可以通过设置递增步长来解决。例如如果我们有 3 台机器,那么我们就设置递增步长为 3,每台机器的 ID 生成策略为:
这种方式解决了集群部署以及 ID 冲突的问题,可以在一定程度上提升并发访问的容量。但其缺点也比较明显:
由于 Redis 是内存数据库,其强大的性能非常适合用来实现高并发的分布式 ID 生成。基于 Redis 实现自增 ID,其主要还是利用了 Redis 中的 INCR 命令。该命令可以将某个数自增一并返回结果,并且这个操作是原子操作。
通过 Redis 实现分布式 ID 功能,其模式与通过数据库自增 ID 类似,只是存储介质从硬盘变成了内存。当单台 Redis 无法支撑并发请求的时候,Redis 同样可以通过集群部署和设置步长的方式去解决。但数据库自增主键有的问题,Redis 自增 ID 的方式也同样会有,即只能堆机器,同时水平扩展困难。此外,比起数据库存储的持久化,Redis 是基于内存的存储,需要考虑持久化的问题,这同样是一个头疼的问题。
看了这么多个分布式 ID 的解决方案,那么我们到底应该选哪个呢?
当我们在决策的时候,我们应该确定决策的维度。对于这个问题,我们应该关注的维度大致有:研发成本、并发量、性能、运维成本。
首先,对于 UUID 而言,其在各个方面其实都不如雪花算法,唯一的优点是 JDK 自带 API。因此,如果你只是极其简单地使用,那么就直接使用 UUID 就可以,毕竟雪花算法还得写一写实现代码呢。
其次,对于类雪花算法而言,其毋庸置疑是非常好的一种实现。与 UUID 相比,其不仅有 UUID 本地生成、不依赖第三方系统的优点,还有业务含义、能提高写入性能、解决了安全问题。但其缺点在于要实现雪花算法的代码,因此其研发成本稍稍比 UUID 高一些。
最后,对于数据库自增 ID 与 Redis 原子自增这两种方式。数据库自增 ID 的方式,其优点同样在于简单方便,不需要太高的研发成本。但其缺点是支撑的并发量太低,并且后续运维成本太高。因此,数据库自增 ID 这种方式,应该适用于小规模的使用场景下。而 Redis 原子自增的方式,其优先在于能支撑高并发的场景。但缺点是需要自行处理持久化问题,运维成本可能比较高。
本人更倾向于数据库自增方式。这两种方式都是非常类似的,唯一的区别是存储介质。Redis 原子自增方式非常快,可能单机可以是数据库方式的好几倍。但是如果要考虑持久化的问题,那对于 Redis 来说就太复杂了。
我们把上面这四种实现方式整理一下,可以汇总成下面的对比表格:
实现方案
优点
缺点
使用场景
UUID
性能高、不依赖第三方、研发成本低
字段长浪费存储空间、ID 不自增写入性能差、无业务含义
非常简单的使用场景(用于简单测试)
类雪花算法
有业务含义、单调递增写入性能好、不依赖第三方、业务安全
强依赖机器时间
高并发、业务场景复杂、需要将 ID 暴露给外部系统
数据库自增 ID
研发成本低、单调递增写入性能好
依赖数据库、只能堆机器提高性能、维护成本高
对持久性有要求,不暴露给外部系统
Redis 原子自增
高并发、单调递增写入性能好
依赖 Redis、有业务问题、有持久性问题
对持久性没要求,不暴露给外部系统
总的来说,如果站在长期使用考虑,那么运维成本、高并发肯定是需要考虑的。在这个基础条件下,类雪花算法与数据库自增 ID 或许是相对好的选择。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章