一种适合容器化部署的雪花算法ID生成器

雪花算法简介

SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。

image.png

雪花算法有以下几个优点:

  • 高并发分布式环境下生成不重复 id,每秒可生成百万个不重复 id。
  • 基于时间戳,以及同一时间戳下序列号自增,基本保证 id 有序递增。
  • 不依赖第三方库或者中间件。
  • 算法简单,在内存中进行,效率高。

雪花算法有如下缺点:

  • 依赖服务器时间,服务器时钟回拨时可能会生成重复 id。算法中可通过记录最后一个生成 id 时的时间戳来解决,每次生成 id 之前比较当前服务器时钟是否被回拨,避免生成重复 id。
  • 需要配置机器ID和服务器ID

[参考来源](SnowFlake 雪花算法详解与实现 - 掘金 (juejin.cn))

容器化部署雪花算法遇到的问题

  • 容器化无状态部署机器ID不可获取

    以前项目使用物理机器部署时,我们可以根据机器的IP分配对就的机器id,可是现在都是使用容器化部署,一般都是部署成无状态模式,无法获取workId;

  • 一个容器一般只部署一个服务,所有服务id可以不需要了。

解决思路

  • 将机器id和服务id合并
  • 项目启用时通过redis获取一个workId

RID的诞生

基本思路

  • 为每个微服务配置一个rid.redisKey,当作该服务在redis中的唯一标识服务
  • 在项目启用时,将rid.redisKey自增,获取到一个workId
  • 将wordId与maxWorkerId取余运算,得到单个服务的唯一workId

核心代码


/**
 * 基本Redis生成ID
 */
@Component
public class ID {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static Long REDIS_ID;
    /**
     * redis KEY 不同项目需要修改
     */
    @Value("${rid.redisKey}")
    private String ID_REDIS_KEY = "RID";
    /**
     * 起始时间戳
     */
    @Value("${rid.startStamp}")
    private Long startStamp = 1577808000000L;

    /**
     * 机器id所占的位数 最多机器节点2^5=32个
     */
    @Value("${rid.workerIdBits}")
    private final long workerIdBits = 5L;
    /**
     * 序列号所占的位数 决定单个容器每毫秒生成速度,默认每毫秒生成 2^7=128
     */
    @Value("${rid.sequenceBits}")
    private final long sequenceBits = 7L;
    /**
     * 时间戳位数 从startStamp开始可以  2^41/(1000606024365)=69,大概可以使用69年。
     */
    private final long timeStampBits = 41L;

    private Long workerId;

    @PostConstruct
    void init() {
        REDIS_ID = stringRedisTemplate.opsForValue().increment(ID_REDIS_KEY) % maxWorkerId;
    }


    /**
     * 时间戳最大值
     */
    private final long maxTimeStamp = ~(-1L << timeStampBits);
    /**
     * 机器id的最大值
     */
    private final long maxWorkerId = ~(-1L << workerIdBits);
    /**
     * 序列号的最大值
     */
    private final long maxSequence = ~(-1L << sequenceBits);


    private long sequence = 0L;
    private long lastTimeStamp = -1L;


    public synchronized long id() {
        long currentTimeStamp = timeGen();
        if (currentTimeStamp < lastTimeStamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimeStamp - currentTimeStamp));
        }
        if (lastTimeStamp == currentTimeStamp) {
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                currentTimeStamp = tilNextMillis(lastTimeStamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimeStamp = currentTimeStamp;
        return ((currentTimeStamp - startStamp) & maxTimeStamp) << (sequenceBits + workerIdBits) | (workerId << sequenceBits) | sequence;
    }


    private long tilNextMillis(long lastTimeStamp) {
        long timeStamp = timeGen();
        while (timeStamp <= lastTimeStamp) {
            timeStamp = timeGen();
        }
        return timeStamp;
    }


    private long timeGen() {
        return System.currentTimeMillis();
    }


    private static class SingletonHolder {
        private static ID ID;

        static {
            ID = new ID(REDIS_ID);
        }
    }

    public static ID getInstance() {
        return SingletonHolder.ID;
    }


    private ID() {
    }

    private ID(long workerId) {
        this.workerId = workerId;
    }
}
@Component
public class RID {

    public static Long generateId() {
        return ID.getInstance().id();
    }
}

测试代码

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class SpringBootApplicationTests {

    @Test
    public void testId() {
        Set<Long> ids = new HashSet<>();
        int size = 1000000;
        long startTime=System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            ids.add(RID.generateId());
        }
        System.out.println(String.format("generate %d ids spend  %s ms",size,System.currentTimeMillis()-startTime));
        Assert.assertEquals(size, ids.size());
    }

}

优点

  • 无需配置机器ID,服务ID,可以用微服务的applicationId当作rid.redisKey,可无状态化部署
  • 单个微服务容器最大节点数量及生成速度可配置
  • ID大部分情况是自增了

存在问题

  • ID只能做到单个容器唯一,不可做到全局唯一
  • 2^workerIdBits> 2倍容器数据,容器节点重启会生成新的容器,然后替换原来老容器
  • 集群中如果有容器一直不重启,后面重启容器可能会分配到相当的workId导致ID重复
  • 生成的ID位数不固定 当前时间-startStamp 位数会随着时间增加,数据库不要用varchar类型
  • 服务器时间回拨,可能导致ID重复
  • ID不是严格全局自增,同一毫秒内rid.redisKey达到maxWorkerId后从0开始,可能导致生成ID比其它服务生成的小

最后

猜你喜欢

转载自blog.csdn.net/whzhaochao/article/details/129967344