精华内容
下载资源
问答
  • 雪花算法机器id保证全局唯一

    千次阅读 2019-03-09 12:52:02
    雪花算法机器id保证全局...雪花算法中的核心就是机器id和数据中心id, 通常来说数据中心id可以在配置文件中配置, 通常一个服务集群可以共用一个配置文件, 而机器id如果也放在配置文件中维护的话, 每个应用就需要一...

    雪花算法中机器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;
    }
    
    展开全文
  • mybatis-plus自3.3.0开始,默认使用雪花算法+UUID(不含中划线),但是它并没有强制让开发者配置机器号。这一点很是疑惑,这样可能会让不了解雪花算法的人埋下了一个坑。是这么强大的一个框架难道真的没有做优化吗?

    1、雪花算法原理

            雪花算法使用一个 64 bit 的 long 型的数字作为全局唯一 id。这 64 个 bit 中,其中 1 个 bit 是不用的,然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。

    img

    1. 1bit,不用,因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。
    2. 41bit-时间戳,用来记录时间戳,毫秒级。
    3. 10bit-工作机器id,用来记录工作机器id。
    4. 12bit-序列号,序列号,用来记录同毫秒内产生的不同id。即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。

    SnowFlake可以保证:

    1. 所有生成的id按时间趋势递增
    2. 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分)

            如上大概了解了雪花算法的原理,而且也知道机器号对于雪花算法的重要性。如果机器号一样,可能会出现id重复的情况。

            mybatis-plus自3.3.0开始,默认使用雪花算法+UUID(不含中划线),但是它并没有强制让开发者配置机器号。这一点很是疑惑,这样可能会让不了解雪花算法的人埋下了一个坑。
            但是这么强大的一个框架难道真的没有做优化吗?带着问题,查看了下mybatis-plus雪花算法源码com.baomidou.mybatisplus.core.toolkit.Sequence。最终发现在没有设置机器号的情况下,会通过当前物理网卡地址和jvm的进程ID自动生成。这真的是一个较好的解决方案。一般在一个集群中,MAC+JVM进程PID一样的几率非常小。

    2、自动生成唯一机器号源码

    核心代码。有两个构造方法,一个无参构造,一个有参构造。

    public Sequence() {
        //通过当前物理网卡地址获取datacenterId
        this.datacenterId = getDatacenterId(maxDatacenterId);
        //物理网卡地址+jvm进程pi获取workerId
        this.workerId = getMaxWorkerId(datacenterId, maxWorkerId);
    }
    
    /**
     * 有参构造器
     *
     * @param workerId     工作机器 ID
     * @param datacenterId 序列号
     */
    public Sequence(long workerId, long datacenterId) {
        Assert.isFalse(workerId > maxWorkerId || workerId < 0,
                String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        Assert.isFalse(datacenterId > maxDatacenterId || datacenterId < 0,
                String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }
    
    • 无参构造 开发者没有设置机器号时
    • 有参构造 开发者自行设置机器号
    protected static long getDatacenterId(long maxDatacenterId) {
        long id = 0L;
        try {
            //获取本机(或者服务器ip地址)
            //DESKTOP-123SDAD/192.168.1.87
            InetAddress ip = InetAddress.getLocalHost();
            NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            //一般不是null会进入else
            if (network == null) {
                id = 1L;
            } else {
                //获取物理网卡地址
                byte[] mac = network.getHardwareAddress();
                if (null != mac) {
                    id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
                    id = id % (maxDatacenterId + 1);
                }
            }
        } catch (Exception e) {
            logger.warn(" getDatacenterId: " + e.getMessage());
        }
        return id;
    }
    
    /**
     * 获取 maxWorkerId
     */
    protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);
        //获取jvm进程信息
        String name = ManagementFactory.getRuntimeMXBean().getName();
        if (StringUtils.isNotBlank(name)) {
            /*
             * 获取进程PID
             */
            mpid.append(name.split(StringPool.AT)[0]);
        }
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
    }
    
    展开全文
  • 雪花算法是生成数据id非常好的一种方式,但是对于集群应用,让不同的机器自动产生不同的机器id传统做法就是针对每一个机器进行单独配置,但这样做不利于集群水平扩展,且操作过程非常复杂,所以每一个机器在集群环境...

    雪花算法是生成数据id非常好的一种方式,机器id是雪花算法不可分割的一部分。但是对于集群应用,让不同的机器自动产生不同的机器id传统做法就是针对每一个机器进行单独配置,但这样做不利于集群水平扩展,且操作过程非常复杂,所以每一个机器在集群环境下是一个头疼的问题。现在借助spring+redis,给出一种策略,支持随意水平扩展,肥肠好用。
    大致策略分为4步:
    1.对机器ip进行hash,对某一个(大于你的机器的个数)数取模,作为初始的机器ID。
    2.将机器id存储到redis中(注册)。
    3.以此类推,每一个机器初始化的时候都向redis注册一个数,若这个数注册过了,则+1重新注册。
    4.在spring容器销毁时从redis中将注册信息删除。
    下面给出具体实现代码:

    @Service("machineIdUtil")
    public class MachineIdUtil {
    //日誌
        private static Logger logger = LogUtil.getLogger(MachineIdUtil.class);
      /**
      *redis 实例
      */
        @Autowired
        private Cluster jimClient;
        /**
         * 机器id
         */
        public static Integer machine_id;
        /**
         * 本地ip地址
         */
        private static String localIp;
        private static TimeUnit timeUnit = TimeUnit.DAYS;
    
        /**
         * hash机器IP初始化一个机器ID
         */
        @PostConstruct
        public void initMachineId() throws Exception {
            localIp = IpUtil.getInet4Address();
    
            Long ip_ = Long.parseLong(localIp.replaceAll("\\.", ""));
            //这里取128,为后续机器Ip调整做准备。
            machine_id = ip_.hashCode()% 128;
            //创建一个机器ID
            createMachineId();
            logger.info("初始化 machine_id :{}", machine_id);
            SnowFlakeGenerator.initMachineId(machine_id);
        }
    
        /**
         * 容器销毁前清除注册记录
         */
        @PreDestroy
        public void destroyMachineId() {
            jimClient.del(RedisConstant.OPLOG_MACHINE_ID_kEY + machine_id);
        }
    
    
        /**
         * 主方法:获取一个机器id
         *
         * @return
         */
        public Integer createMachineId() {
            try {
                //向redis注册,并设置超时时间
                Boolean aBoolean = registMachine(machine_id);
                //注册成功
                if (aBoolean) {
                    //启动一个线程更新超时时间
                    updateExpTimeThread();
                    //返回机器Id
                    return machine_id;
                }
                //检查是否被注册满了.不能注册,就直接返回
                if (!checkIfCanRegist()) {
                    //注册满了,加一个报警
                    Profiler.businessAlarm("medicine-oplog-createMachineId", System.currentTimeMillis(), "128个机器码已经注册满!");
                    return machine_id;
                }
                logger.info("createMachineId->ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate());
    
                //递归调用
                createMachineId();
            } catch (Exception e) {
                getRandomMachineId();
                return machine_id;
            }
            getRandomMachineId();
            return machine_id;
        }
    
        /**
         * 检查是否被注册满了
         *
         * @return
         */
        private Boolean checkIfCanRegist() {
            Boolean flag = true;
            //判断0~127这个区间段的机器IP是否被占满
            for (int i = 0; i <= 127; i++) {
                flag = jimClient.exists(RedisConstant.OPLOG_MACHINE_ID_kEY + i);
                //如果不存在。说明还可以继续注册。直接返回i
                if (!flag) {
                    machine_id = 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(machine_id));
                    if (b) {
                        logger.info("更新超时时间 ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate("yyyy-MM-dd HH:mm:ss"));
                        jimClient.expire(RedisConstant.OPLOG_MACHINE_ID_kEY + machine_id, 1, timeUnit);
                    } else {
                        logger.info("重新生成机器ID ip:{},machineId:{}, time:{}", localIp, machine_id, DateUtil.getDate("yyyy-MM-dd HH:mm:ss"));
                        //重新生成机器ID,并且更改雪花中的机器ID
                        getRandomMachineId();
                        //重新生成并注册机器id
                        createMachineId();
                        //更改雪花中的机器ID
                        SnowFlakeGenerator.initMachineId(machine_id);
                        //結束當前任務
                        logger.info("Timer->thread->name:{}", Thread.currentThread().getName());
                        this.cancel();
                    }
                }
            }, 10 * 1000, 1000 * 60 * 60 * 23);
        }
    
        /**
         * 获取1~127随机数
         */
        public void getRandomMachineId() {
            machine_id = (int) (Math.random() * 127);
        }
    
        /**
         * 机器ID顺序获取
         */
        public void incMachineId() {
            if (machine_id >= 127) {
                machine_id = 0;
            } else {
                machine_id += 1;
            }
        }
    
        /**
         * @param mechineId
         * @return
         */
        private Boolean checkIsLocalIp(String mechineId) {
            String ip = jimClient.get(RedisConstant.OPLOG_MACHINE_ID_kEY + mechineId);
            logger.info("checkIsLocalIp->ip:{}", ip);
            return localIp.equals(ip);
        }
    
        /**
         * 1.注册机器
         * 2.设置超时时间
         *
         * @param mechineId 取值为0~127
         * @return
         */
        private Boolean registMachine(Integer mechineId) throws Exception {
            return jimClient.set(RedisConstant.OPLOG_MACHINE_ID_kEY + mechineId, localIp, 1, TimeUnit.DAYS, false);
        }
    
    
    }
    
    
    展开全文
  • //机器ID 2进制5位 32位减掉1位 31个 private long workerId; //机房ID 2进制5位 32位减掉1位 31个 private long datacenterId; //代表一毫秒内生成的多个id的最新序号 12位 4096 -1 = 4095 个 private long ...

     

    创建工具类

    SnowFlakeGenerateIDUtils
    @Component
    //@ConfigurationProperties(prefix = "snow-flake")
    //@PropertySource(value = {"classpath:/application.yml"}, encoding = "utf-8")
    public class SnowFlakeGenerateIDUtils {
        //因为二进制里第一个 bit 如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
    
        //机器ID  2进制5位  32位减掉1位 31个
        private long workerId;
        //机房ID 2进制5位  32位减掉1位 31个
        private long datacenterId;
        //代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
        private long sequence;
        //设置一个时间初始值    2^41 - 1   差不多可以用69年
        private long twepoch = 1585644268888L;
        //5位的机器id
        private long workerIdBits = 5L;
        //5位的机房id
        private long datacenterIdBits = 5L;
        //每毫秒内产生的id数 2 的 12次方
        private long sequenceBits = 12L;
        // 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
        private long maxWorkerId = -1L ^ (-1L << workerIdBits);
        // 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
        private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    
        private long workerIdShift = sequenceBits;
        private long datacenterIdShift = sequenceBits + workerIdBits;
        private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
        //记录产生时间毫秒数,判断是否是同1毫秒
        private long lastTimestamp = -1L;
        private int count = 1;
    
        public long getWorkerId() {
            return workerId;
        }
    
        public long getDatacenterId() {
            return datacenterId;
        }
    
        public long getTimestamp() {
            return System.currentTimeMillis();
        }
    
        //public static SnowFlakeGenerateIDUtils snowWork = new SnowFlakeGenerateIDUtils(1,1,1);
        //用yml注入@value 静态变量赋值行不通
        public static long currentWorkerId;//当前机器
        public static long currentDatacenterId;//当前机房
        public static long currentSequence;//当前序列号
        public volatile static SnowFlakeGenerateIDUtils snowWork = null;
    
        public static String getSnowId() {
            //单例,解决并发问题
            if (snowWork == null) {
                synchronized (SnowFlakeGenerateIDUtils.class) {
                    if (snowWork == null) {
                        snowWork = new SnowFlakeGenerateIDUtils(currentWorkerId, currentDatacenterId, currentSequence);
                    }
                }
            }
            Long nextId = snowWork.nextId();
            return nextId.toString();
        }
    
        public SnowFlakeGenerateIDUtils() {
        }
    
        /***
         * 功能描述: 
         * 〈〉
         * @Param: [workerId:机器ID, datacenterId:机房ID, sequence:序列号]
         * @Return:
         * @Author: by
         * @Date: 2020/5/6 11:45
         */
        public SnowFlakeGenerateIDUtils(long workerId, long datacenterId, long sequence) {
            // 检查机房id和机器id是否超过31 不能小于0
            if (workerId > maxWorkerId || workerId < 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));
            }
            this.workerId = workerId;
            this.datacenterId = datacenterId;
            this.sequence = sequence;
        }
    
        // 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
        public synchronized long nextId() {
            // 这儿就是获取当前时间戳,单位是毫秒
            long timestamp = timeGen();
            if (timestamp < lastTimestamp) {
    
                System.err.printf(
                        "clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
                throw new RuntimeException(
                        String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
                                lastTimestamp - timestamp));
            }
    
            // 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
            // 这个时候就得把seqence序号给递增1,最多就是4096
            if (lastTimestamp == timestamp) {
    
                // 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
                //这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
                //4095:111111111111  ==  sequenceMask
                //4096:1000000000000
                //当sequence大于4095,与运算 1000000000000 & 111111111111 =0 0000 0000 0000 0000
                //即sequence =0
                sequence = (sequence + 1) & sequenceMask;
                //System.out.println("同一毫秒:\t" + timestamp + ",生成ID数:\t" + count);
                count++;
                //当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                    count = 1;
                }
    
            } else {
                sequence = 0;
                count = 1;
            }
            // 这儿记录一下最近一次生成id的时间戳,单位是毫秒
            lastTimestamp = timestamp;
            // 这儿就是最核心的二进制位运算操作,生成一个64bit的id
            // 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
            // 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
            return ((timestamp - twepoch) << timestampLeftShift) |
                    (datacenterId << datacenterIdShift) |
                    (workerId << workerIdShift) | sequence;
        }
    
        /**
         * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
         *
         * @param lastTimestamp
         * @return
         */
        private long tilNextMillis(long lastTimestamp) {
    
            long timestamp = timeGen();
    
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        //获取当前时间戳
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        public static void main(String[] args) {
            SnowFlakeGenerateIDUtils worker = new SnowFlakeGenerateIDUtils(1, 1, 1);
            for (int i = 0; i < 10; i++) {
                System.out.println(worker.nextId());
            }
        }
    
        @Value("${snow-id.currentWorkerId}")
        public void setCurrentWorkerId(long currentWorkerId) {
            SnowFlakeGenerateIDUtils.currentWorkerId = currentWorkerId;
        }
        @Value("${snow-id.currentDatacenterId}")
        public void setCurrentDatacenterId(long currentDatacenterId) {
            SnowFlakeGenerateIDUtils.currentDatacenterId = currentDatacenterId;
        }
        @Value("${snow-id.currentSequence}")
        public void setCurrentSequence(long currentSequence) {
            SnowFlakeGenerateIDUtils.currentSequence = currentSequence;
        }
    }

     

    yml参数配置

    snow-id:
      currentWorkerId: 1
      currentDatacenterId: 1
      currentSequence: 1

     

    展开全文
  • 分布式ID生成 - 雪花算法

    万次阅读 多人点赞 2018-08-30 00:00:03
    雪花算法是一种生成...1. 机器ID(5位)和数据中心ID(5位)配置没有解决,分布式部署的时候会使用相同的配置,任然有ID重复的风险。 2. 使用的时候需要实例化对象,没有形成开箱即用的工具类。 本文针对上面...
  • 配置雪花算法机器id和数据中心id 二 配置mybatis拦截器设置主键id package com.xy.pay.main.web.interceptor.snowFlakeIdWorke; import org.apache.ibatis.executor.Executor; import org.apache....
  • 雪花算法生成的id总共64位8个...sharding jdbc 4.1.1使用雪花算法生成分布式id时,会使用到属性work.id表示机器标识位,取值范围[0,1024),配置示例如下: sharding: tables: user: actual-data-nodes: ds$-..
  • 雪花算法是Twitter公司采用的开源的id...包括5位的数据中心id和5位的机器id。 12位的序列号-每毫秒都会重置,表示1毫秒内生成的第几个id。 private Long twepoch = 1288834974657L; ... public synchronized Long n
  • 其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0 4-2.png ...
  • 雪花算法 php Snowflake

    2020-06-24 17:16:51
    <?... /** * /** * ID 生成策略 * 毫秒级时间41位+机器ID, 10位+毫秒内序列12位。高位始终为0,表示正数。 * 0 41 51 64 * +-----------+------+------+ ... * 接着10bits是事先配置好的机器ID
  • 机器ID(5位)和数据中心ID(5位)配置没有解决,分布式部署的时候会使用相同的配置,任然有ID重复的风险。 使用的时候需要实例化对象,没有形成开箱即用的工具类。 本文针对上面两个问题进行解决,笔者的解决...
  • 雪花算法里最好用的主键ID生成工具 技术支持 开源地址:https://github.com/yitter/IdGenerator QQ群:646049993 算法介绍 ❄ 这是优化的雪花算法(雪花漂移),它生成的ID更短、速度更快。 ❄ 支持 k8s 等容器...
  • 在分布式系统中,全局唯一序列为分布式系统重要组成部分,而雪花算法(或基于雪花算法之上封装的类)在序列生成最为广泛,但是雪花算法需要获取应用所在服务器的ID--机器ID。如果配置文件为统一管理(配置中心),则...
  • 利用预先分配好的机器ID,工作区ID,机器时间可以生成全局唯一的随时间趋势递增的Long类型ID.长度在17-19位。随着时间的增长而递增,在MySQL数据库中,InnoDB存储引擎可以更快的插入递增的主键。而不像UUID那样因为...
  • 【business】 ID生成器设计参考资料雪花生成器根据不同业务配置成多个生成器生成器的生成逻辑配置 workerId测试雪花算法实现 参考资料 分布式系统里用户 ID 生成有什么好的方法和规则能满足“唯一、尽量短、不能...
  • 毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味 着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。可以保证几乎全球唯一! 5.2配置主键...
  • 主键生成策略的几种方式 数据库自增:利用数据库主键自增,优点:简单、便于排序,缺点:数据库迁移需要... Twitter的snowflake算法(雪花算法):生成一个19位的数字。使用41bit作为毫秒数,10bit作为机器ID(5个b.
  • MybatisPlus自带主键生成策略"雪花算法",如果不做配置,默认也是使用这个算法生成id。 snowflake(雪花)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:使用41bit作为毫秒数,10bit作为...
  • springboot集成百度UID

    千次阅读 2019-06-27 19:25:02
    我们在平时的项目中需要用到唯一ID,UID的生成可以...百度的uid是基于推特的雪花算法的改进,不需要在给每台机器指定一个数据中心,现在都是容器话部署,改动配置文件不现实。 第一步:https://github.com/baidu/...

空空如也

空空如也

1 2
收藏数 23
精华内容 9
关键字:

雪花算法机器id配置