大道至简,知易行难
广阔天地,大有作为

Seata 1.4.0分布式事务全局唯一序列号总是被1024整除的问题

在开发中,针对Seata 1.4.0 Server的数据库进行分库分表时,发现其transaction_id总是被1024整除,导致数据总是会被插入到第0张表中。本来以为,Seata使用的是Snowflake算法生成的分布式序列号,在直觉上应该是均匀的(如果不做分库分表是一点问题没有),出现transaction_id总是被1024整除的现象很诡异,遂看其实现:

Seata Server启动的时候会通过命令行参数传入一个long类型的ServerNode,最终通过UUIDGenerator调到IdWorker,并通过nextId获取一个全局序列号用于区分每一个分布式事务。

Snowflake算法是一种以划分命名空间来生成ID的一种算法,这种方案把64-bit分别划分成多段,分别表示机器、时间、序号等:

其中:
1、时间戳是从当前毫秒时间到某个时间的增量,通常使用41bit来表示,因此可以覆盖(1L<<41)/(1000L*3600*24*365)=69年;
2、10bit的worker可以表示1024台机器;
3、12位自增序列号可以允许在同一个毫秒内容纳2^12个序号;

性能:
1、保证在任何一个IDC的任何一个实例在任意毫秒内生成的ID都是不同的;
2、理论最高QPS为409.6w/s(2^12*1000);

优势:
1、毫秒数在高位,自增序列在低位,整个ID都是趋势递增的;
2、不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也非常高;
3、可以根据自身业务特性分配比特位,非常灵活;

缺点:
1、强依赖机器时钟,如果机器上的时钟回拨,会导致发号重复或者服务会处于不可用状态;

显然,由于:

只要((timestamp – twepoch) << timestampLeftShift)、(workerId << workerIdShift)、sequence都可以被1024整除那么序列号就可以被1024整除。由于timestampLeftShift+workerIdShift=12+10=22、workerIdShift=12、sequence从0开始,因此只要获取序列号时跨毫秒,那么workerIdShift的12个算数左移就决定了得到的结果一定能够被2^10=1024整除(实际上是被2的1-12次方整除)。

不放心的话可以代码验证:

如果不切毫秒时间戳的话,则可以观察到序列号递增:

硬要解决这个问题的话比较简单,让sequence不要每次切换毫秒后从0开始就行了,在合理范围内随机,不过会牺牲一些序列(实际远远够用了):

在翻GitHub的时候发现1.4.0以后master分支对上述Snowflake进行了调整:

换了一种实现,改成了:

上述改过的全局事务ID与之前的Snowflake算法略有不同,其结构为:

很明显把WorkerId放在了最前面,然后用AtomicLong实现了序列递增(初始值用的是当前时间戳),跑一下长这样:

不过,这样,毫秒时间戳的随机性就只在初始化时有用了,本质上退化成了利用前十位WorkerId区分了每一个实例,然后每个实例维护一个递增序列,用作全局事务ID没有任何问题,拿来做分库分表也没有问题。由于初始化时sequence从0开始,所以每次启动后对1024取模后都是从1开始的:

但如果是从分布式序列号的角度看,由于毫秒时间戳不在最前貌似也不再是趋势递增的了。当然了,是否有必要保持递增趋势的需要看具体的业务场景。

参考文档:
1、https://tech.meituan.com/2017/04/21/mt-leaf.html

转载时请保留出处,违法转载追究到底:进城务工人员小梅 » Seata 1.4.0分布式事务全局唯一序列号总是被1024整除的问题

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址