• 雪花算法中机器id保证全局唯一


    关于分布式id的生成系统, 美团技术团队之前已经有写过一篇相关的文章, 详见 Leaf——美团点评分布式ID生成系统

    通常在生产中会用Twitter开源的雪花算法来生成分布式主键 雪花算法中的核心就是机器id和数据中心id, 通常来说数据中心id可以在配置文件中配置, 通常一个服务集群可以共用一个配置文件, 而机器id如果也放在配置文件中维护的话, 每个应用就需要一个独立的配置, 难免也会出现机器id重复的问题

    解决方案: 1. 通过启动参数去指定机器id, 但是这种方式也会有出错的可能性 2. 每个应用启动的时候注册到redis或者zookeeper, 由redis或zookeeper来分配机器id

    接下来主要介绍基于redis的实现方式, 一种是注册的时候设置过期时间, 配置定时器定时去检查机器id是否过期需要重新分配; 另一种是不设置过期时间, 只依靠在spring容器销毁的时候去删除记录(但是这种方式容易删除失败)

    实现方式一

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 日志包...开始 -->
        <!-- log配置:Log4j2 + Slf4j -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
        </dependency>
        <dependency> <!-- 桥接:告诉Slf4j使用Log4j2 -->
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
        </dependency>
        <dependency> <!-- 桥接:告诉commons logging使用Log4j2 -->
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jcl</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <!-- 日志包...结束 -->
    </dependencies>
    

    redis的配置

    /**
     * redis的配置
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port:6379}")
        private Integer port;
    
        @Bean
        public JedisPool jedisPool() {
            //1.设置连接池的配置对象
            JedisPoolConfig config = new JedisPoolConfig();
            //设置池中最大连接数
            config.setMaxTotal(50);
            //设置空闲时池中保有的最大连接数
            config.setMaxIdle(10);
            config.setMaxWaitMillis(3000L);
            config.setTestOnBorrow(true);
            //2.设置连接池对象
            return new JedisPool(config,host,port);
        }
    
    }
    

    snowflake算法中机器id的获取

    /**
     * snowflake算法中机器id的获取
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @Configuration
    public class MachineIdConfig {
    
        @Resource
        private JedisPool jedisPool;
    
        @Value("${snowflake.datacenter}")
        private Integer dataCenterId;
    
        @Value("${snowflake.bizType}")
        private String OPLOG_MACHINE_ID_kEY;
    
        /**
         * 机器id
         */
        public static Integer machineId;
        /**
         * 本地ip地址
         */
        private static String localIp;
        private static TimeUnit timeUnit = TimeUnit.DAYS;
    
        private static final Logger LOGGER = LoggerFactory.getLogger(MachineIdConfig.class);
    
        /**
         * 获取ip地址
         *
         * @return
         * @throws UnknownHostException
         */
        private String getIPAddress() throws UnknownHostException {
            InetAddress address = InetAddress.getLocalHost();
            return address.getHostAddress();
        }
    
        /**
         * hash机器IP初始化一个机器ID
         */
        @Bean
        public SnowFlakeGenerator initMachineId() throws Exception {
            localIp = getIPAddress();
    
            Long ip_ = Long.parseLong(localIp.replaceAll("\.", ""));
            //这里取128,为后续机器Ip调整做准备。
            machineId = ip_.hashCode() % 32;
            //创建一个机器ID
            createMachineId();
            LOGGER.info("初始化 machine_id :{}", machineId);
    
            return new SnowFlakeGenerator(machineId, dataCenterId);
        }
    
        /**
         * 容器销毁前清除注册记录
         */
        @PreDestroy
        public void destroyMachineId() {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.del(OPLOG_MACHINE_ID_kEY + dataCenterId + machineId);
            }
        }
    
    
        /**
         * 主方法:获取一个机器id
         *
         * @return
         */
        public Integer createMachineId() {
            try {
                //向redis注册,并设置超时时间
                Boolean aBoolean = registerMachine(machineId, localIp);
                //注册成功
                if (aBoolean) {
                    //启动一个线程更新超时时间
                    updateExpTimeThread();
                    //返回机器Id
                    return machineId;
                }
                //检查是否被注册满了.不能注册,就直接返回
                if (!checkIfCanRegister()) {
                    //注册满了,加一个报警
                    return machineId;
                }
                LOGGER.info("createMachineId->ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
    
                //递归调用
                createMachineId();
            } catch (Exception e) {
                getRandomMachineId();
                return machineId;
            }
            getRandomMachineId();
            return machineId;
        }
    
        /**
         * 检查是否被注册满了
         *
         * @return
         */
        private Boolean checkIfCanRegister() {
            Boolean flag = true;
            //判断0~127这个区间段的机器IP是否被占满
            try (Jedis jedis = jedisPool.getResource()) {
                for (int i = 0; i <= 127; i++) {
                    flag = jedis.exists(OPLOG_MACHINE_ID_kEY + dataCenterId + i);
                    //如果不存在。说明还可以继续注册。直接返回i
                    if (!flag) {
                        machineId = i;
                        break;
                    }
                }
            }
            return !flag;
        }
    
        /**
         * 1.更新超時時間
         * 注意,更新前检查是否存在机器ip占用情况
         */
        private void updateExpTimeThread() {
            //开启一个线程执行定时任务:
            //1.每23小时更新一次超时时间
            new Timer(localIp).schedule(new TimerTask() {
                @Override
                public void run() {
                    //检查缓存中的ip与本机ip是否一致, 一致则更新时间,不一致则重新获取一个机器id
                    Boolean b = checkIsLocalIp(String.valueOf(machineId));
                    if (b) {
                        LOGGER.info("更新超时时间 ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                        try (Jedis jedis = jedisPool.getResource()) {
                            jedis.expire(OPLOG_MACHINE_ID_kEY + dataCenterId + machineId, 60 * 60 * 24 * 1000);
                        }
                    } else {
                        LOGGER.info("重新生成机器ID ip:{},machineId:{}, time:{}", localIp, machineId, new Date());
                        //重新生成机器ID,并且更改雪花中的机器ID
                        getRandomMachineId();
                        //重新生成并注册机器id
                        createMachineId();
                        //更改雪花中的机器ID
                        SnowFlakeGenerator.setWorkerId(machineId);
                        // 结束当前任务
                        LOGGER.info("Timer->thread->name:{}", Thread.currentThread().getName());
                        this.cancel();
                    }
                }
            }, 10 * 1000, 1000 * 60 * 60 * 23);
        }
    
        /**
         * 获取1~127随机数
         */
        public void getRandomMachineId() {
            machineId = (int) (Math.random() * 127);
        }
    
        /**
         * 机器ID顺序获取
         */
        public void incMachineId() {
            if (machineId >= 127) {
                machineId = 0;
            } else {
                machineId += 1;
            }
        }
    
        /**
         * @param mechineId
         * @return
         */
        private Boolean checkIsLocalIp(String mechineId) {
            try (Jedis jedis = jedisPool.getResource()) {
                String ip = jedis.get(OPLOG_MACHINE_ID_kEY + dataCenterId + mechineId);
                LOGGER.info("checkIsLocalIp->ip:{}", ip);
                return localIp.equals(ip);
            }
        }
    
        /**
         * 1.注册机器
         * 2.设置超时时间
         *
         * @param machineId 取值为0~127
         * @return
         */
        private Boolean registerMachine(Integer machineId, String localIp) throws Exception {
            try (Jedis jedis = jedisPool.getResource()) {
                jedis.set(OPLOG_MACHINE_ID_kEY + dataCenterId + machineId, localIp);
                jedis.expire(OPLOG_MACHINE_ID_kEY + dataCenterId + machineId, 60 * 60 * 24 * 1000);
                return true;
            }
        }
    
    }
    

    雪花算法(雪花算法百度上很多, 自己可以随便找一个)

    /**
     * 雪花算法
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    public class SnowFlakeGenerator {
    
        private final long twepoch = 1288834974657L;
        private final long workerIdBits = 5L;
        private final long datacenterIdBits = 5L;
        private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
        private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
        private final long sequenceBits = 12L;
        private final long workerIdShift = sequenceBits;
        private final long datacenterIdShift = sequenceBits + workerIdBits;
        private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        private static long workerId;
        private long datacenterId;
        private long sequence = 0L;
        private long lastTimestamp = -1L;
    
        public SnowFlakeGenerator(long actualWorkId, long datacenterId) {
            if (actualWorkId > maxWorkerId || actualWorkId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
            }
            if (datacenterId > maxDatacenterId || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
            }
            workerId = actualWorkId;
            this.datacenterId = datacenterId;
        }
    
        public static void setWorkerId(long workerId) {
            SnowFlakeGenerator.workerId = workerId;
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
            if (timestamp < lastTimestamp) {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
            lastTimestamp = timestamp;
            return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
        }
    
        protected long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        protected long timeGen() {
            return System.currentTimeMillis();
        }
    }
    

    测试的controller

    /**
     * 雪花算法
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @RequestMapping("/snowflake")
    @RestController
    public class SnowflakeController {
    
        @Resource
        private SnowFlakeGenerator snowFlakeGenerator;
    
        /**
         * 获取分布式主键
         *
         * @return
         */
        @GetMapping("/get")
        public long getDistributeId() {
            return snowFlakeGenerator.nextId();
        }
    
    }
    

    配置文件

    server:
      port: 12892
    spring:
      redis:
        database: 0
        host: mini7
        lettuce:
          pool:
            max-active: 8
            max-idle: 8
            max-wait: -1
            min-idle: 0
        port: 6379
        timeout: 10000
    
    snowflake:
      datacenter: 1 # 数据中心的id
      bizType: order_id_ # 业务类型
    

    实现方式二

    机器id注册到redis的时候, 不设置过期时间 同时采用sharding-jdbc的分布式主键生成组件

    maven依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
    </parent>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <sharding-sphere.version>3.0.0.M4</sharding-sphere.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    
        <!--sharding-jdbc依赖开始-->
        <!-- for spring boot -->
        <dependency>
            <groupId>io.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>
    
        <!-- for spring namespace -->
        <dependency>
            <groupId>io.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-namespace</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>
        <!--sharding-jdbc依赖结束-->
    
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.41</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.0</version>
        </dependency>
    
        <!-- 日志包...开始 -->
        <!-- log配置:Log4j2 + Slf4j -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
        </dependency>
        <dependency> <!-- 桥接:告诉Slf4j使用Log4j2 -->
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
        </dependency>
        <dependency> <!-- 桥接:告诉commons logging使用Log4j2 -->
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-jcl</artifactId>
            <version>2.2</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <!-- 日志包...结束 -->
    </dependencies>
    

    配置文件

    server:
      port: 12893
    
    # sharding-jdbc分库分表的配置
    sharding:
      jdbc:
        datasource:
          ds0:
            type: com.alibaba.druid.pool.DruidDataSource
            driver-class-name: com.mysql.jdbc.Driver
            url: jdbc:mysql://localhost:3306/ds0
            username: root
            password: 123456
          names: ds0
    spring:
      redis:
        database: 0
        host: mini7
        lettuce:
          pool:
            max-active: 8
            max-idle: 8
            max-wait: -1
            min-idle: 0
        port: 6379
        timeout: 10000
    
    snowflake:
      datacenter: 1 # 数据中心的id
      bizType: sharding_jdbc_id_ # 业务类型
    

    redis的配置

    /**
     * redis的配置
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @Configuration
    public class RedisConfig {
    
        @Value("${spring.redis.host}")
        private String host;
    
        @Value("${spring.redis.port:6379}")
        private Integer port;
    
        @Bean
        public JedisPool jedisPool() {
            //1.设置连接池的配置对象
            JedisPoolConfig config = new JedisPoolConfig();
            //设置池中最大连接数
            config.setMaxTotal(50);
            //设置空闲时池中保有的最大连接数
            config.setMaxIdle(10);
            config.setMaxWaitMillis(3000L);
            config.setTestOnBorrow(true);
            //2.设置连接池对象
            return new JedisPool(config,host,port);
        }
    
    }
    

    机器id的配置

    /**
     * 保证workerId的全局唯一性
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @Component
    public class WorkerIdConfig {
    
        @Resource
        private JedisPool jedisPool;
    
        @Value("${snowflake.datacenter}")
        private Integer dataCenterId;
    
        @Value("${snowflake.bizType}")
        private String bizType;
        /**
         * 机器id
         */
        private int workerId;
    
        private static final Logger LOGGER = LoggerFactory.getLogger(WorkerIdConfig.class);
    
        public int getWorkerId() throws UnknownHostException {
            String ipAddress = getIPAddress();
            Long ip = Long.parseLong(ipAddress.replaceAll("\.", ""));
            //这里取128,为后续机器Ip调整做准备。
            workerId = ip.hashCode() % 1024;
            try (Jedis jedis = jedisPool.getResource()) {
                Long setnx = jedis.setnx(bizType + dataCenterId + workerId, ipAddress);
                if (setnx > 0) {
                    return workerId;
                } else {
                    // 判断是否是同一ip
                    String cacheIp = jedis.get(bizType + dataCenterId + workerId);
                    if (ipAddress.equalsIgnoreCase(cacheIp)) {
                        return workerId;
                    }
                }
                throw new RuntimeException("机器id:" + workerId + "已经存在, 请先清理缓存");
            }
        }
    
        @PreDestroy
        public void delWorkerId() {
            LOGGER.info("开始销毁机器id:" + workerId);
            try (Jedis jedis = jedisPool.getResource()) {
                Long del = jedis.del(bizType + dataCenterId + workerId);
                if (del == 0) {
                    throw new RuntimeException("机器id:" + workerId + "删除失败");
                }
            }
        }
    
        /**
         * 获取ip地址
         *
         * @return
         * @throws UnknownHostException
         */
        private String getIPAddress() throws UnknownHostException {
            InetAddress address = InetAddress.getLocalHost();
            return address.getHostAddress();
        }
    }
    

    sharding-jdbc分布式主键生成的配置

    /**
     * sharding-jdbc分布式主键生成的配置
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @Configuration
    public class ShardingIdConfig {
    
        @Resource
        private WorkerIdConfig workerIdConfig;
    
        @Bean
        public DefaultKeyGenerator defaultKeyGenerator() throws UnknownHostException {
            DefaultKeyGenerator generator = new DefaultKeyGenerator();
            // 最大值小于1024
            DefaultKeyGenerator.setWorkerId(workerIdConfig.getWorkerId());
            return generator;
        }
    
    }
    

    测试的controller

    /**
     * 生成分布式主键
     *
     * @author wang.js on 2019/3/8.
     * @version 1.0
     */
    @RestController
    @RequestMapping("/id")
    public class GenIdController {
    
        @Resource
        private DefaultKeyGenerator generator;
    
        @GetMapping("/get")
        public long get() {
            return generator.generateKey().longValue();
        }
    
    }
    

    sharding-jdbc的DefaultKeyGenerator中的源码中可以看到最大的workerId是1024L

    public static void setWorkerId(long workerId) {
        Preconditions.checkArgument(workerId >= 0L && workerId < 1024L);
        workerId = workerId;
    }
  • 相关阅读:
    9.11 eventbus
    9.10,,,实现new instanceof apply call 高阶函数,偏函数,柯里化
    9.9 promise实现 写完了传到gitee上面了,这里这个不完整
    9.5cors配置代码
    9.5 jsonp 实现
    9.5 http tcp https总结
    9.3 es6 class一部分 and es5 class 发布订阅
    8.30 cookie session token jwt
    8.30vue响应式原理
    warning: LF will be replaced by CRLF in renard-wx/project.config.json. The file will have its original line endings in your working directory
  • 原文地址:https://www.cnblogs.com/shanzhai/p/10500274.html
Copyright © 2020-2023  润新知