雪花算法分配DataCenterId和WorkerId的一种思路

2023-04-29 14:31:11 889

雪花算法是其中一个用于解决分布式 id 的高效方案

因其具有自增的特性, 所以更符合b+tree的索引结构

SnowFlake 算法的优点:

  1. 高性能高可用:生成时不依赖于数据库,完全在内存中生成
  2. 高吞吐:每秒钟能生成数百万的自增 ID
  3. ID 自增:存入数据库中,索引效率高

SnowFlake 算法的缺点:

  1. 依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成 ID 冲突或者重复
  2. 其中10bit-工作机器id,如果手动设置重复也可能会造成 ID 冲突或者重复

在mybatis-plus中, 没有设置机器id时,会通过当前物理网卡地址和jvm的进程id自动生成。一般在一个集群中,MAC+JVM进程PID一样的几率非常小, 但是小并不是不可能.

mybatis-plus的 issue 有很多反馈了id重复的问题. 基本都是同一机器内通过docker部署多实例导致机器id重复, 进而导致id重复

这里分享一种思路

package com.thy.backend.parent.framework.mp;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean;

import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * <p>SnowflakeRedissonConfig</p>
 *
 * @author zzx
 * @version 1.0
 * @date 2023/4/26 18:18:46
 */
@Slf4j
@Data
@ConfigurationProperties(prefix = "snowflake.redisson")
public class SnowflakeRedissonConfig implements SmartLifecycle {

    private String address;

    private Integer database;

    private String password;


    private static long WORKER_ID;

    private static long DATA_CENTER_ID;

    private static String KEY;

    private static RLock LOCK;

    public static final int MAX_RETRY = 10;

    private volatile boolean running = false;

    @Bean
    public DefaultIdentifierGenerator defaultIdentifierGenerator(SnowflakeRedissonConfig redissonConfig)
            throws Exception {
        Config config = new Config();
        SingleServerConfig singleConfig = config.useSingleServer();
        singleConfig.setAddress(redissonConfig.getAddress());
        singleConfig.setDatabase(redissonConfig.getDatabase());
        if (StrUtil.isNotBlank(redissonConfig.getPassword())) {
            config.useSingleServer().setPassword(redissonConfig.getPassword());
        }
        singleConfig.setConnectionPoolSize(1);
        singleConfig.setConnectionMinimumIdleSize(1);
        singleConfig.setSubscriptionConnectionMinimumIdleSize(1);
        singleConfig.setSubscriptionConnectionPoolSize(1);


        RedissonClient client = Redisson.create(config);

        Random random = new Random();
        try {
            int inc = 0;
            while (true) {
                WORKER_ID = random.nextLong(1, 31);
                DATA_CENTER_ID = random.nextLong(1, 31);

                KEY = "snowflake_lock_" + DATA_CENTER_ID + "_" + WORKER_ID;
                LOCK = client.getFairLock(KEY);
                // wait 3s
                boolean isLock = LOCK.tryLock(3, TimeUnit.SECONDS);
                if (isLock) {
                    break;
                }
                if (inc >= MAX_RETRY) {
                    throw new Exception("雪花ID数据节点冲突: " + KEY);
                }
                inc++;
            }
            log.info("current snowflake node: {}", KEY);
        } catch (Exception e) {
            log.error("defaultIdentifierGenerator, lockKey: {}", KEY, e);
            throw e;
        }

        return new DefaultIdentifierGenerator(WORKER_ID, DATA_CENTER_ID);
    }

    public static long getWorkerId() {
        return WORKER_ID;
    }

    public static long getDataCenterId() {
        return DATA_CENTER_ID;
    }

    @Override
    public int getPhase() {
        //在 WebServerGracefulShutdownLifecycle 那一组之后
        return SmartLifecycle.DEFAULT_PHASE - 1;
    }

    @Override
    public void start() {
        this.running = true;
    }

    @Override
    public void stop() {
        this.running = false;

        try {
            if (Objects.nonNull(LOCK) && LOCK.isLocked()) {
                LOCK.unlock();
            }
        } catch (Exception e) {
            try {
                if (Objects.nonNull(LOCK) && LOCK.isLocked()) {
                    LOCK.forceUnlock();
                }
            } catch (Exception ex) {
                log.error("雪花ID数据节点释放失败, key: {}", KEY, e);
            }
        }

    }

    @Override
    public boolean isRunning() {
        return running;
    }
}

通过生成两个1-31的随机数 (31)10=(11111)2

31二进制为五位 两个正好就是10bit的机器id

通过Redisson对其加锁, 再使用WatchDog机制进行锁的保持, 从而保证当前DataCenterId和WorderId不会重复

这个类注册了一个Lifecycle事件, 当应用被关闭时, 将锁释放

先用unlock尝试, unlock失败(FairLock会验证操作解锁的线程id是否与加锁线程的id一致, 如不一致则抛出异常)再使用forceUnlock.

forceUnlock再失败则只需要等待lock过期即可, 默认为30s

番外

看看mybatis-plus中对雪花算法 时钟回拨问题的处理方式

com.baomidou.mybatisplus.core.toolkit.Sequence#nextId

public synchronized long nextId() {
    long timestamp = timeGen();
    // 如果当前时间戳小于上次生成id的时间 则发生了时钟回拨
    if (timestamp < lastTimestamp) {
        long offset = lastTimestamp - timestamp;
        if (offset <= 5) {
            try {
                // 若回拨小于5ms 则等待 5 << 1, 就是10ms, 注意存在cpu时间片这个概念, 并不一定是10ms
                wait(offset << 1);
                // 再生成一次 如果仍然小于上次的时间戳, 则抛出异常
                timestamp = timeGen();
                if (timestamp < lastTimestamp) {
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
        }
    }
    ...
}


雪花算法分配DataCenterId和WorkerId的一种思路

雪花算法是其中一个用于解决分布式 id 的高效方案因其具有自增的特性, 所以更符合b+tree的索引结构SnowFlake 算法的优点:高性能高可用:生成时不依赖于数据库,完全在内存中生成高吞吐:每秒钟能生成数百万的自增 IDID 自增:存入数据库中,索引效率高SnowFlake 算法的缺点:依赖与
2023-04-29

MyBatis-Plus动态返回实体类

1. 自定义SqlSession@Slf4j public class GenericSqlSession extends DefaultSqlSession { private static final ThreadLocal<Class<?>> CTX = new ThreadLoca
2022-07-18

MyBatis-Plus部分方法封装

在此仅记录一种思路public interface SuperMapper<T> extends BaseMapper<T> { default List<T> idNotInList(List<?> idList) { return selectList(new Quer
2022-01-29

freemarker 时间显示不正常 设置时区

项目在本地开发的时候显示正常,部署上服务器就一直差8个小时,最后发现freemarker官方文档有这样的说明time_zone:时区的名称来显示并格式化时间。 默认情况下,使用JVM的时区。 也可以是 Java 时区 API 接受的值,或者 "JVM default" (从 FreeMarker 2
2020-03-28
IDEA 2019.1 xml 不高亮

IDEA 2019.1 xml 不高亮

前几天更新了idea后,发现xml里的代码都没有了高亮,变得跟记事本一个德性了打开setting ,搜索 File Types,找到xml项, 查看下方的匹配格式,果然没有xml,(idea真是厉害)点击右方的+,输入*.xml,点击ok,解决问题
2020-03-28

npm install 淘宝镜像

npm install --registry=https://registry.npm.taobao.org
2020-03-28
Java中方法的参数传递机制

Java中方法的参数传递机制

来看一段代码 public class Man { private String name; private Integer age; public String getName() { return name; } publi
2020-03-28
基于自定义注解手写权限控制

基于自定义注解手写权限控制

方法一: AOP 方法二: 拦截器项目结构项目依赖<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-w
2020-03-28

Docker 部署 详细全过程 附代码

Docker 部署本站 全过程环境:CentOS7.61. 安装Docker其他版本CentOS可以参考这个https://help.aliyun.com/document_detail/187598.html查看本机内核版本,内核版本需高于 3.10uname -r 确保 yum 包最新yum u
2020-03-28

SpringBoot 启动普通java工程

引入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.0.9</version> </dependency>
2020-03-28

Vue.js DOM操作

<template> <input type="button" @click="reply($event)" value="回复"> </template> export default { methods: { replyFun(e) {
2020-03-29
CentOS7编译调试OpenJDK12

CentOS7编译调试OpenJDK12

1. 下载源码https://hg.openjdk.java.net/jdk/jdk12点击左侧的browse,再点击zip,就可以下载zip格式的源码压缩包。unzip xxx.zip 解压文件2. 安装jdkyum install java-11-openjdk-devel -y3. 运行con
2020-04-23
编写自己的Spring Boot Starter

编写自己的Spring Boot Starter

1.新建一个maven项目命名规则统一是xxx-spring-boot-starter完整pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"
2020-06-29