赵志浩
Published on 2022-06-23 / 11 Visits
0
0

发号器

当前发号器的主要逻辑

NumServiceImpl是发号器主要的实现类入口,NumProcessor接口为主要的发号器执行逻辑,该接口主要对应的抽象实现类为:AbstractNumProcessor
在该抽象类下面对应各个实现类,分别是:
DynamicNumProcessor>(SLOWORDERED(3, "慢速有序")),
IdWorkerNumProcessor>(QUICK(2, "snowflake")),(类似于雪花算法,这样的本地ID实现方案)
MySQLNumProcessor>(UNORDERED(0, "无序")),
RedisNumProcessor> (ORDERED(1, "快速有序"))

在以上类初始化时,会去注册对应的标识NumChannel.SLOWORDERED 等等,用来表示该类的类型,看似有对应的4套实现,但实际在使用过程中,主要是使用0,和3两个类型。慢速有序主要是针对IM聊天生成ID的场景,所生成的ID是绝对有序。无序场景是目前的大多数业务方使用方式。所以主要针对0和3,此处做下说明。

3、慢速有序
慢速有序的实现,其实就是每次都要走一遍MySql的update更新,对应的是乐观锁的实现方案,一个业务方获取对应的ids时,指定要获取的ids数量,如10个id。
此时发号器的操作是:从一个对应的单独的业务子表里如:ng_business_slowordered23(表的生成规则可能是每小时或者每天一个,规则随意,主要就是降低表数据),然后此时从该表中获取该业务所对应的num值,如:1000000000000000206。那么此时拿到这个结果后,将当前的号段+size,得到新的号段,此时将该结果更新到该表行中,也就是将num值更新为:1000000000000000216,那么更新时的where 条件当然就是num=old值的时候才进行更新,也就是:set num = 1000000000000000216 where num = 1000000000000000206,更新成功,则得到对应的num结果:1000000000000000216,然后将该结果,和对应的size10,for循环--,得到对应的10个具体的数值,也就是:1000000000000000207,1000000000000000208,等等。然后把给号码集合返回即可。

那么如果 update更新失败,则尝试重新更新,重试2秒钟,主要逻辑如下:

long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime <= 2000) {
1、int a = 获取当前db库新的num
2、int b = num +size 得到最新的num值
3、更新当前行数据 set num = b  where num = a
if(更新成功){
return b;
}else{
//否则则肯定还是执行while循环,然后重新执行该操作。获取db库最新num,然后num +size。然后更新。
}
}

所以其实,慢速有序的操作,其实就是利用了MySql来做全局的分布式锁的操作,来保证每次返回的ID都是有序的。并发请求获取发号器数据时,各个请求都需要先更新MySql 的行数据成功后,才能返回对应的id数据,否则则无法获取发号器数据。

所以其实就是将MySql作为了一个第三方的锁的应用来达到这个效果。所以这个模式的发号器,性能上会存在很大的问题。如果请求的并发量过高,其实都还是在竞争行锁更新的操作。会导致性能有很大的问题,所以这个模式基本不会也不推荐让业务方使用,只是对于IM的场景,最初已经接入了这个模式,后续也就是只给IM使用。其它场景不会让接入来使用。

0、无序(或者说,趋势有序)

趋势有序这个东西,也比较简单。想想一下你的发号器服务部署了10台容器,每个请求过来是随机调度到10台容器上的。

而趋势有序这个发号器的玩法是:首先先针对这个业务域设置对应的配置,业务名,buffSize,lowestCriticalValue等。buffSize表示服务内部会缓存的id数量,比如5000,lowestCriticalValue 500,表示当前的缓存数量已经使用了剩余500个了,那么就补充发号器的id数量,重新给到5000个。

1、首次请求,获取1000个id号码,调度到了A容器上,A容器判断,当前该业务没有缓存可用,此时直接查库,查询ng_business_mysql_autoincrement表,也就是发号批次表,查询该表内是否有记录该业务的发号批次,如果没有,则存储一条新的数据,发号批次则为1,然后插入成功,则根据当前发号批次去生成id集合,
插入失败(业务business字段,和version是唯一索引),并发插入则存在一方插入失败,插入失败,尝试get,get不到再插入,也就是这个逻辑。

插入成功,或者get到这条数据后,就根据这个发号批次去发号,原理就是,拿到发号批次为1,则开始循环 bufferSize次,产生ID号码,这个ID号码的产生规则,则是1开头,或者发号器这个业务本身有配置前缀,startNum,则 startNum + 1 +change 等,生成一个新的号码,我们假设为1开头,则此处生成的号码是1000000001,1000000002, 直到 100000005000 ,也就是获取到了5000个数。

那么此时把5000个数,存到了A容器的本地内存里,然后根据size中要获取的1000个id,从该5000个里面,return 1000个id号码。

2、那么当第二次请求,请求到了B容器的时候,此时B容器没有缓存数据,所以db 库里 get 这条记录的时候,此时get出来的发号器批次为1,然后1+1,也就是2,尝试循环更新到这个行数据里,更新成功,则拿到2这个批次后,生成新的5000个数,以2开头,可能是 200000001,200000002,2000005000,这样一批数据缓存到了B容器。那么return 1000,还缓存4000。

这种情况下,也就是每次的请求来获取id集合时,第一次获取1000个,是1开头的连续递增的一批数,第二次请求到了B容器,则获取到的批次数是2开头的,递增的一批次数量。 所以说这个模式拿到的号码是保证趋势递增。就是整体是递增的,但不是绝对递增。

那么对于每次查询获取的size数量,都超过buffer的话,则会每次都会执行乐观锁的行更新,这样就比较耗时一些。正常情况下,buffer在,每次都是消耗buffer,速度极快,只要补充队列中id集合的时候,才会进行一次更新的操作。

当buffer队列中id集合数量,小于lowestCriticalValue时,则单独开线程,补充发号器中队列数量。比如:5000buffer,剩1000的时候就补充,那么补充这个过程,
也就是update行数据的时候,尽管存在耗时的可能,但剩余的1000buffer,也可以保证新来的请求还可以一直有的使用。

所以这个模式下的速率是极高的。


其实发号器这里的主要逻辑是一样的,都是基于MySql乐观锁的方式来实现的,不同的是趋势递增内部增加了缓存效果,所以请求分发到不同的容器时,将会得到不同的数据集合。
A容器缓存:0-5000,B容器缓存:5000-10000,A容器再次补充数据则是:10000-15000,所以获取发号数据时是趋势递增。

而针对慢速有序,则是每次请求,都是需要执行乐观锁的update操作,更新成功则,返回对应的数据size,如请求:10个号段,则直接将该业务所对应的num update为10,更新成功则返回10个号段,同理,再次请求10个号段时,则 set num=20 where num =10 ,更新成功,则库里num为20,然后服务端返回10个号码给出去。由于每次请求无论是调度到任何一个容器,都是执行mysql 更新来获取的最新号段,所以是绝对递增的效果。请求进来,我先查询获取该业务对应的最新num,然后执行update操作后并返回对应的号段数据。所以,是绝对的号码递增效果。

针对趋势递增,每次update一次,缓存5000的数据,则相对在乐观锁的执行rt上并没有大的问题。

但慢速有序,每次的请求都会进行一次update的乐观锁操作,这样会导致该业务每次的发号器请求,都会导致该数据被更新,从而导致抢锁的操作。然后rt变严重。如何解决这个问题?拆分!

按照上述的逻辑,我们一般都是每个业务单独对应一条行数据,然后进行更新行字段的操作,来确定当前的num值。然后返回对应的数据。

那么该业务如果请求的QPS很高,每次都执行该一行数据的更新,岂不是问题很大。所以针对这个问题,慢速有序这边的解决方案就是拆分。

通常情况下,一个business和subsiness来确定一行数据。那么针对这种场景就拆分为多行数据就OK。也就是你一个业务对应100行数据,一个business,对应100个不同的subsiness。也就是对应100行不同的数据,你每次该业务过来的请求,都是分散到不同的行数据上的,这样所谓的行数据更新的锁的压力,岂不是就完全分散开了。

private static String getTableName(String subBusiness) {
        int hash = 0;
        char[] chars = subBusiness.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            hash = 31 * hash + chars[i];
        }
        if (hash < 0) {
            hash = -hash;
        }
        return TABLENAME + hash % 64;
    }
```
所以,绝对有序这块也就是完全按照这个逻辑来的,一方面拆行,一方面拆表。针对绝对有序共预先创建64个表。
表名规则就是ng_business_slowordered1,ng_business_slowordered2,ng_business_slowordered3 ..... 

业务接入发号器,创建对应的元数据信息时,只确定business和对应的一些startNum等基础数据即可,

而对于subBusiness,则根据业务方所传递的值来确定,并不固定死。
根据业务所传递的subBusiness 来执行上述的getTableName(),也就是根据对应的subBusiness的值来确定该subBusiness所hash后所要使用的表是那个表名,如:ng_business_slowordered2,则将该行数据,插入到ng_business_slowordered2表数据中即可。

后续根据该subBusiness来查询对应的发号器数据时,则根据hash规则,还是会映射到对应的ng_business_slowordered2表中,然后查询该subBusiness所对应的行数据,得到对应的num值。

所以针对绝对有序这块的逻辑,拆分,先拆表,再拆行,完全把压力在一个行上的乐观锁的压力,给分散开了。

原创声明:作者:赵志浩、个人博客地址:https://zhaozhihao.com

原创声明:笔名:陈咬金、 博客园地址:https://www.cnblogs.com/zh94/


Comment