分库分表如何处理主键ID

当关系型数据库数据量过大时,通常会采用分库分表降低数据库查表压力。分库分表有多种,有分一个库多张分表额,有分多个库多张表的。一般分库分表使用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生成算法,具体使用哪种需要根据业务的情况来使用。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值