分库分表如何处理主键ID

雪花算法或UUID--ID生成器的实例分析 雪花算法简介 以往的工作中都是通过自增的方式生成id,今天忽然看到一种生成式的算法-雪花算法。小编查找了一些资料参考了许多篇博客,发现大多关于雪花算法的解读大同小异,所以将看到的内容整合一下,如有错误欢迎大家指出。 雪花算法是一种生成分布式全局唯一ID的经典算法。它会生成64位的二进制: ... 阅读详情

当关系型数据库数据量过大时,通常会采用分库分表降低数据库查表压力。分库分表有多种,有分一个库多张分表额,有分多个库多张表的。一般分库分表使用ShardingSphere分表,建分片键等。但是分库分表之后,主键ID如何处理呢?相同业务表不同分表的主键ID是不可以相同的。所以要考虑一下主键ID如何赋值的问题。
有以下几种我了解或者涉及到过的主键ID的处理方式:

一、自动生成主键ID

这种方式一般会将主键设置为bitint类型,自增的。但是会存在一个问题,多张分表保证主键不冲突,因为在业务上来说,多张分表的数据组成某个业务,因此主键是不允许冲突的。
当采用自动生成主键ID的方案时,可以设置固定的几张分表,每个分表的起点不一样,每次新增的步长一样,这样就可以保证每张分表的主键不冲突。
举例,如某张表分表有10张,可以设置每张表的起始主键ID从1到10,每张分表主键ID递增步长为10。

表名起始主键ID步长
table_1110
table_2210
table_3310
table_4410
table_5510
table_6610
table_7710
table_8810
table_9910
table_101010

根据上面分表主键递增规律,每张表的行数如下递增

表名第一条数据第二条第三条第四条第五条
table_1111213141
table_2212223242
table_3313233343
table_4414243444
table_5515253545
table_6616263646
table_7717273747
table_8818283848
table_9919293949
table_101020304050

按照主键递增格式有弊端,即新增表时,不好处理主键逻辑。这种主键ID递增的方式适用于分表比较固定的情况。

2.UUID做主键

uuid获取方式:

String id = UUID.randomUUID();

结果:
647be5bd-a477-4eff-8e58-99a573bb14ec

在前几年的时候,uuid作为主键的表遍地都是,因为它数据范围之广,用法方便受很多人青睐。但是uuid长度为36位,即使去掉中间的“-”,长度也有32位,因此比较占用存储空间。
因为uuid是无序的,因此新增到数据库时,数据表如果采用btree索引,那么每次新增一条数据都需要重新排序,比较费时间,因此uuid作为分表主键也是不太推荐的。

3.雪花算法

采用SnowFlake算法生成唯一id,包含时间戳,工作中心id,数据中心id,序列号组成,结构如下:
在这里插入图片描述
(1)一位占位符:默认为0。最高位代表正负,1代表负数,0代表正数,默认为正数。
(2)41位时间戳:毫秒级的时间,可以存69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
(3)5位工作中心id:十进制范围在0-31;5位数据中心id:十进制范围在0-31。两个组合在一起最多可以容纳1024个节点。
(4)序列号:占用12bit,最多可以累加到4095。自增值支持同一毫秒内同一个节点可以生成4096个ID,这个值在同一毫秒同一节点上从0开始不断累加。(最大可以支持单节点差不多四百万的并发量)

java一般使用hutool中的IdUtil类生成雪花算法id。以下是代码解析
(1)IdUtil

public class IdUtil {

    public IdUtil() {
    }

    /** @deprecated 创建Snowflake对象,已废弃 */
    @Deprecated
    public static Snowflake createSnowflake(long workerId, long datacenterId) {
        return new Snowflake(workerId, datacenterId);
    }

    /**
     * 根据工作中心id和数据中心id获取数据
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static Snowflake getSnowflake(long workerId, long datacenterId) {
        return (Snowflake) Singleton.get(Snowflake.class, new Object[]{workerId, datacenterId});
    }

    /**
     * 根据工作中心id获取数据
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static Snowflake getSnowflake(long workerId) {
        return (Snowflake) Singleton.get(Snowflake.class, new Object[]{workerId});
    }

    /**
     * 无参获取Snowflake
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static Snowflake getSnowflake() {
        return (Snowflake) Singleton.get(Snowflake.class, new Object[0]);
    }

    /**
     * 根据数据中心id计算算法id中的数据中心值
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static long getDataCenterId(long maxDatacenterId) {
        Assert.isTrue(maxDatacenterId > 0L, "maxDatacenterId must be > 0", new Object[0]);
        //9223372036854775807L转成二进制,由63位的1组成
        if (maxDatacenterId == 9223372036854775807L) {
            --maxDatacenterId;
        }

        long id = 1L;
        byte[] mac = null;

        try {
            mac = NetUtil.getLocalHardwareAddress();
        } catch (UtilException var6) {
        }

        if (null != mac) {
            id = (255L & (long) mac[mac.length - 2] | 65280L & (long) mac[mac.length - 1] << 8) >> 6;
            //取余
            id %= maxDatacenterId + 1L;
        }

        return id;
    }

    /**
     * 获取算法id中的工作中心id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static long getWorkerId(long datacenterId, long maxWorkerId) {
        StringBuilder mpid = new StringBuilder();
        mpid.append(datacenterId);

        try {
            mpid.append(RuntimeUtil.getPid());
        } catch (UtilException var6) {
        }

        return (long) (mpid.toString().hashCode() & '\uffff') % (maxWorkerId + 1L);
    }

    /**
     * 无参获取算法id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static long getSnowflakeNextId() {
        return getSnowflake().nextId();
    }

    /**
     * 无参获取字符串类型算法id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public static String getSnowflakeNextIdStr() {
        return getSnowflake().nextIdStr();
    }
}

(2) Snowflake类(核心类)

public class Snowflake implements Serializable {
    private static final long serialVersionUID = 1L;
    public static long DEFAULT_TWEPOCH = 1288834974657L; //时间戳,二进制为41位,对应时间为2010-11-04 09:42:54
    public static long DEFAULT_TIME_OFFSET = 2000L;  //允许时钟回拨差值,两秒
    private static final long WORKER_ID_BITS = 5L;  //工作中心位数
    private static final long MAX_WORKER_ID = 31L; //最大工作中心id值
    private static final long DATA_CENTER_ID_BITS = 5L; //数据中心位数
    private static final long MAX_DATA_CENTER_ID = 31L; //最大数据中心id值
    private static final long SEQUENCE_BITS = 12L;   //序列化位数
    private static final long WORKER_ID_SHIFT = 12L;  //工作中心移动位数(计算id用)
    private static final long DATA_CENTER_ID_SHIFT = 17L; //工作中心移动位数((计算id用)计算id用)
    private static final long TIMESTAMP_LEFT_SHIFT = 22L; //时间戳移动位数
    private static final long SEQUENCE_MASK = 4095L;   //序列化最大值
    private final long twepoch;
    private final long workerId;
    private final long dataCenterId;
    private final boolean useSystemClock;
    private final long timeOffset;
    private final long randomSequenceLimit;
    private long sequence;
    private long lastTimestamp;

    /**
     * 无参构造
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake() {
        this(cn.hutool.core.util.IdUtil.getWorkerId(cn.hutool.core.util.IdUtil.getDataCenterId(31L), 31L));
    }

    /**
     * 根据工作中心id构造
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(long workerId) {
        this(workerId, IdUtil.getDataCenterId(31L));
    }

    /**
     * 根据工作中心id和数据中心id构造
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(long workerId, long dataCenterId) {
        this(workerId, dataCenterId, false);
    }

    /**
     * 参数:工作中心id、数据中心id、是否采用系统时间
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(long workerId, long dataCenterId, boolean isUseSystemClock) {
        this((Date) null, workerId, dataCenterId, isUseSystemClock);
    }

    /**
     * 参数:时间,工作中心id、数据中心id、是否采用系统时间
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, DEFAULT_TIME_OFFSET);
    }

    /**
     * 参数:工作中心id、数据中心id、是否采用系统时间, 允许时针回拨的时长
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset) {
        this(epochDate, workerId, dataCenterId, isUseSystemClock, timeOffset, 0L);
    }

    /**
     * 参数:工作中心id、数据中心id、是否采用系统时间, 允许时针回拨的时长,序列化
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public Snowflake(Date epochDate, long workerId, long dataCenterId, boolean isUseSystemClock, long timeOffset, long randomSequenceLimit) {
        this.sequence = 0L;
        this.lastTimestamp = -1L;
        this.twepoch = null != epochDate ? epochDate.getTime() : DEFAULT_TWEPOCH;
        this.workerId = Assert.checkBetween(workerId, 0L, 31L);
        this.dataCenterId = Assert.checkBetween(dataCenterId, 0L, 31L);
        this.useSystemClock = isUseSystemClock;
        this.timeOffset = timeOffset;
        this.randomSequenceLimit = Assert.checkBetween(randomSequenceLimit, 0L, 4095L);
    }

    /**
     * 获取工作中心id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public long getWorkerId(long id) {
        return id >> 12 & 31L;
    }

    /**
     * 获取数据中心id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public long getDataCenterId(long id) {
        return id >> 17 & 31L;
    }

    /**
     * 获取时间戳
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public long getGenerateDateTime(long id) {
        return (id >> 22 & 2199023255551L) + this.twepoch;
    }

    /**
     * 获取id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public synchronized long nextId() {
        //获取当前时间戳,默认取项目时间
        long timestamp = this.genTime();
        if (timestamp < this.lastTimestamp) {
            //校验时间回拨差值是否大于配置的差值,若是,则报错
            if (this.lastTimestamp - timestamp >= this.timeOffset) {
                throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{this.lastTimestamp - timestamp}));
            }
            //将上次时间戳赋值给当前时间
            timestamp = this.lastTimestamp;
        }

        //设置序列化号
        if (timestamp == this.lastTimestamp) {
            //设置序列化号,上次序列化号+1之后与4095进行与计算。
            long sequence = this.sequence + 1L & 4095L;
            if (sequence == 0L) {
                // 毫秒内序列溢出(序列化号已满,说明当前秒的序列化号都已被占用过) 阻塞到下一个毫秒,获得新的时间戳
                timestamp = this.tilNextMillis(this.lastTimestamp);
            }
            //记录当前的序列化号
            this.sequence = sequence;
        } else if (this.randomSequenceLimit > 1L) {
            //当timestamp > this.lastTimestamp且this.randomSequenceLimit > 1L,则随便设置一个不大于等于randomSequenceLimit的值赋予sequence
            this.sequence = RandomUtil.randomLong(this.randomSequenceLimit);
        } else {
            //当timestamp > this.lastTimestamp且this.randomSequenceLimit <= 1时,则默认设置sequence=0
            this.sequence = 0L;
        }

        //将计算完的时间戳赋予全局变量
        this.lastTimestamp = timestamp;
        //将时间戳(当前时间戳-默认的时间,这样可以时间戳值的范围更大些),数据中心,工作中心,序列化拼接在一起组成id。
        return timestamp - this.twepoch << 22 | this.dataCenterId << 17 | this.workerId << 12 | this.sequence;
    }

    /**
     * 获取字符串算法id
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    public String nextIdStr() {
        return Long.toString(this.nextId());
    }

    /**
     * 计算下一个时间
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp;
        //计算时间,目的是算出当前时间,最大只能等于传进来的参数时间
        for (timestamp = this.genTime(); timestamp == lastTimestamp; timestamp = this.genTime()) {
        }

        if (timestamp < lastTimestamp) {
            throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", new Object[]{lastTimestamp - timestamp}));
        } else {
            return timestamp;
        }
    }

    /**
     * 生成时间戳
     *
     * @author zhouxy
     * @date 2023/5/12
     */
    private long genTime() {
        //SystemClock.now() 获取当前项目计算的时间
        //System.currentTimeMillis()获取的是当前系统时间
        return this.useSystemClock ? SystemClock.now() : System.currentTimeMillis();
    }
}

上面是hutool中提供的雪花算法,与原始的雪花算法区别在于,hutool中允许一定范围的时间回拨。
hutool中的雪花算法的优缺点:
优点
(1)按照时间排序,则数据库存储时不需要重复排序变动存储位置。
(2)可使用范围长,时间戳的位数41位,可支持69年。
缺点
(1)允许时针回拨,在某些极端情况下会产生重复id

结语:除了上面的几种id生成算法,当然还有其他的主键id生成算法,具体使用哪种需要根据业务的情况来使用。

【Java面试题】分库分表之后,id 主键如何处理? 更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。 阅读详情

相关推荐

分库分表之后,id 主键如何处理

分库分表id处理

没有伞的孩子只能快跑,如若不跑,只有一个结果:超过你的人回头鄙视你 1251

分库分表之后,id 主键如何处理?

因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id来支持。

wupanP的博客 604

8种MySQL分库分表后主键ID生成策略,你选对了吗?

根据具体应用场景的特点,可以考虑将某些业务属性(如用户ID、订单日期等)融入到主键ID的设计当中,以此降低并发冲突的可能性。// 示例:结合时间戳与用户ID生成订单号优点高度定制化,能满足特殊业务需求。减少了纯数字ID带来的单调感。缺点设计复杂度增加。可能会受到业务规则变化的影响。

java专栏 2228

分库分表之后如何设计主键ID(分布式ID)?

分库分表之后如何设计主键ID(分布式ID)?

qq_38723814的博客 1333

GitLab中国区注册终极解决方案:绕过+86手机验证的实战技巧(2023最新)

本文详细解析了GitLab中国区注册时绕过+86手机验证的三种实战技巧,包括开发者工具调试方案、使用国际号码的变通方案以及本地化部署替代方案。通过技术分析和实践验证,帮助开发者顺利注册GitLab账号,提升团队协作效率。

rl6adventurer的博客 1498

akshare 布林通道策略

import datetime import pandas as pd import backtrader as bt import matplotlib.pyplot as plt from datetime import datetime import matplotlib import akshare as ak %matplotlib inline class Boll_strategy(bt.Strategy): #自定义参数,每次买入1800手 params=(('size'.

qq_26742269的博客 5339

分库分表之后,主键ID如何处理?

当关系型数据库数据量过大时,通常会采用分库分表降低数据库查表压力。分库分表有多种,有分一个库多张分表额,有分多个库多张表的。一般分库分表使用ShardingSphere分表,建分片键等。但是分库分表之后,主键ID如何处理呢?相同业务表不同分表的主键ID是不可以相同的,其实这是分库分表之后你必然要面对的一个问题,就是 主键id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。

风中追风 2365

[华为ensp]双核心的园区网

(给部分只会emm......借鉴的朋友提个醒,在group-member e0/0/2 to e0/0/20是所选,所以到了p l t那会给2-20端口都配置上trunk,别紧张,不是你问题)完成上述配置后,PC1可以ping通HTTP2服务器,Client1可以访问HTTP2服务器的HTTP站点,如果不能就要检查上述是否正确。本次实验主要目的是采用双核心的冗余网络,采用MSTP和VRRP技术,在核心层交换机上配置SC1上配置OSPF协议。SC1交换机上配置VRRP。在SC1上配置DHCP。

2301_78859499的博客 1416

分库分表之后,id 主键如何处理?

更重要的是,UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作(连续的 ID 可以产生部分顺序写),还有,由于在写的时候不能产生有顺序的 append 操作,而需要进行 insert 操作,将会读取整个 B+ 树节点到内存,在插入这条记录后会将整个节点写回磁盘,这种操作在记录占用空间比较大的情况下,性能下降明显。这个 snowflake 算法相对来说还是比较靠谱的,所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够你用了。

阿杰同学的博客 766

T265摄像头插着开机后,需要重新插拔才能正常启动

T265摄像头插着开机后,需要重新插拔才能正常启动

qq_35598561的博客 1079

eDP/DP接口简介

目前,在视频传输协议以及接口领域,外部接口主要是DP与HDMI两分天下,当然之前出现过很多接口-VGA, DVI等等,最新平台基本以这两个为主;笔记本内部接口就基本上使用eDP((Embedded Display Port))作为标准接口来驱动液晶屏了。那么问题来了,eDP和DP,是完全兼容的么?eDP,DP,HDMI,这三种接口的显示器,可以插在同一个接口上使用么?这篇文章,就会从硬件和一部分协议的角度分析一下这三大协议。

weixin_50518899的博客 2万+

分库分表之后,id主键怎么处理

问:分库分表之后,id 主键如何处理? 面试官心理分析 其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。 面试题剖析 基于数据库的实现方案 数据库自增 id 这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没...

sinat_27143551的博客 206

【生产实践】PostgreSQL高可用Patroni安装(超详细已踩坑)

一 安装Patroni 0 Patroni 对Python的版本要求 我安装的是Patroni最新版本 :4.0.3,Patroni 对Python 版本有要求,没有具体的版本矩阵,但是根据每个版本发布信息可以得到以下信息 Patroni 4.0 需要 Python 3.8 以上(可以根据下面发布版本信息推理出) Version 3.0.2 Version 3.0.2 dropped support of Python older than 3.6. 版本3.0.2不再支持3.6版本以下的P

DBA之路的博客 1741

(面试题 )分库分表之后,id 主键如何处理?

面试题 分库分表之后,id 主键如何处理? 面试官心理分析 其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。 面试题剖析 基于数据库的实现方案 数据库自增 id 这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入...

海子@幸福人 1065

【AlphaFold2】深入浅出,讲解Evoformer|学习笔记

本篇博客是围绕youtube德国博主Kilian Mandon()的《从头实现AlphaFold2:AlphaFold Decoded》的第五期视频讲解和note-book内容写成的笔记,加上了自己的理解和对相关概念的解释。上期我们讲解了AlphaFold2的特征提前的部分【AlphaFold2】Feature extraction:提取特征,为模型输入做准备|Datapipeline讲解。本期,我们将介绍AlphaFold2中最为庞大且最为重要的部分——Evoformer。

Yaoyao2024的博客 2015

【Proteus仿真】用24C04与1602LCD设计的简易加密电子密码锁

【Proteus仿真】用24C04与1602LCD设计的简易加密电子密码锁

perseverance51博客 1122

[数据库][分库分表]分库分表之后,id主键如何处理

面试题 分库分表之后,id 主键如何处理? 面试官心理分析 其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。 面试题剖析 基于数据库的实现方案 数据库自增 id 这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入...

疯一样的女子 3069

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

精选(71) 面试题:分库分表之后,id 主键如何处理?

面试题 分库分表之后,id 主键如何处理? 面试官心理分析 其实这是分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。所以这都是你实际生产环境中必须考虑的问题。 面试题剖析 基于数据库的实现方案 数据库自增 id 这个就是说你的系统里每次得到一个 id,都是往一个库的一个表里插入一条没什么业务...

cowbin2012的专栏 1万+

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

上一篇: 上传文件报: /tmp/undertow的解决办法
下一篇: 11-@Transaction与AOP冲突解决
CAT_cwds
博客等级 码龄9年 106粉丝 124原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值