1总结
雪花算法:
- 时间回拨问题
- 时间戳+工作机器ID+数据中心ID 组成。后两个可以随机数。 分布式集群部署就有可能重复(部署在同一个服务器多个服务 等等) (ip+端口+随机数)
美团Leaf snowflake雪花算法模式:zk
美团Leaf segment号段模式:双缓存策略,很大的缓解了数据库压力。
2 美团
2.1 美团Leaf snowflake雪花算法模式
snowflake雪花算法模式要依赖于Zookeeper组件,必须要懂得安装Zookeeper和使用
snowflake雪花算法模式同一个服务器 部署多个项目,如何保证工作机器id和数据中心id不重复
官网spring-boot-starter下载地址
https://github.com/Meituan-Dianping/Leaf/tree/feature/spring-boot-starter
导入项目leaf-parent,使用Maven install到Maven本地仓库,然后在自己新建立的项目中引入依赖
<dependency>
<groupId>com.sankuai.inf.leaf</groupId>
<artifactId>leaf-boot-starter</artifactId>
<version>1.0.1-RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.6.0</version>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
创建问价leaf.properties
#属性注入类文件是:com.sankuai.inf.leaf.plugin.LeafSpringBootProperties
#leaf.name:leaf 服务名,用于zookeeper的保存路径
#com.sankuai.inf.leaf.common.PropertyFactory
leaf.name=leaf_id
#雪花算法ID
#当然,为了追求更高的性能,需要通过RPC Server来部署Leaf 服务,那仅需要引入leaf-core的包,把生成ID的API封装到指定的RPC框架中即可。
#是否开启snowflake模式,默认是false
leaf.snowflake.enable=true
#leaf.snowflake.address:snowflake模式下的zk地址,包括IP和端口
leaf.snowflake.address=192.168.170.14:2181
#leaf.snowflake.port:snowflake模式下的服务注册端口(非Zk端口)
#leaf.snowflake.port就是leaf项目的访问端口
#例如你在一台服务器上部署了多个leaf服务,每个leaf服务的HTTP服务访问端口不一样,
#那么在使用snowflake模式生成ID时,每个leaf服务都会有一个唯一的workId进行区分,来防止id重复,
#这个workId就是个根据服务所在的服务器Ip+服务端口leaf.snowflake.port来唯一确定的,
#主要通过将ip:port以写入到zookeeper特定路径下,写入一个永久有序节点,会生成一个唯一的序列号,
#这个序列作为这个leaf服务的workId,以后再在这个ip的服务器上以这个端口启动leaf服务,
#会根据ip和端口去Zookeeper找到以前生成的对应的workId,进行使用
#示例:
#[zk: localhost:2181(CONNECTED) 7] get /snowflake/null/forever/168.168.2.120:9600-0000000002
#{"ip":"168.168.2.120","port":"9600","timestamp":1621225915118}
leaf.snowflake.port=9600
leaf.name要加上,不然在zookeeper设置的路径就是null,看起来不直观,建议设置成自己的,如:leaf_id
未设置前:
[zk: localhost:2181(CONNECTED) 16] ls /snowflake
[null]
[zk: localhost:2181(CONNECTED) 7] get /snowflake/null/forever/168.168.2.120:9600-0000000002
{"ip":"168.168.2.120","port":"9600","timestamp":1621225915118
设置leaf.name=leaf_id后:
[zk: localhost:2181(CONNECTED) 16] ls /snowflake
[leafId, leaf_id, null]
[zk: localhost:2181(CONNECTED) 15] get /snowflake/leaf_id/forever/168.168.2.120:9600-0000000000
{"ip":"168.168.2.120","port":"9600","timestamp":1621232446738}
3、启动类加上@EnableLeafServer 注解
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import com.sankuai.inf.leaf.plugin.annotation.EnableLeafServer;
@EnableLeafServer
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
public class IdLeafApplication {
public static void main(String[] args) {
SpringApplication.run(IdLeafApplication.class, args);
}
}
4、Controller端生成snowflake模式的ID
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.lqy.Result;
import com.sankuai.inf.leaf.common.Status;
import com.sankuai.inf.leaf.service.SnowflakeService;
@RestController
@RequestMapping("/leaf")
public class IdController {
//雪花算法ID
@Autowired
private SnowflakeService snowflakeService;
@RequestMapping("/snowflake")
public Result getSnowflakeId() {
//获取snowflake分布式ID
//id 这个参数是没有意义的,只是为了和号段模式的接口统一,所以要传一个参数,自己随意定义一个
com.sankuai.inf.leaf.common.Result r = snowflakeService.getId("id");
//判断是否成功,成功返回具体的id,不成功返回错误提示
if(r.getStatus() == Status.SUCCESS) {
return Result.ok(r.getId());
}
return Result.failMsg("获取snowflake分布式ID失败");
}
}
三、 美团Leaf segment号段模式
同:美团Leaf snowflake雪花算法模式。如果之前已经使用snowflake模式,此时不用重复引入
此处需要注意的是:
由于leaf-boot-starter使用的是druid连接数据,在默认情况使用的是Mysql 8.x版本的mysql-connector-java,会导致报错,
因为连接的驱动类不是:com.mysql.cj.jdbc.Driver,会导致项目启动不了,而且没有相关的日志输出。
最简单的方法,是让项目直接使用5.x版本的连接驱动,如下
<properties>
<!-- 5.1.49 -->
<!-- 8.0.25 -->
<mysql.version>5.1.49</mysql.version>
</properties>
2、在项目引入 log4j.properties 配置文件
解决因数据库驱动连接出错,导致无日志输出的问题。
具体如下(log4j.appender.file.File:路径修改成自己的路径):
log4j.rootLogger = INFO,stdout,file
### direct log messages to stdout ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%p][%l]%n%m%n%n
log4j.appender.stdout.DatePattern='.'dd
### direct messages to file land-landSupply.log ###
log4j.appender.file = org.apache.log4j.DailyRollingFileAppender
log4j.appender.file.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.file.File=E:/logs/id-leaf/leaf.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%p][%l]%n%m%n%n
##打印sql部分
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Connection = DEBUG
log4j.logger.java.sql.Statement = DEBUG
log4j.logger.java.sql.PreparedStatement = DEBUG
log4j.logger.java.sql.ResultSet = DEBUG
#下面的两条配置非常重要,设置为trace后,将可以看到打印出sql中 ? 占位符的实际内容
#this is the most important config for showing parames like ?
log4j.logger.org.hibernate.type=TRACE
#above two configs
log4j.logger.org.hibernate.tool.hbm2ddl=DEBUG
log4j.logger.org.hibernate.hql=DEBUG
log4j.logger.org.hibernate.cache=DEBUG
log4j.logger.org.hibernate.transaction=DEBUG
log4j.logger.org.hibernate.jdbc=DEBUG
log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=TRACE
log4j.logger.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
### set log levels - for more verbose logging change 'info' to 'debug' ###
3、创建leaf.properties文件
同:美团Leaf snowflake雪花算法模式。
追加下面的配置(官网是没有leaf.segment.driver-class-name,后面自己修改源码加的,用于适配Mysql 8.x的连接)
#分段模式
#配置Mysql数据库连接的Url、账号、密码(仅支持Mysql数据库,要先执行脚本创建数据库和表)
leaf.segment.enable=true
leaf.segment.url=jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8&pinGlobalTxToPhysicalConnection=true&autoReconnect=true
#com.mysql.cj.jdbc.Driver
#com.mysql.jdbc.Driver
#官网是没有leaf.segment.driver-class-name,后面自己修改源码加的,用于适配Mysql 8.x的连接
#如果不懂修改,建议参考【1、pom.xml 引入依赖】,直接使用Mysql 5的数据库驱动连接,就不需要配置修改leaf.segment.driver-class-name
leaf.segment.driver-class-name=com.mysql.cj.jdbc.Driver
leaf.segment.username=root
leaf.segment.password=root
4、创建数据库leaf和表leaf_alloc
标识符:biz_tag不能相同,所以可以直接使用表名作为biz_tag
#创建表
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '业务key',
`max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
`step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
`description` varchar(256) DEFAULT NULL COMMENT '业务key的描述',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
#插入分段测试数据,标识符:biz_tag不能相同 一次缓存1000个数
INSERT INTO leaf_alloc(biz_tag, max_id, step, description) VALUES('t_order', 1, 1000, '测试');
5、启动类加上@EnableLeafServer 注解:
同美团Leaf snowflake雪花算法模式。
6、Controller端生成segment号段模式的ID
import com.sankuai.inf.leaf.common.Status;
import com.sankuai.inf.leaf.service.SegmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/leaf")
public class IdController {
//号段ID
@Autowired
private SegmentService segmentService;
@RequestMapping("/segment/{segmentTag}")
public String getSegmentId(@PathVariable("segmentTag") String segmentTag) {
//获取snowflake分布式ID
com.sankuai.inf.leaf.common.Result r = segmentService.getId(segmentTag);
//判断是否成功,成功返回具体的id,不成功返回错误提示
if(r.getStatus() == Status.SUCCESS) {
return "获取成功+"+r.getId();
}
return "获取失败";
}
}
测试:localhost:8080/leaf/segment/t_order
3 雪花算法
import java.util.Random;
/**
* todo: 雪花算法获得唯一编号
*
*/
public class SnowKeyGenUtils {
private static volatile SnowKeyGenUtils snowKeyGenUtils = new SnowKeyGenUtils();
public static SnowKeyGenUtils getInstance() {
if (null == snowKeyGenUtils) {
synchronized (SnowKeyGenUtils.class) {
if (null == snowKeyGenUtils) {
snowKeyGenUtils = new SnowKeyGenUtils();
}
}
}
return snowKeyGenUtils;
}
//开始时间(2019-12-19)
private final long startTime = 1576741028115L;
//机器ID所占大小
private final long workerIdBits = 5L;
// 数据标识id所占的位数
private final long dataCenterIdBits = 5L;
// 支持的最大机器id(十进制),结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
// -1L 左移 5位 (worker id 所占位数) 即 5位二进制所能获得的最大十进制数 - 31
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据标识id - 31
private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
// 序列在id中占的位数
private final long sequenceBits = 12L;
// 机器ID 左移位数 - 12 (即末 sequence 所占用的位数)
private final long workerIdMoveBits = sequenceBits;
// 数据标识id 左移位数 - 17(12+5)
private final long dataCenterIdMoveBits = sequenceBits + workerIdBits;
// 时间截向 左移位数 - 22(5+5+12)
private final long timestampMoveBits = sequenceBits + workerIdBits + dataCenterIdBits;
// 生成序列的掩码(12位所对应的最大整数值),这里为4095 (0b111111111111=0xfff=4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long dataCenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
/**
* 功能描述: 随机生成 workerId 和 dataCenterId ,解决多台机器并发问题
*
* @author : pig
* @date :
*/
private SnowKeyGenUtils() {
long workerId = getRandom();
long dataCenterId = getRandom();
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;
}
/**
* 生成1-31之间的随机数
* @return
*/
private long getRandom() {
int max = (int) (maxDataCenterId);
int min = 1;
Random random = new Random();
return random.nextInt(max - min) + min;
}
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param dataCenterId 数据中心ID (0~31)
*/
public SnowKeyGenUtils(long workerId, long dataCenterId) {
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;
}
// 线程安全的获得下一个 ID 的方法
public synchronized String getNextId() {
long timestamp = currentTime();
//如果当前时间小于上一次ID生成的时间戳: 说明系统时钟回退过 - 这个时候应当抛出异常
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;
//毫秒内序列溢出 即 序列 > 4095
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = blockTillNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
long nextId = ((timestamp - startTime) << timestampMoveBits) //
| (dataCenterId << dataCenterIdMoveBits) //
| (workerId << workerIdMoveBits) //
| sequence;
return Long.toString(nextId);
}
// 阻塞到下一个毫秒 即 直到获得新的时间戳
private long blockTillNextMillis(long lastTimestamp) {
long timestamp = currentTime();
while (timestamp <= lastTimestamp) {
timestamp = currentTime();
}
return timestamp;
}
// 获得以毫秒为单位的当前时间
private long currentTime() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
String nextId = SnowKeyGenUtils.getInstance().getNextId();
}
}