个人博客
欢迎访问个人博客: https://www.crystalblog.xyz/
备用地址: https://wang-qz.gitee.io/crystal-blog/
1. Zookeeper如何实现分布式锁?
2. 说说你如何用redis实现分布式锁?
3. 集群高并发情况下如何保证生成分布式全局ID的唯一性?
3.1 为什么需要分布式全局唯一ID以及分布式ID的业务需求
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识. 例如:
美团点评的金融, 支付, 餐饮, 酒店.
猫眼电影等产品的系统中数据日渐增长, 对数据分库分表后需要有一个唯一ID来标识一条数据或消息.
此时能够生成全局唯一ID的实现非常必要.
3.2 ID生成规则部分硬性要求
-
全局唯一性: 不能出现重复的ID号;
-
趋势递增: 在Mysql的InnoDB引擎中使用的是聚集索引, 由于多数RDBMS使用BTree的数据结构来存储索引数据, 在主键的选择上面我们应该尽量使用有序的主键保证写入性能.
-
单调递增: 保证下一个ID一定大于上一个ID, 例如事务版本号等.
-
信息安全: 如果ID是连续的, 恶意用户的爬取工作就非常容易了, 直接按照顺序下载指定URL即可; 如果是订单号就更危险了, 竞争对手可以直接知道我们后台真实的业务量. 所以, 在一些场景下的ID需要是不规则的.
-
含时间戳: 含有时间信息,方便排查.
3.3 ID号生成系统的可用性要求
- 高可用: 发一个获取分布式ID的请求, 服务器就要保证99.999%的情况下给我们创建一个唯一的分布式ID.
- 低延迟: 发一个获取分布式ID的请求, 服务要快.
- 高QPS: 满足高并发场景的分布式ID生成.
3.4 一般通用方案
3.4.1 UUID
UUID(Universally Unique Identifier)的标准型式包含32个十六进制数字, 以连字号分为5段, 性能非常高, 本地生成, 没有网络消耗.
例如: 52095f3f-6002-40dd-ba35-28998514f582
@Test
public void IdTest() {
UUID uuid = UUID.randomUUID();
System.out.println(uuid);
}
但是问题来了, 无序的UUID会导致入库的性能变差. 为什么呢?
-
无序, 导致无法预测它的生成顺序, 不能生成递增有序的数字.
首先分布式ID一般都会作为主键, 但MySql官方推荐主键尽量越短越好, UUD每一个都很长, 所以不推荐.
-
主键, ID作为主键在特定的环境会存在一些问题.
比如做DB主键的场景下, UUID就非常不适用, Mysql官方有明确的建议, 主键尽量越短越好, 36个字符长度的UUID不符合要求.
-
索引,B+树索引的分裂
既然分布式ID是主键, 主键是包含索引的, Mysql的索引是通过B+树来实现的, 每一次新的UUID数据的插入, 为了查询效率的优化, 都会对索引底层的B+树进行修改, 因为UUID是无序的, 所以每一次UUID数据的插入都会对主键底层的B+树进行很大的修改, 插入完全无序, 不但会导致一些中间节点产生分裂, 也会创造出很多不饱和的节点, 大大降低了数据库插入的性能.
3.4.2 数据库自增主键(小公司够用,大厂不行)
-
单机
在分布式里面, 数据库的自增ID机制的主要原理是: 数据库自增ID和mysql数据库的replace into实现的.
replace into含义: 插入一条记录, 如果表中唯一索引的值遇到冲突, 则替换老(旧)数据.
relace into和insert类似, 不同点在于, replace into首先尝试插入数据, 如果发现表中已经有了此行数据(根据主键或唯一索引判断)则先删除, 再插入; 否则直接插入新数据.
create table t_test( id bigint(20) unsigned not null auto_increment primary key, stub char(1) not null default '', unique key uk_stub(stub) ); select * from t_test; replace into t_test(stub) values('b'); select last_insert_id();
-
集群分布式
数据库自增ID机制不适合用作分布式ID.
-
系统水平扩展比较困难, 比如定义好了步长和机器台数还好说, 如果要添加机器怎么办? 假设现在只有一台机器生成ID号是1,2,3,4,5(步长是1), 此时需要扩容一台机器, 可以把第二台机器的初始值设置的比第一台机器超过很多, 貌似可以接收. 如果扩容100台机器呢? 水平扩展复杂度很高, 难以实现.
-
如果在高并发下, 都去数据库获取ID, 数据库压力很大, 每次获取ID都要读写一次数据库, 非常影响性能, 不符合分布式ID里面的低延迟和高QPS要求.
-
3.4.3 基于Redis生成全局ID策略
因为Redis是单线程的, 保证了原子性. 通过incr和incrby来实现.
集群分布式
注意: 在Redis集群情况下, 和Mysql一样需要设置不同的增长步长, 同时key一定要设置有效期.
可以使用Redis集群来获取更高的吞吐量.
假如一个集群中有5台Redis, 可以初始化每台Redis的值, 分别1,2,3,4,5, 步长设置为5. 各台Redis生成的ID为:
A: 1,6,11,16,21
B: 2,7,12,17,22
C: 3,8,13,18,23
D: 4,9,14,19,24
E: 5,10,15,20,25
Redis方案可以落地实现, 但是为了生成一个全局ID要搞多台Redis集群建设, 浪费资源. 且如果遇到某台机器宕机, 会存在ID不连续的问题.
3.5 终极方案(雪花算法)
Twitter发布的分布式自增ID算法 snowflake.
3.5.1 概述
最初Twitter把存储系统从Mysql迁移到Cassandra(Facebook开发的一套开源分布式NoSql数据库系统), 因为Cassandra没有顺序ID生成机制, 所以自行开发了一套全局唯一ID生成算法.
Twitter的分布式雪花算法snowflake, 经测试每秒能够产生26万个自增可排序的ID.
- Twitter的snowflake生成ID能够按照时间有序生成.
- snowflake算法生成ID的结果是一个64bit大小的整数, 为一个Long型, 转换成字符串后长度最多19.
- 由datacenterId和workerId作区分,分布式系统内不会产生ID碰撞, 并且效率高.
分布式系统中,有一些需要使用全局唯一ID的场景, 生成ID的基本要求:
- 在分布式的环境下必须全局且唯一.
- 一般都需要单调递增, 因为一般唯一ID都会存到数据库, 而InnoDB的特性就是讲内容存储在主键索引树上的叶子节点, 而且是从左往右递增的, 所以考虑到数据库性能, 一般生成的ID也最好是单调递增的. 为了防止ID冲突可以使用36位的UUID, 但是UUID有一些缺点, 首先它相对比较长, 另外UUID一般是无序的.
- 分布式唯一ID还需要是无规则的, 防止竞争对手攻击.
3.5.2 结构
雪花算法的核心组成:
在Java中64bit的整数是long类型,所以在snowflake算法生成的ID就是long类型来存储的.
号段解析:
- 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个)/(365*24*60*60*1000ms)=69年
- 10bit位, 工作机器ID, 用来记录工作机器ID.
- 可以并部署在2^{10}=1024个节点, 0,1,2,3,…1023共1024个数字, 包括5位datacenterId和5位workId.
- 5位(bit)可以表示的最大正整数是2^{5}-1=31, 即可以用0,1,2,3,…31共32个数字, 来表示不同的datacenterId和workId.
- 12bit位, 序列号, 用来记录同毫秒内产生的不同ID.
- 12位(bit)可以表示的最大正整数是2^{12}-1=4095, 即可以用0,1,2,3,…,4095共4096个数字, 来表示同一机器同一时间戳(毫秒)内产生的4095个ID序号.
SnowFlake可以保证:
所有生成的ID按时间趋势递增.
整个分布式系统内不会产生重复ID, 因为有datacenterId和workId来做区分.
3.5.2 落地实现
hutool工具包已经提供雪花算法ID生成的工具类.
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.13</version>
</dependency>
package com.atguigu.springcloud.snowflake;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* 类描述:雪花算法ID生成器
*/
@Component
@Slf4j
public class IdGeneratorSnowflake {
/**
* 0-31
*/
private long workId = 0;
/**
* 0-31
*/
private long datacenterId = 1;
private Snowflake snowflake = IdUtil.createSnowflake(workId, datacenterId);
@PostConstruct
public void init() {
try {
workId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("当前机器的workId: {}", workId);
} catch (Exception e) {
e.printStackTrace();
log.warn("当前机器的workId获取失败", e);
workId = NetUtil.getLocalhostStr().hashCode();
}
}
public synchronized long snowflakeId() {
return snowflake.nextId();
}
public synchronized long snowflakeId(long workId, long datacenterId) {
snowflake = IdUtil.createSnowflake(workId, datacenterId);
return snowflake.nextId();
}
public static void main(String[] args) {
long id = new IdGeneratorSnowflake().snowflakeId();
System.out.println(id);
}
}
3.5.3 优缺点
优点:
毫秒数在高位, 自增序列在低位, 整个ID都是趋势递增的.
不依赖数据库等第三方系统, 以服务的方式部署, 稳定性更高, 生成ID的性能也非常高.
可以根据自身业务特性分配bit位, 非常灵活.
缺点:
依赖机器时钟, 如果机器时钟回拨, 会导致重复ID生成.
在单机上是递增的, 但是由于涉及到分布式环境, 每台机器上的时钟不可能完全同步, 有时候会出现不是全局递增的情况, 此缺点可以认为无所谓, 一般分布式ID只要求趋势递增, 并不会严格要求递增, 90%的需求都只要求趋势递增.
3.5.4 补充
针对雪花算法的时钟回拨问题, 业界也有解决方案.
- 百度开源的分布式唯一ID生成器UidGenerator
- 美团点评分布式ID生成系统-Leaf