百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

“步步精心”-分布式数据库主键方案

mhr18 2024-10-05 17:25 23 浏览 0 评论

采用 MySQL 实现方式

在只使用单数据库时,使用自增主键ID无疑是最适合的。但在集群、主从架构上时就会有一些问题,比如 : 主键的全局唯一

在集群环境下除自增ID 外的其它创建主键的方案

1> 通过应用程序生成一个 GUID,然后和数据一起插入切分后的集群

优点 : 维护简单,实现也容易

缺点 : 应用的计算成本较大,且 GUID 的长度比较长,占用数据库存储空间较大,涉及到应用的开发

2> 通过独立的应用程序事先在数据库中生成一系列唯一的 ID,各应用程序通过接口或者自己去读取再和数据一起插入到切分后的集群中

优点 : 全局唯一主键简单,维护相对容易

缺点 : 实现复杂,需要应用开发,且 ID表 要频繁查和频繁更新,插入数据时,影响性能

3> 通过 中心数据库服务器 利用数据库自身的自增类型 (如 MySQL 的 auto_increment 字段),或者自增对象 (如 Oracle 的 Sequence) 等先生成一个唯一ID 再和数据一起插入切分后的集群 (不推荐)

优点 : 没有特别明显的优点

缺点 : 实现较为复杂,且整体可用性维系在这个中心数据库服务器上,一旦这里 crash,所有的集群都无法进行插入操作

4> 通过集群编号加集群内的自增 (auto_increment类型) 两个字段 共同组成唯一主键 (虽然是两个字段,但是这方式存储空间最小,仅仅多了一个 smallint 两个字节)

优点 : 实现简单,维护也比较简单,对应用透明

缺点 : 引用关联操作相对比较复杂,需要两个字段,主键占用空间较大,在使用 InnoDB 的时候这一点的副作用很明显

5> 通过设置每个集群中自增 ID 起始点 (auto_increment_offset),将各个集群的 ID 进行绝对的分段来实现全局唯一。当遇到某个集群数据增长过快后,通过命令调整下一个 ID 起始位置跳过可能存在的冲突

优点 : 实现简单,且比较容易根据 ID 大小直接判断出数据处在哪个集群,对应用透明

缺点 : 维护相对较复杂,需要高度关注各个集群 ID 增长状况

6> 通过设置每个集群中自增 ID 起始点 (auto_increment_offset) 以及 ID 自增步长 (auto_increment_increment),让目前每个集群的起始点错开 1,步长选择大于将来基本不可能达到的切分集群数,达到将 ID 相对分段的效果来满足全局唯一的效果 (避免重合需要多种方案结合)

实现 : N台数据库,第一台 mysql 主键从1 开始每次加 N,第二台从2开始每次加 N,以此类推,在获取数据时如果第一台 Server 获取失败,则从第二台 Server 上获取

优点 : 实现简单,后期维护简单,对应用透明

缺点 : 第一次设置相对较为复杂

MySQL 中使用 UUID函数 作为主键带来的问题

对于 InnoDB 这种聚集主键类型的引擎来说,数据会按照主键进行物理排序,这对 auto_increment int 是个好消息,因为后一次插入的主键位置总是在最后。但是对 uuid 来说,这却是个坏消息,因为 uuid 是杂乱无章的,每次插入的主键位置是不确定的,可能在开头,也可能在中间,在进行主键物理排序的时候,势必会造成大量的 IO操作影响效率,因此不适合使用 UUID 做物理主键。比较适合的做法是把 uuid 作为逻辑主键,物理主键依然使用 自增ID

Redis 生成 ID

当使用数据库来生成 ID 性能不够要求的时候,可以尝试使用 Redis 来生成ID。这主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID。可以用 Redis 原子操作 INCR 和 INCRBY 来实现。可以使用 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...

随便负载到哪个机确定好,未来很难做修改。但是 3-5 台服务器基本能够满足器上,都可以获得不同的 ID。但是步长和初始值一定要事先确定,使用Redis集群也可以防止单点故障的问题。另外,比较适合使用 Redis 来生成每天从 0 开始的流水号,比如 订单号=日期+当日自增长号。可以每天在 Redis 中生成一个 Key,使用 INCR进行累加

优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库

2> 数字ID天然排序,对分页或者需要排序的结果很有帮助

缺点 : 1> 如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度

2> 需要编码和配置的工作量比较大

Twitter 的 snowflake算法

snowflake算法 的原理就是用 64位整数来表示主键,其结构如下图 :

1 bit符号位 : 表示正负,方便使用负数标识不正确的ID

41 bit毫秒时间 : 2^41 / (365 * 24 * 3600 * 1000) ≈ 69年

10 bit机房ID + 机器ID : 最大值为1023

12 bit递增序列 : 最大值为4095

/**

* User: mew <p />

* Time: 18/3/6 17:33 <p />

* Version: V1.0 <p />

* Description: Twitter_Snowflake

* SnowFlake的结构如下(每部分用-分开):

* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0

* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值 (当前时间截 - 开始时间截) 得到的值,这里的的开始时间截,一般是 id 生成器开始使用的时间

* 由程序来指定的 (如下下面程序 IdWorker类 的 startTime属性)

* 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

* 10位的数据机器位,可以部署在1024个节点,包括5位 datacenterId 和5位 workerId

* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒 (同一机器,同一时间截) 产生4096个ID序号加起来刚好64位,为一个Long型

* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右 <p />

*/

public class SnowflakeIdWorker {

// ==============================Fields===========================================

/** 开始时间截 (2015-01-01) */

private final long twepoch = 1420041600000L;

/** 机器id所占的位数 */

private final long workerIdBits = 5L;

/** 数据标识id所占的位数 */

private final long datacenterIdBits = 5L;

/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */

private final long maxWorkerId = ~(-1L << workerIdBits);

/** 支持的最大数据标识id,结果是31 */

private final long maxDatacenterId = ~(-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 << 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 当前时间戳

*/

private long tilNextMillis(long lastTimestamp) {

long timestamp = timeGen();

while (timestamp <= lastTimestamp) {

timestamp = timeGen();

}

return timestamp;

}

/**

* 返回以毫秒为单位的当前时间

*

* @return 当前时间(毫秒)

*/

private long timeGen() {

return System.currentTimeMillis();

}

/**

* 测试

*/

public static void main(String[] args) {

SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);

for (int i = 0; i < 1000; i++) {

long id = idWorker.nextId();

System.out.println(Long.toBinaryString(id));

System.out.println(id);

}

}

}

snowflake算法可以根据自身项目的需要进行一定的修改,比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的 bit数

优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库

2> ID按照时间在单机上是递增的

缺点 : 1> 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况

利用 zookeeper生成唯一ID

zookeeper 主要通过其 znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。

很少会使用 zookeeper来生成唯一ID。主要是由于需要依赖 zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想

MongoDB 的 ObjectId

MongoDB 的 ObjectId 和 snowflake 算法类似,它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求,使其在分片环境中要容易生成得多。

相关推荐

SQL入门知识篇(sql入门新手教程视频)

一、什么是数据库?什么是SQL?1、数据库:存放数据,可以很多人一起使用2、关系数据库:多张表+各表之间的关系3、一张表需要包含列、列名、行4、主键:一列(或一组列),其值能够唯一区分表中的每个行。5...

postgresql实现跨库查询-dblink的妙用

技术导语:用惯了oracle的dblink,转战postgresql,会一时摸不着头脑。本期就重点详细讲解postgresql如何安装dblink模块及如何使用dblink实现跨库查询。安装cont...

Oracle VM VirtualBox虚拟机软件(oracle vm virtualbox win10)

OracleVMVirtualBox是一款完全免费的虚拟机软件,下载银行有提供下载,软件支持安装windows、linux等多个操作系统,让用户可以在一台设备上实现多个操作系统的操作。同时软件有着...

开源 SPL 轻松应对 T+0(开源srs)

T+0问题T+0查询是指实时数据查询,数据查询统计时将涉及到最新产生的数据。在数据量不大时,T+0很容易完成,直接基于生产数据库查询就可以了。但是,当数据量积累到一定程度时,在生产库中进行大数据...

中小企业佳选正睿ZI1TS4-4536服务器评测

随着科技的不断发展,各行各业对于数据使用越加频繁,同时针对服务器的选择方面也就越来越多样化和细分化。那么对于我们用户来说,如何选择符合自身业务需求和最优性价比的产品呢?笔者将通过刚刚购买的这台服务器的...

MFC转QT:Qt基础知识(mfc和qt的区别)

1.Qt框架概述Qt的历史和版本Qt是一个跨平台的C++应用程序开发框架,由挪威公司Trolltech(现为QtCompany)于1991年创建。Qt的发展历程:1991年:Qt项目启动1995年...

数据库,QSqlTableModel(数据库有哪些)

QMYSQL——mysqlQSQLITE——sqliteQOICQ——orcale所需头文件.pro增加sql#include<QSqlDatabase>#include<Q...

python通过oledb连接dbf数据库(python连接jdbc)

起因:因为工作需要,需要读取dbf文件和系统数据中数据进行校对,因为知道dbf文件可以用sql查询,所以想能不能像mysql/oracle那样连接,再调用执行sql方法,通过一系列百度,尝试,最终通过...

Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库)

在VBA(VisualBasicforApplications)中使用数据库(如Access、SQLServer、MySQL等)具有以下优点,适用于需要高效数据管理和复杂业务逻辑的场景:1....

Excel常用技能分享与探讨(5-宏与VBA简介 VBA与数据库-二)

以下是常见数据库软件的详细配置步骤,涵盖安装、驱动配置、服务启动及基本设置,确保VBA能够顺利连接:一、MicrosoftAccess适用场景:小型本地数据库,无需独立服务。配置步骤:安装Acces...

Windows Docker 安装(docker安装windows容器)

Docker并非是一个通用的容器工具,它依赖于已存在并运行的Linux内核环境。Docker实质上是在已经运行的Linux下制造了一个隔离的文件环境,因此它执行的效率几乎等同于所部署的L...

Windows下安装Ubuntu虚拟机方法(windows下安装ubuntu20)

在Windows下安装Ubuntu虚拟机。选择使OracleVMVirtualBox安装Ubuntu虚拟机。1.下载和安装OracleVMVirtualBox:访问OracleVMVir...

java入门教程1 - 安装和配置(win和linux)

windows安装和配置安装javahttps://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html目前大部分项目的...

Centos7 安装Tomcat8服务及配置jdk1.8教程

1、下载jdk1.8压缩包下载地址:https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.htmltom...

全网最完整的免费java教程讲义(一)——java配置和安装

一,安装Java1)安装JDK要学习和使用java,首先需要安装JDK(JavaDevelopemntKit),相当于java安装包。Java的下载页在甲骨文官网上:https://www.or...

取消回复欢迎 发表评论: