使用场景:集群高并发情况下分布式唯一全局Id的生成
为什么需要分布式全局唯一ID以及分布式ID的业务需求
ID生成规则部分硬性要求
ID号生成系统的可用性要求
UUID
UUID(Universally Unique ldentifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符, 示例:550e8400-e29b-41d4-a716-446655440000
如果只考虑唯一性是OK的,性能非常高,本地生成,没有网络消耗
但是UUID的生成是无序的。
- 无序,无法预测他的生成顺序,不能生成递增有序的数字。
- 首先分布式id-一般般都会作为主键,但是安装mysq|官方推荐主键要尽量越短越好,UUID每-一个 都很长,所以不是很推荐。
- 索引,B+树索引的分裂
既然分布式id是主键,然后主键是包含索引的,然后mysq|的索引是通过b+树来实现的,每一 次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键的b+树进行很大的修改,这一点很不好。 插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能
数据库自增主键 —— 单机
数据库自增主键 —— 集群分布式
那数据库自增ID机制适合作分布式ID吗?
答案是不太适合。要看数据量中小型还是可以的
系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3.4,5 (步长是1),这个时候需要扩容机器一台。 可以这样做:把第二台机器的初始值设置得比第- - 台超过很多,貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
数据库压力还是很大,每次获取ID都得读写一次数据库, 非常影响性能,不符合分布式ID里面的延迟低和要高QPS的规则(在高并发下,如果都去数据库里面获取id,那是非常影响性能的)
基于Redis生成全局id策略
twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移到Cassandra(由Facebook开发一套 开源分布式NoSQL数据库系统)因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务 。
Twitter的分布式雪花算法SnowFlake,经测试snowflake 每秒能够产生26万个自增可排序的ID
分布式系统中,有- .些需要使用全局唯- -ID的场景, 生成ID的基本要求
1bit
不用,因为二进制中最高位是符号位,1表示负数,0表示正数。
生成的id一般都是用整数,所以最高位固定为0。
41bit-时间戳,用来记录时间戳,亳秒级。
41位可以表示2^{41}-1个数字,
如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是: 0至2^{41}-1, 减1是因为可表示的数值范围是从0开始算的,而不是1.也就是说41位可以表示2{41}-1个毫秒的值,转化成单位年则是(2[(41}-1)/ (1000* 60 * 60 * 24 *365)= 69年
10bit-工作机器id,用来记录工作机器id.
可以部署在2^{10}= 1024个节点,包括5位datacenterld和15位workerld
5位(bit) 可以表示的最大正整数是2^{5}-1=31,即可以用0、1. 2、3、…这32个数字来表示不同的datecenterld或workerld
12bit-序列号,序列号,用来记录同毫秒内产生的不同id.
12位(bit) 可以表示的最大正整数是2^{12}-1 = 4095,即可以用0、1、2、3、…4094这4095个数字,
来表示同一-机器同一时间截(毫秒)内产生的4095个ID序号。
SnowFlake可以保证:
所有生成的id按时间趋势递增
整个分布式系统内不会产生重复id (因为有datacenterld和workerld来做区分)
twitter的雪花算法:https://github.com/twitter-archive/snowflake
GitHub上java版的雪花算法:
https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java
https://github.com/souyunku/SnowFlake/blob/master/SnowFlake.java
public class SnowflakeIdWorker {
// ==============================Fields==================
/** 开始时间截 (2019-08-06) */
private final long twepoch = 1565020800000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是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位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为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;
//==============================Constructors====================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(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;
}
// ==============================Methods=================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次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;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/** 测试 */
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 10; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
新建spring boot项目
除了spring boot需要的依赖,另外导入测试雪花算法需要的依赖
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-captchaartifactId>
<version>5.7.5version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.7.5version>
dependency>
/**
* @author cVzhanshi
* @create 2021-08-19 0:06
*/
@Slf4j
@Component
public class IdGeneratorSnowflake {
private long workerId = 0; //第几号机房
private long datacenterId = 1; //第几号机器
private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
@PostConstruct //构造后开始执行,加载初始化工作
public void init(){
try{
//获取本机的ip地址编码
workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("当前机器的workerId: " + workerId);
}catch (Exception e){
e.printStackTrace();
log.warn("当前机器的workerId获取失败 ----> " + e);
workerId = NetUtil.getLocalhostStr().hashCode();
}
}
public synchronized long snowflakeId(){
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId){
Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
//测试
public static void main(String[] args) {
System.out.println(new IdGeneratorSnowflake().snowflakeId()); //1428025721380536320S
}
}
/**
* @author cVzhanshi
* @create 2021-08-19 0:07
*/
@Service
public class OrderService {
@Autowired
private IdGeneratorSnowflake idGenerator;
public String getIDBySnowFlake() {
//新建线程池(5个线程)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 20; i++) {
threadPool.submit(() -> {
System.out.println(idGenerator.snowflakeId());
});
}
threadPool.shutdown();
return "hello snowflake";
}
}
/**
* @author cVzhanshi
* @create 2021-08-19 0:08
*/
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/snowflake")
public String index(){
return orderService.getIDBySnowFlake();
}
}
优点
缺点
解决时钟回拨问题