基于乐观锁实现的美团Leaf Segment的唯一ID生成器

背景:

分布式唯一ID主键几乎是每个应用都需要的一个功能,不论是生成订单还是为了表示记录的唯一性,都要使用到唯一ID的服务类,那么生成唯一ID除了常见的UUID,数据库自增主键生成外,还有哪些可选呢?本文就来介绍下基于乐观锁实现的美团的Leaf Segment的分布式唯一主键生成算法

基于乐观锁实现的Leaf Segment分布式唯一ID生成器:

这种算法对数据库自增主键时对数据库太过依赖和每次获取自增ID都要读写DB的性能损耗的优化,该算法可以容许短时间内的DB网络波动,并且其性能几乎不会受到DB IO性能的影响,那么下面我们就来看一下基于"段号分段"实现的Leaf Segment算法实现,其大概思路是首先从数据库中取出一段的段号放到内存中,比如第一次获取[0,1000)的段号到内存中进行ID的分配,当分配到1000的时候会向数据库中在申请[1000,2000)的段号进行分配,所以分配ID几乎都是内存中的操作,性能非常好,下面我们来看下如何实现:

一.创建数据库表

CREATE TABLE `uniq_id` (
  `biz_tag` varchar(100) DEFAULT '0' COMMENT '业务',
  `max_id` bigint(20) DEFAULT '0' COMMENT '当前已经使用的最大的号ID',
  `step` int(11) DEFAULT '0' COMMENT '当前已经使用的最大的号ID',
  Unique KEY (biz_tag)
) ENGINE=InnoDB AUTO_INCREMENT=1;

数据表中重要的字段包括:

a. biz_tag表示不同的业务标记,比如订单域和用户域分别是两个不同的标识

b.max_id当前业务已经获取到的最大的段号,比如2000,表示订单域或者是用户域已经分配到了2000这个ID值了

c. step表示每个业务每次获取的段号的范围,比如100,表示业务每次获取100个段号到内存中进行分配,一直到这100个段号分配完之前,不会再次和DB通信申请新的段号范围

二.多线程更新数据库字段时保证数据库更新的原子性

我们再向数据库申请段号范围时都是使用多线程的形式,那么我们如何保证多个线程拿到的段号不会重复呢?比如会不会把数据库的[2000,2100)这个段号范围重复给了不同的线程从而导致ID重复呢?如果我们在更新数据库时没有采取任何措施的话是可能发生这种情况的,那么如何防止这种情况的发生?我们可以使用DB的乐观锁来实现原子更新:

update uniq_id set max_id=max_id + step where biz_tag=#{biz_tag} and max_id=#{old_max_id}

只有当max_id和更新db前取出来的max_id一样时才会更新db的记录,否则就循环重试,通过这种乐观锁的更新控制机制,就可以保证不会出现分配相同的ID段号的情况

三.如何应付DB的抖动?

如果我们按照内存中分批完Db给的段号范围之后才去DB获取下一个段号范围的形式的话,有可能此时DB抖动的话,此时所有的线程都会卡主等待获取段号范围的线程的返回以便可以获取到唯一ID,然而此时获取段号范围的线程由于DB抖动,一直没法获取到段号范围,这就导致了整个应用的阻塞,造成严重影响,那么我们怎么避免这个问题呢?也就是我们怎么应付DB偶尔的抖动呢?

我们可以参照Hbase中双MemStore的实现,也就是维护在内存中维护两个不同的Segment结构,当第一个Segment中ID的分配已经达到比如10%时,就开启异步线程去获取新的段号更新第二个Segment,这样当第一个Segment的ID分配完后,直接就可以切换到第二个Segment继续分配了,不需要再去DB获取段号范围了,通过异步去更新另一个Segment段号范围的形式循环使用这两个Segment,这样就可以轻松的应付DB的抖动了

参考文献:

https://tech.meituan.com/2017/04/21/mt-leaf.html

https://juejin.cn/post/7191431147593662523

你可能感兴趣的:(数据结构,mysql,数学建模,java,开发语言)