雪花算法生成实例
一、集群高并发情况下如何保证分布式唯一全局id生成?
1.1 为什么需要分布式全局唯一ID以及分布式ID的业务需求
对分库分表后需要有一个唯一ID来标识一条数据或消息;
特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识
1.2 ID生成规则部分硬性要求
① 全局唯一:不能出现重复的ID号,作为唯一标识的基本要求
② 趋势递增:MySQL的InnoDB引擎中使用的是聚簇索引,多数索引使用B+树的数据结构来存储索引数据,在主键选择上应尽量使用有序的主键保证写入性能
③ 单调递增:保证下一个ID一定大于上一个ID,新增数据对于索引结构的影响也最小
④ 信息安全:ID信息安全
⑤ 含时间戳:快速了解分布式id的生成时间
1.3 ID号生成系统的可用性要求
① 高可用
② 低延迟
③ 高QPS
二、一般通用方案
2.1 UUID
UUID
:(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:abcd1234-ef56-2356-elid-23fges458965
性能非常高:本地生成,没有网络消耗
缺点:
无序,每一次UUID数据的插入都会对主键的B+树进行很大的修改,容易引起B+树索引的分裂
2.2 数据库自增主键
数据库自增主键
:分布式里,数据库的自增ID机制的主要原理是:数据库自增ID和mysql数据库的replace into实现的(replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主键或唯一索引判断)则先删除,再插入)
缺点:
系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?
每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID里面的延迟低和高QPS的规则(在高并发下,如果都去数据库里面获取id,那是非常影响性能的)
2.3 基于Redis生成全局id策略
基于Redis生成全局id策略
:因为Redis是单线程的,天生保证原子性,可以使用原子操作INCR和INCRBY来实现
集群分布式
在Redis集群情况下,可以设置不同的增长步长,同事key一定要设置有效期
缺点:
配置麻烦,维护Redis集群
2.4 SnowFlake
Twitter的分布式自增ID算法
:
①ID能够按照时间有序生成
②生成id的结果是一个64bit大小的整数,为一个Long型(转换成字符串后长度最多19)
③分布式系统内不会产生ID碰撞(由datacenter和workId作区分)并且效率较高
号段解析:
1bit-不用
因为二进制中最高位是符号位,1表示负数,0表示正数
生成的id一般都是用整数,所以最高位固定为0
41bit-时间戳,毫秒级
-41位可以表示241-1 个数字,
-如果只用来标识正整数(计算机中正数包含0),可以表示的数值范围是:0至241-1,减1是因为可表示的数值范围是从0开始算的,而不是1(可以使用69年,到2039年)
10bit-工作机器id,用来记录工作机器id
-可以部署在210 = 1024个节点,包括5位datacenter和5位workId
-5位(bit)可以表示的最大正整数是25-1 = 31,即可以用0、1、2、3、…31这32个数字,来表示不同的datacenterId和workId
12bit-序列号,用来记录同毫秒内产生的不同id
-12位(bit)可以表示的最大正整数是212-1 = 4095,即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号
三、Hutool工具包代码实现
Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。
引入依赖:
<!--包含雪花算法的Hutool工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.4</version>
</dependency>
Controller层:
/**
* @Author: Ron
* @Create: 2020 10:41
*/
@RestController
public class SnowFlakeController {
@Autowired
private OrderService orderService;
@RequestMapping("/snowflake")
public String index() {
return orderService.getIDBySnowFlake();
}
}
Service层:
/**
* @Author: Ron
* @Create: 2020 10:43
*/
public interface OrderService {
String getIDBySnowFlake();
}
Impl:
/**
* @Author: Ron
* @Create: 2020 10:43
*/
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private IdGeneratorSnowflake idGenerator;
@Override
public String getIDBySnowFlake() {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
threadPool.submit(() -> {
System.out.println(idGenerator.snowflakeId());
});
}
threadPool.shutdown();
return "Hello SnowFlake!!!";
}
}
Mapper层:
/**
* @Author: Ron
* @Create: 2020 10:46
*/
@Repository
@Slf4j
public class IdGeneratorSnowflake {
private long workId = 0;
private long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workId, 1);
@PostConstruct
public void init() {
workId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("当前机器id: {}", workId);
}
public synchronized long snowflakeId() {
return snowflake.nextId();
}
// 范围为0~31
public synchronized long snowflakeId(long workId, long datacenterId) {
Snowflake snowflake = IdUtil.createSnowflake(workId, datacenterId);
return snowflake.nextId();
}
public static void main(String[] args) {
System.out.println(new IdGeneratorSnowflake().snowflakeId());
}
}
启动类:
@SpringBootApplication
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
application.yml配置文件
server:
port: 8888
页面访问:
控制台打印结果:
缺点:
依赖机器时钟,如果机器时钟回拨,会导致重复ID生成
在单机上是递增的,但是由于设计到分布式环境,每台机器的时钟不可能完全同步,有时候会出现不是全局递增的情况(此缺点可以认为无所谓,一般分布式ID只要求趋势递增,并不会严格要求递增,90%的需求都只要求趋势递增)