共计 3686 个字符,预计需要花费 10 分钟才能阅读完成。
1. 前言
面对互联网零碎的三高(高可用,高性能,高并发),数据库方面咱们多会采纳分库分表策略,如此必然会面临另一个问题,分库分表策略下如何生成数据库主键?那么明天针对此问题,咱们就聊聊如何设计一款“百万级”的分布式 ID 生成器。
2. 我的项目背景
因为业务拓展单量剧增,为满足现有业务倒退,遂决定对以后业务进行分库分表革新。分库分表模式下如何保障逻辑表在不同库、不同表下主键的唯一性成为了首要解决的问题,之初思考仍采纳数据库形式生成主键,但思考数据库系统瓶颈、零碎性能等问题,故调研后决定开发部署一套可反对百万级的分布式 ID 生成器,以用来反对现有业务,并逐渐为后续其它业务做撑持。
3. 技术选型
明确我的项目背景之后,就是技术选型了。
之后比对了 uuid 形式、Redis 计数器、数据库号段、雪花算法、美团 Leaf 等多种 ID 生成器的形式。因为 uuid 的随机无序性,易导致 B +Tree 索引的决裂,不适宜做 MySQL 的数据索引;Redis 计数器须要思考其长久化形式,宕机状况下可能会导致号段反复等问题,故暂不思考以上 2 种形式。之后又对其它的形式如数据库号段、雪花算法等的优缺点、是否引入新的技术依赖、复杂度等进行剖析,最终决定采纳相似美团 Leaf 的形式生成分布式主键 ID。
4. 架构设计
4.1 总体架构
总体上采纳双缓存架构,业务 key 对应的号段长久化在数据库之中,每次从数据库加载指定步长号段保留到本地缓存,业务申请优先从本地缓存获取 ID。
执行步骤如下:
STEP1:服务启动或首次申请时 - 从数据库加载以后业务 key,依据业务 key 配置步长,加载号段到本地。
STEP2:业务 key 调用时,优先从本地缓存 A 获取 ID。
- step 2.1:如以后“本地缓存 A”的使用率超过 15%(可动静调整),将异步从数据库加载号段到本地缓存 B;
- step 2.2:如以后“本地缓存 A”号段已应用完,切换缓存为“本地缓存 B”,持续提供服务。
STEP3:返回申请后果(极其状况缓存 A 号段耗尽,缓存 B 号段未加载实现,重试肯定次数后失败)。
4.2 具体设计
如何反对百万的 QPS,如何保障业务的高可用?为满足高并发、高可用分布式号段的数据结构又该如何设计的呢?接下来咱们从表构造、缓存构造两个方面看下分布式号段的具体设计,逐渐揭开其神秘的面纱:
4.2.1 表结构设计
表外围字段如下:
id:主键
biz_key:业务
keymax_id:以后业务 key 号段应用的 MAX 值
step:步长(每次加载 step 步长到本地缓存)
<sql id="id_generator_sql">
id as id,
biz_key as bizKey,
max_id as maxId,
step as step,
create_time as createTime,
update_time as updateTime,
version as version,
app_name as appName,
description as description,
is_del as isDel
</sql>
<insert id="insert" parameterType="com.jd.presell.idgenerator.model.Segment">
insert into id_generator
(biz_key,max_id,step,create_time,update_time,version,app_name,description,isDel)
values
(#{bizKey},#{maxId},#{step},now(),now(),0,#{appName},#{description},#{isDel})
</insert>
4.2.2 缓存结构设计
理解完表构造之后,大家必定还会有疑难,如仅仅采纳数据库的形式实现分布式 ID,其可反对的 QPS、零碎的稳定性少数都得不到保障,那又是采纳什么样的数据形式保障系统的高并发、高可用呢?接下来咱们从“缓存构造”设计中找答案:
- Buffer(缓存管理器)
bizKey:业务
keysegments:数组存储双缓存
currentIndex:游标,指向 segments 中以后正在应用的缓存
segmentModifyStatus:CAS 形式更新此号段状态
readWritelock 读写锁:号段的读取、更新采纳加锁形式采纳读写锁(此场景读多写少)
private String bizKey; // 操作 key
private Segment[] segments; // 双缓存
private volatile int currentIndex; // 以后应用的 Segment[]的下标
private volatile int segmentModifyStatus;
private final ReadWriteLock readWritelock = new ReentrantReadWriteLock(); // 读写锁
- segment(实际操作缓存)
bizKey:业务
keymaxId:以后缓存反对最大值
step:数据库加载时业务 key 的步长 current:以后号段已用值
threshold:更新下一个缓存阀值
private String bizKey; //key
private volatile long maxId; // 以后号段最大值
private volatile int step; // 步长
private volatile AtomicLong current; // 以后号段已用值
private long threshold; // 加载下一个缓存阀值
private Date modifyTime; // 更新工夫, 前期用于动静计算 step
4.3 要害流程链路
当分明后面提到的“表构造”和“缓存构造”后,接下来咱们来看下要害流程链路,更加清晰的理解到以上介绍的“表”和“缓存”在业务中的利用,详细信息如下:
- 服务初始化加载业务 bizKey
- 依据业务 bizKey 获取 ID
- 双缓存 - 预加载(提前加载下一个缓存)
- 双缓存 - 缓存切换
置信大家能够从上图看出要害信息,充沛理解到要害业务及其实现细节,上面是从业务和技术上做简略的概述。
(1)业务概述
- 服务初始化加载号段:为了不影响服务公布后的 t,遂采纳饿汉式模式,服务启动时加载指定步长的号段到本地缓存;
- 业务 key 保护:新增或下线的业务 key 通过 JOB 定时保护,新增 bizKey 增加到本地缓存,生效 bizKey 从本地缓存移除(后期业务 key 比拟少全表扫描,前期 bizKey 较多时可采纳告诉或扫描指定工夫变更的增量 bizKey);
- 预加载:以后缓存应用超阈值后,异步加载另一个缓存;为了尽量保障业务的稳定性,个别设置以后缓存应用到 15% 左右(可动静调整),开始执行预加载;
- 缓存切换:以后缓存号段耗尽,切换到下一个缓存并持续提供服务;
(2)关键技术 - ReadWriteLock 锁利用:此业务场景是典型的读多写少场景,故采纳读写锁模式。
读锁:获取分布式 ID;
写锁:预加载下一个缓存、缓存切换。 - CAS 原子操作:预加载下一个缓存时,为了防止单机多线程同时操作,采纳 CAS 形式更新 Buffer 的状态标识,更新胜利的线程才能够进行异步预加载操作。
- volatile:保障数据的可见性,确保共享变量能被精确和统一地更新保障。
5. 总结 & 瞻望
我的项目实现之后进行压测,在步长设置正当的时候,单机可反对近 10 万 QPS,压测过程中其 TP 失常,TP99、TP999 根本维持在 5 毫秒以内,整体上已满足现阶段业务需要。
尽管现阶段的设计已满足以后业务需要,然而能够优化的空间还很大,咱们还有很长的路要走,比方上面的号段节约、动静布局步长等。
(1)号段节约
利用启动时加载号段,如遇服务重启、发版等状况会节约掉局部号段。
针对此问题能够:
- 服务启动时初始化 10% 步长的号段,尽量减少首次初始化号段数量
- 服务敞开时增加钩子,保留号段应用状况到 Redis,服务启动后可优化从 Redis 号段池加载到本地缓存。
(2)动静布局步长
目前步长是手工配置,前期可依据号段的更新频率,匹配肯定的规定,动静调整业务 key 对应的号段(能够在申请时配置:步长动静调整规定)。
(3)数据库分库分表
现阶段 bizKey 较少,前期有需要可依据 bizKey 分库分表。
(4)长久化形式优化
目前仅采纳 MySQL 长久化号段信息,依据业务能够增加多级缓存,可引入 Redis,数据库预加载号段到 Redis,本地缓存优先从 Redis 获取号段加载到本地。
(5)监控告警
联合公司组件,目前对单个接口以及单个 bizKey 的 QPS、可用率、TP 进行了监控。可在此基础上减少:号段更新频率、号段单机散布状况 (已散布号段、已应用号段) 等进行监控。
6. 结语
以上内容简略的总结了该项目标背景、选型、设计等内容,总体方案上或者并不是最优解,还有许多待改良点。也是秉着先有后优,逐渐拓展、迭代的思维,抉择了分期、分需实现,在满足以后业务的状况下,疾速、稳固、继续落地!
感激大家的反对,心愿通过这篇文章能够让你理解到,原来局部百万业务量的设计也并不简单,原来仅需上十台服务器也能够轻轻松松撑持百万 QPS 的业务!
* 文 / 袁向飞
@得物技术公众号