seata改进型雪花分布式ID算法-java实现

seata改进型雪花算法分布式ID-java实现

1,简介

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。通俗的讲就是,多台机器支撑一个服务,但是他们生成的id是不重复的,且最好单调递增(降低mysql B+聚簇索引的页分裂的出现)。

当前现有的实现方式有:

实现方式 描述 优缺点
mysql自增id 直接使用mysql自带的自增id功能
优点:
①实现简单
缺点:
①并发性能差
②依赖于mysql数据库
redis自增键值 直接使用redis自带的incr key功能实现 优点:
①实现简单
缺点:
①并发性能差
②依赖于redis缓存
uuid 直接使用代码生成的uuid 优点:
①实现简单
②并发性能高
缺点:
①id生成不是单调递增
雪花算法及其衍生改进型 使用标记位+时间戳+节点id+序列号的方式组成 优点:
①实现简单
缺点:
①时钟回溯问题
②标准版每个时刻只有4096个并发量
Seata改进型雪花算法 使用标记位+节点id+时间戳+序列号的方式组成 优点:
①实现简单
②并发性能可达409.6W/s
③解决部分时钟回拨问题
缺点:
①不是全局单调递增,只是分机器单调递增
美团leaf分布式id生成框架 直接调用leaf的分布式id生成服务 优点:
①并发性能高
②解决时钟回溯问题
缺点:
①需要额外依赖其他服务

2,优化策略:

  • 时间戳与节点ID互换位置

由原版的标记位(1位)+时间戳(41位)+节点ID(10位)+序列号(12位)

原版位分配策略

更改为 标记位(1位)+节点ID(10位)+时间戳(41位)+序列号(12位)

改进版位分配策略

  • id生成只依赖于初始化时缓存的时间戳,不再实时追随最新时间

3,核心解决问题:

1,解决原有雪花算法一个ms内4096/ms的性能限制。由于标准版雪花算法是实时追随系统时间的,所以1台机器1个ms内最多只能生成4096个唯一id;但是改进型只是在系统初始化时缓存一次时间戳,之后是在这个时间戳+序列号的组合基础上进行单调递增,即便序列号4095继续向上递增,也只会超前消费时间戳里面的位数,不会出现违反唯一性的问题;

2,线程安全(使用CAS原子类保证每一个节点ID内安全单调递增);

3,弱依赖于系统时间。只会在系统启动的时候缓存当前时间戳,之后就不依赖时间戳,即便时钟小幅度回拨也是不受影响;除非人为的大幅度回拨,那么会有影响;

4,其他问题:

1,理论上会有并发高的时候序列号消耗完,超前消费时间戳导致的数据重复可能。但是,前提是生成器的QPS稳定在4096/ms以上,也就是409.6W/s以上,但是我这边测试了一下在8C16G的机器上的QPS性能只有26.7W/s,所以说现在的瓶颈已经不是分布式id生成器,这个超前消费的问题现在不用担心

2,id不随机问题,这个是美团那边的leaf分布式id生成器的一个需求,他们怕被竞争对手窃取数据,直接通过一天id的起始值和终止值分析出业务量是多少!这个问题当前确实存在。

3,不是全局单调递增,只是分机器单调递增。这个可以查看博文【关于新版雪花算法的答疑】给出的解析,分段单调递增也是可以减少插入数据的也分裂问题,只不过是分段的段尾进行递增!

5,java代码实现

package site.activeclub.acCore.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;


/**
 * seata中优化的分布式雪花算法生成分段自增id
 */
@Slf4j
@Component
public class SnowflakeIdUtil implements InitializingBean{

    /**
     * 机器码移位53
     */
    private final long MACHINE_BIT = 53;

    /**
     * 时间戳移位12
     */
    private final long TIMESTAMP_BIT = 12;

    /**
     *
     * business meaning: machine ID (0 ~ 1023)【每个机器码下对应的id是分段单调递增】
     * actual layout in memory:
     * highest 1 bit: 0
     * next 10 bit: workerId【机器码】
     * middle  41 bit: timestamp【时间戳】
     * lowest  12 bit: sequence【这个时间戳下的自增id,严格单调递增】
     */
    private AtomicLong idSequence;

    @Value("${snowflake.worker.id:1}")
    private long workerId;

    /**
     * 将机器码移位到高53位
     */
    @Override
    public void afterPropertiesSet() {
        // 机器码左移至高位
        workerId <<= MACHINE_BIT;
        // 跟先前保存好的高11位进行一个或的位运算
        long startId = workerId | (System.currentTimeMillis()<<TIMESTAMP_BIT);
        idSequence = new AtomicLong(startId);
    }

    public long nextId() {
        return idSequence.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        SnowflakeIdUtil snowflakeIdUtil = new SnowflakeIdUtil();

        // 机器码左移至高位
        snowflakeIdUtil.workerId <<= snowflakeIdUtil.MACHINE_BIT;
        // 跟先前保存好的高11位进行一个或的位运算
        long startId = snowflakeIdUtil.workerId | (System.currentTimeMillis()<<snowflakeIdUtil.TIMESTAMP_BIT);
        snowflakeIdUtil.idSequence = new AtomicLong(startId);

        //计时开始时间
        long start = System.currentTimeMillis();
        //让100个线程同时进行
        final CountDownLatch latch = new CountDownLatch(100);
        //判断生成的20万条记录是否有重复记录
        final Map<Long, Integer> map = new ConcurrentHashMap();
        for (int i = 0; i < 100; i++) {
            //创建100个线程
            new Thread(() -> {
                for (int s = 0; s < 20000; s++) {
                    long snowID =snowflakeIdUtil.nextId();
                    log.info("生成雪花ID={}",snowID);
                    Integer put = map.put(snowID, 1);
                    if (put != null) {
                        throw new RuntimeException("主键重复");
                    }
                }
                latch.countDown();
            }).start();
        }
        //让上面100个线程执行结束后,在走下面输出信息
        latch.await();
        log.info("生成20万条雪花ID总用时={}", System.currentTimeMillis() - start);
    }

}

https://gitee.com/Mufasa007/activeclub/blob/master/ac-core/src/main/java/site/activeclub/acCore/utils/SnowflakeIdUtil.java

输出结果:

nowflakeIdUtil - 生成雪花ID=6753605615453437
00:11:48.165 [Thread-30] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615452978
...
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463735
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463736
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463737
00:11:48.201 [Thread-81] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463729
00:11:48.201 [Thread-81] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463739
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463733
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463740
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463738
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463742
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463741
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463743
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463744
00:11:48.201 [main] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成20万条雪花ID总用时=748

参考链接

  1. java算法(4)---静态内部类实现雪花算法
  2. Seata基于改良版雪花算法的分布式UUID生成器分析
  3. 关于新版雪花算法的答疑
  4. Leaf 美团点评分布式ID生成系统

posted on 2022-04-02 10:38  周健康  阅读(1224)  评论(1编辑  收藏  举报

导航