公司网站制作与维护,学校网站建设设计方案,网页制作模板和库的联系与区别,公司网站怎么建背景
在复杂分布式系统中#xff0c;往往需要对大量的数据和消息进行唯一标识。随着数据日渐增长#xff0c;对数据分库分表后也需要有一个唯一ID来标识一条数据或消息#xff0c;数据库的自增 ID 显然不能满足需求#xff1b;此时一个能够生成全局唯一 ID 的系统是非常必…背景
在复杂分布式系统中往往需要对大量的数据和消息进行唯一标识。随着数据日渐增长对数据分库分表后也需要有一个唯一ID来标识一条数据或消息数据库的自增 ID 显然不能满足需求此时一个能够生成全局唯一 ID 的系统是非常必要的。
分布式 ID 需要满足以下条件
全局唯一不能出现重复的 ID 号既然是唯一标识这是最基本的要求。高性能高可用低延时ID 生成速度要快否则反倒会成为业务瓶颈高可用尽量保证服务的可用性多实例化避免因一个实例挂掉影响整个业务应用的运行。容易接入要秉着拿来即用的设计原则在系统设计和实现上要尽可能的简单避免增加开发人员的使用成本。趋势递增 最好趋势递增这样方便进行数据排序、过滤当然这个要求还需要根据具体的业务场景作出安排。信息安全如果 ID 是连续递增的恶意用户就可以很容易的推测出订单号的规则从而猜出下一个订单号如果是竞争对手就可以直接知道我们一天的订单量。所以在某些场景下需要 ID 无规则来保证安全性。
方案一UUID
public class UUIDTest {public static void main(String[] args) {// 生成 UUIDString uuid UUID.randomUUID().toString();// 输出 UUID 串System.out.println(uuid);}
}优点
高性能实现简单本地生产 ID不需要依赖第三方组件
缺点
不是趋势递增不方便排序生成的 ID 只能用字符串类型存储占用空间大
方案二基于数据库单机自增 ID
数据库自增 ID 是依赖数据库中提供的自动增量功能实现这种生成 ID 的方案比较容易实现与使用。在这种方案中为了存储生成的 ID 值往往我们会单独创建立一张专用于存储生成 ID 的表然后往表中插入数据替换旧数据过程中 ID 会递增我们只需要查询该递增的 ID 值然后再与时间戳、随机值等元素进行组合处理生成分布式 ID。 如下图比方案一多一次 Mysql 请求。 用于存储 ID 的表结构如下
idstub1a2b
id自增生成的 ID 值stub用于记录 ID 是归属的业务
CREATE TABLE myid ( id bigint(20) unsigned NOT NULL auto_increment, stub char(1) NOT NULL default , PRIMARY KEY (id))每当应用程序需要一个 ID 时就插入一条数据使其自增 ID然后替换旧的数据最后读取新生成的 ID 值
begin;
REPLACE INTO myid(stub) VALUES (a);SELECT LAST_INSERT_ID();
commit;等取到 ID 值后我们在让其与时间戳、“随机值”、业务码等组合生成与业务挂钩的分布式 ID 串一般时候我们生成的串都不会超过 64 位以方便用 long 类型存储该串
优点
实现简单生成的 ID有序递增数值类型便于创建索引查询快。
缺点
性能较差只能应用于并发量小的业务需求。存在单点问题如果数据库不可用将导致依赖它的服务不能正常执行。
方案三基于数据库集群模式自增生成 ID
前面讲述了单机数据库方式通过自增方式生成 ID这种方式由于单机部署不论是性能还是可用性都无法得到保障。故而往往都不会直接采用该方案而是对其进行改动将其改为使用多主的集群模式部署利用多个数据库来进行自增生成 ID。
使用多台数据库会导致每个数据库的 ID 都是从 1 开始递增且递增步长为 1在这种情况下一定会生成重复的 ID 值。解决这种 ID 重复生成的问题也很简单只需要对每个数据库都提前配置好其初始值auto_increment_increment以数据库个数充当自增长步长auto_increment_offset这样每个库中增长的 ID 就不会重复了。 优点
高可用趋势递增
缺点
性能一般只能并发量小的业务需求水平扩展比较麻烦需要手动调整集群数据库中的初始值与步长。
方案四基于数据库的号段模式
号段模式一般也是基于数据库自增实现分布式 ID 的一种方式是当下分布式 ID 生成方式中比较流行的一种其使用可以简单理解为每次从数据库中获取生成的 ID 号段范围将范围数据获取到应用本地后在范围内循递增生成一批 ID然后将这批数据存入缓存。
每次应用需要获取 ID 时这时就候就可以从缓存中读取 ID 数据当缓存中的 ID 消耗到一定数目时候这时再去从数据库中读取一个号段范围再执行生成一批 ID 操作存入缓存这是一个重复循环的过程这样重复操作每次都只是从数据库中获取待生成的 ID 号段范围而不是一次次获取数据库中生成的递增 ID这样减少对数据库的访问次数大大提高了 ID 的生成效率。 在使用号码模式时我们通常会先建立一张表用于记录上述的 ID 号段范围。
CREATE TABLE myid (id int(10) NOT NULL AUTO_INCREMENT, max_id bigint(20) NOT NULL, step int(20) NOT NULL, biz_type int(20) NOT NULL, version int(20) NOT NULL, PRIMARY KEY (id)) max_id当前最大可用的 ID。step号段的步长。bit_type业务类型version记录更新的版本号主要作用是乐观锁每次更新时都会更新该值以保证并发时数据的正确性。
分布在不同服务上的相同业务对应一条数据库记录任务执行时去数据区去抢 ID 号段并更新号段供下个服务使用。不同业务数据独立可以 ID 重复。
每次从数据库中获取号段 ID 的范围时都会执行更新语句其中计算新号段范围最大值 max_id 的公式 max_id max_id step 组成。
begin;
UPDATE myid
SET max_id max_id step,
version version 1
WHERE version {执行更新的版本号} AND biz_type {业务类型};
SELECT max_id, step, version FROM myid WHERE biz_type {业务类型};
commit;过程描述
例如某个业务需要批量获取 ID首先它往数据库 myid 中插入一条初始化值设置 max_id 0 和步长 step 1000 及使用该 ID 的业务标识 biz_type test 与版本 version 0如下
INSERT INTO myid(max_id,step,biz_type,version) VALUES(0,1000,test,0);idmax_idstepbiz_typeversion101000test0
然后以 biz_type 作为筛选条件从数据库 myid 中读取 max_id 与 step 的值:
max_id0step1000
通过这两个值可以知道号段范围为 (0, 1000 ]生成该批 ID 存入缓存中那么这是缓存大小为1000
每次都从缓存中取值创建一个监听器用于监听缓存中 ID 消耗比例设置阈值判断如果取值超过的阈值后就进行数据库号段更新操作跟上面第一次执行更新时候一样也是执行下面的更新 SQL 语句。
然后执行获取分布式 ID 的方法方法中应执行下面语句进行号段更新方便生成新的一批号段
begin;
UPDATE myid
SET max_id max_id step, version version 1 WHERE version 0 AND biz_type test;
SELECT max_id, step, version
FROM myid
WHERE biz_type test;
commit;这时候数据库中的值为
idmax_idstepbiz_typeversion110001000test1
比如设置阈值为 50%当缓存中存在 1000 个 ID监听器监听到业务应用已经消耗到 500 个后超过阈值创建一个新的线程去执行上面的更新 SQL 语句让数据库中号段范围按照设置的 step 扩大然后获取新的号段最大值应用中再生成一批范围为 (1001,2000] 范围的 ID 存入缓存供应用使用这时候缓存中数据大小为2000已经使用了 500可用 1500
过程是个循环的过程每到消耗到一定数据后就会生成新的一批。这里只是对其进行了简单介绍很多时候为了保证数据库可用性都会采用集群模式现在通过号码模式生成 ID 的开源框架有很多比如
美团开源的 Leaf滴滴开源的 TinyId
优点
趋势递增使用缓存机制容灾性高即使数据库不可用还能撑一段时间可以自定义每次扩展的大小控制 ID 生成速度可以设置生成 ID 的初始范围方便业务从原有的 ID 方式上迁移过来。
缺点
数据库宕机会造成整个系统不可用ID 号码不够随机有可能泄露发号数量的信息不太安全。
所以采用这种方案我们也经常使用数据库多主模式保证数据库的高可用性。
方案五基于 Redis 单节点实现分布式 ID
Redis 中存在原子操作指令 INCR 或 INCRBY执行后可用于创建初始化值或者在原有数字基础上增加指定数字并返回执行 INCR 命令之后 key 的值这样就可以很方便的创建有序递增的 ID。
优点
实现简单有序递增方便排序
缺点
强依赖于 redis可能存在单点问题如果 Redis 超时可能会对业务操作影响Redis 持久化有可能会丢数据。如果丢失数据有可能出现重复 ID 。
方案六使用 Redis 集群实现分布式 ID
使用 Redis 单机生成 ID 存在性能瓶颈无法满足高并发的业务需求且一旦 Redis 崩溃或者服务器宕机那么将导致整个基于它的服务不可用这是业务中难以忍受的所以一般时候会用集群的方式来实现 Redis 的分布式 ID 方案。
此方案的思想与方案三基于数据库集群模式自增生成 ID 的思想是一致的通过初始值和步长来控制不同 Redis 的ID 生成确保不生成重复 ID。
使用集群的方式需要设置提前设置 初始值 和 步长 来保证每个节点增加的 ID 不会冲突正常做法每个节点都配置一个跟节点挂钩的 Lua 脚本脚本内容中设置好对应节点的 初始值 和 步长其中初始值是按照节点个数从 1 开始递增分配而步长则是等于集群中 Master 节点的个数。按照这种方式生成 ID 并获取后后面的执行逻辑跟单节点 Redis 一样都是对 ID 进行加工处理操作。 优点
集群模式高可用趋势递增方便分类、排序
缺点
如果 Redis 超时可能会对业务操作影响存在网络开销集群模式需要数据同步对性能有影响。集群规模固定后改动规则影响很大所以扩展比较困难。Redis 持久化有可能会丢数据。如果丢失数据有可能出现重复 ID 。
方案七基于雪花算法模式
今天的主角雪花算法它是 Twitter 开源的由 64 位整数组成分布式 ID性能较高并且在单机上递增。
如下图
第一位 占用1bit由于 long 基本类型在 Java 中是带符号的整数为 0 负数为 1一般生成的 ID 都为正数所以固定为0;时间戳 占用41bit时间戳不是存储当前时间的时间戳而是存储时间的差值当前时间-固定的开始时间这里的的开始时间戳为我们的ID生成器开始使用的时间。通过计算1L 41) / (1000L * 60 * 60 * 24 * 365得出69总共可以容纳约69年的时间。工作机器id 占用10bit其中高位5bit是数据中心ID低位5bit是工作节点ID做多可以容纳1024个节点。序列号 占用12bit在同一机器同一毫秒内可生成不同的序列号12 位支持最多能生成 4096 个。
SnowFlake 算法在同一毫秒内最多可以生成多少个全局唯一ID呢 同一毫秒的ID数量 1024 X 4096 4194304 雪花算法效率很高理论上其生成 ID 的 QPS 约为 409.6w/s这种分配方式可以保证在任何一个机房的任何一台机器在任意毫秒内生成的 ID 都是不同的。
Snowflake 算法的扩展位
在实际使用过程中我们往往都会根据具体的业务对雪花算法的组成进行改动常改动的是 10bit 的 WorkerID 位置该位置由5位数据中心标识与5位机器标识共同组成那么这时候可以 如果部署的服务都在同一个数据中心即不考虑数据中心概念可以将 5bit 数据中心为替换成我们的业务编码。 如果数据中心不是很多这时候可以将5bit数据中心位拆成3bit2bit其中3bit为数据中心标识2bit为业务编码可以设置该值为随机值放置别人猜测 ID 号。
还有很多拆分方法这里省略请大家根据业务需求进行拆分
Snowflake 算法的不足点
根据上面介绍已经对雪花算法有了大概的了解不过雪花算法中部分由时间戳组成所以其强依赖机器时钟如果机器上时钟回拨会导致发号重复或者服务会处于不可用状态。
为了解决这个问题网上给出了很多方案
关闭时钟同步 将 ID 生成交给少量服务器并关闭时钟同步抛出异常 直接抛出异常交给上层业务处理。短时间等待 如果回拨时间较短在耗时要求内比如 5ms那么可以让时钟等待一小段时间时间到达后再次进行判断如果已经超过回拨前的时间则正常执行逻辑否则接着抛出异常。使用扩展位预防时钟回拨 如果回拨时间很长那么无法等待可以调整算法占用的64位将 1~2位作为回拨位一旦时钟回拨将回拨位1可得到不一样的ID2位回拨位允许标记三次时钟回拨基本够使用。如果超出了再选择抛出异常。
其中比较推荐的就是使用上面介绍的雪花算法扩展位如利用 WorkerID 作为扩展位可以让这 10bit 预留出 2bit让其作为回滚的标识当发生时钟回拨时候使其值 1由于是 2bit 预留位所以支持最多三次回拨一般来说够用毕竟时钟回拨几率比较小当然如果还发生了且超过三次后只能抛出进行处理了。
Snowflake 的 Java 实现示例
这里提供两种方式在 Java 中使用 Snowflake 生成分布式 ID第一种是使用现成封装好的工具 Hutool其对 Snowflake 进行了封装可以直接使用。另一种是自己写代码实现 Snowflake这种方式可以灵活配置其中的位数分配。
方式一使用 Hutool 工具封装的 Snowflake 工具
通过 Maven 引入 Hutool 工具包
dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.4.2/version
/dependency使用 Hutool 中提供的 Snowflake 工具
public class SnowflakeHutool { public static void main(String[] args) { // 实例化生成 ID 工具对象 Snowflake snowflake IdUtil.getSnowflake(1, 3); long id snowflake.nextId(); }
}方式二自己写代码实现 Snowflake 生成 ID 工具
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;/*** 手动实现 Snowflake 生成 ID 逻辑*/
public class Snowflake {/*** 机器id5位*/private final long machineId;/*** 数据中心id5位*/private final long datacenterId;/*** 序列号12位*/private long sequence 0L;/*** 初始时间戳*/private final long INIT_TIMESTAMP 1288834974657L;/*** 机器id位数*/private final long MAX_MACHINE_ID_BITS 5L;/*** 数据中心id位数*/private final long DATACENTER_ID_BITS 5L;/*** 机器id最大值*/private final long MAX_MACHINE_Id -1L ^ (-1L MAX_MACHINE_ID_BITS);/*** 数据中心id最大值*/private final long MAX_DATACENTER_ID -1L ^ (-1L DATACENTER_ID_BITS);/*** 序列号id最大值*/private final long SEQUENCE_BITS 12L;/*** 序列号最大值*/private final long sequenceMask -1L ^ (-1L SEQUENCE_BITS);/*** workerid需要左移的位数12位*/private final long WORKER_ID_SHIFT SEQUENCE_BITS;/*** 数据id需要左移位数(12序列号)(5机器id)共17位*/private final long DATACENTER_ID_SHIFT SEQUENCE_BITS MAX_MACHINE_ID_BITS;/*** 时间戳需要左移位数(12序列号)(5机器id)(5数据中心id)共22位*/private final long TIMESTAMP_LEFT_SHIFT SEQUENCE_BITS MAX_MACHINE_ID_BITS DATACENTER_ID_BITS;/*** 上次时间戳初始值为负数*/private long lastTimestamp -1L;/*** 构造方法进行初始化检测** param machineId 机器ID* param datacenterId 数据ID*/public Snowflake(long machineId, long datacenterId) {// 检查数(机器ID)是否大于5或者小于0if (machineId MAX_MACHINE_Id || machineId 0) {throw new IllegalArgumentException(String.format(机器id不能大于 %d 或者小于 0, MAX_MACHINE_Id));} // 检查数(据中心ID)是否大于5或者小于0if (datacenterId MAX_DATACENTER_ID || datacenterId 0) {throw new IllegalArgumentException(String.format(数据中心id不能大于 %d 或者小于 0, MAX_DATACENTER_ID));}// 配置参数this.machineId machineId;this.datacenterId datacenterId;}/*** 获取下一个生成的分布式 ID** return 分布式 ID*/public synchronized long nextId() {// 获取当前时间戳long currentTimestamp timeGen();//获取当前时间戳如果小于上次时间戳则表示时间戳获取出现异常if (currentTimestamp lastTimestamp) {// 等待 10ms如果时间回拨时间短能在 10ms 内恢复则正常生产 ID否则抛出异常long offset lastTimestamp - currentTimestamp;if (offset 10) {try {wait(offset 1);if (currentTimestamp lastTimestamp) {throw new RuntimeException(系统时间被回调无法生成ID);}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException(系统时间被回调无法生成ID且等待中断);}}}// 判断当前时间戳是否等于上次生成ID的时间戳同1ms内是则进行序列号递增1如果递增到设置的最大值默认4096则等待if (lastTimestamp currentTimestamp) {sequence (sequence 1) sequenceMask;if (sequence 0) {currentTimestamp tilNextMillis(lastTimestamp);}}// 如果当前时间戳大于上次生成ID的时间戳说明已经进入下一毫秒则设置序列化ID为0else {sequence 0;}// 设置最后时间戳为当前时间戳lastTimestamp currentTimestamp;// 生成 ID 并返回结果// (currStamp - INIT_TIMESTAMP) TIMESTAMP_LEFT_SHIFT 时间戳部分// datacenterId DATACENTER_ID_SHIFT 数据中心部分// machineId WORKER_ID_SHIFT 机器标识部分// sequence 序列号部分return ((currentTimestamp - INIT_TIMESTAMP) TIMESTAMP_LEFT_SHIFT) | (datacenterId DATACENTER_ID_SHIFT) | (machineId WORKER_ID_SHIFT) | sequence;}/*** 当某一毫秒时间内产生的ID数超过最大值则进入等待* 循环判断当前时间戳是否已经变更到下一毫秒* 是则返回最新的时间戳** param lastTimestamp 待比较的时间戳* return 当前时间戳*/private long tilNextMillis(long lastTimestamp) {long timestamp timeGen();while (timestamp lastTimestamp) {timestamp timeGen();}return timestamp;}/*** 获取系统当前时间** return 系统当前时间毫秒*/private long timeGen() {return System.currentTimeMillis();}/*** 测试 main 方法*/public static void main(String[] args) {// 实例化生成 ID 工具对象Snowflake worker new Snowflake(1, 3);// 创建用于存储 id 的集合ListLong idList new ArrayList();SetLong idSet new HashSet();// 标记开始时间long start System.currentTimeMillis();// 设置 1000ms 内循环生成 IDwhile (System.currentTimeMillis() - start 1000) {// 生成 ID 加入集合long id worker.nextId();idList.add(id);idSet.add(id);}// 输出1s内生成ID的数量System.out.println(生成 ID 总数量 idList.size() , idSet.size());}
}优点
高性能趋势递增可以灵活调整结构不依赖第三方组件
缺点
ID 长度比较大大概 18位ID 不连续生成 ID 是无规则的强依赖时钟如果机器时钟回拨就可能会导致 id 生成重复不同机器的时钟不是完全一致的导致全局的 ID 并不是统一向上自增。
方案八使用 Zookeeper 生成 ID
在 Zookeeper 中主要通过节点数据版本号来生成序列号可以生成 32 位和 64 位的数据版本号客户端可以使用这个版本号来作为唯一的序列号。在 Zookeeper 中本身就是支持集群模式所以能保证高可用性且生成的 ID 为趋势递增且有序不过在实际使用中很少用 Zookeeper 来充当 ID 生成器因为 Zookeeper 中存在强一致性在高并发场景下其性能可能很难满足需求。
不过由于使用 Zookeeper 节点的版本号来充当 ID 号是比较繁琐需要创建节点获取生成的 ID然后去掉节点命令前缀只截取数字部分最后还要异步执行删除节点启动新的线程执行删除节点操作防止占用生成ID线程执行的实际。过程比较耗时且繁琐所以在操作 Zookeeper 时经经常不会采用该方案常使用 Curator 客户端提供的基于乐观锁的计数器来自增实现 ID 生成这个过程和数据库自增生成 ID 类似。
优点
高可用趋势递增
缺点
性能差定期删除之前生成的节点比较繁琐
方案九使用 MongoDB 创建 ObjectID 生成 ID
MongoDB 中如何生成 ID 值
在 MongoDB 中每插入一条数据且没有指定 ID 就会生成一个 _id 键作为唯一标识该键默认是 ObjectID 串常常可以类似于像数据库插入数据一样往 MongoDB 中插入数据获取其默认生成的 ObjectID 值来充当分布式 ID。
MongoDB 的 ObjectId 的组成
在 MongoDB 中默认生成 ObjectId十六进制是有一个 12 bit 组词的 BSON组成类似雪花算法如下图 4 字节时间戳以 Unix 纪元以来的秒数为单位精确到秒5 字节随机数3 字节递增计数器初始化为随机值它能确保相同进程同一秒产生的 ObjectId 也是不一样的。同一秒最多允许每个进程拥有 2563个不同的 ObjectId
Java 中操作 MongoDB 生成 ID 的实现
引入 Maven 包
dependency groupIdorg.mongodb/groupId artifactIdmongo-java-driver/artifactId version3.12.7/version
/dependency然后使用插入一条数据生成 ID
import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import org.bson.Document;
import org.bson.types.ObjectId;public class MongoExample {public static void main(String[] args) {//连接 MongoDB 服务器端口号为 27017MongoClient mongoClient new MongoClient(127.0.0.1, 27017);// 获取数据库如果就创建不存在就创建MongoDatabase dbTest mongoClient.getDatabase(test);// 插入一条文档数据如果就创建不存在就创建Document doc new Document();dbTest.getCollection(myid).insertOne(doc);// 获取 ID 值ObjectId id (ObjectId) doc.get(_id);// 输出 ID 值System.out.println(id);}
}优点
实现简单集群模式易于扩展没有单点问题
缺点
性能一般只能并发量小的业务需求
分布式 ID 开源框架 滴滴 Tinyid 数据库号段模式。 https://github.com/didi/tinyid **美团 Leaf**数据库 Snowflake。 https://github.com/Meituan-Dianping/Leaf **百度 Uid-Generator**数据库号段模式、Zookeeper Snowflake。 https://github.com/baidu/uid-generator
这几张流行的开源分布式 ID 的实现都做了如下操作
减少网络延迟没有使用 Zookeeper、Redis 等作为分布式 ID 的核心组件。可以灵活配置生成的 ID可以在其中添加跟业务挂钩的业务号以满足不同业务需求。大部分考虑的是高可用方案组成统一分布式 ID 分发组件且组成集群规模保证可用性。将生成的 ID 存入缓存这样相当于提前往缓存中存入一批数据能防止并发突增导致 ID 需求大也能防止数据库突然不可用。都会设置一个监控器和异步更新缓存中分布式 ID 的多个线程监控器会监控缓存中的使用比例达到一定比例后会通知更新缓存的线程执行更新分布式 ID 任务这样会再往缓存中放入一批可用的 ID 号段
总结
对部分方案进行了简单测试由于没有精细的配置组件环境和参数所以这里的数据不一定准确只供参考
方案性能ID 生成速度单位s数据库号段模式生成 ID非常高100000000Snowflake 生成 ID很高4000000UUID 生成 ID高710000MongoDB 创建 ObjectID 生成 ID一般1500Redis 的 INCR 或 INCRBY 命令生成 ID一般2000Zookeeper 的节点 ID差600数据库自增生 ID差300
根据上面比较还是比较推荐使用 号段模式 与 Snowflake 两种方案用于生成分布式 ID具体还是得根据业务实际来选择不同方案。