MySQL主键设计盘点

Posted 三分恶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL主键设计盘点相关的知识,希望对你有一定的参考价值。


@


最近在项目中用了UUID的方式生成主键,一开始只是想把这种UUID的方式生成主键记录下来,在查阅资料的过程中,又有了一些新的认识和思考。


主键定义

唯一标识表中每行的一个列(或一组列)称为主键。主键用来表示一个特定的行。


主键设计和应用原则

除了满足mysql强制实施的规则(主键不可重复;一行中主键不可为空)之外,主键的设计和应用应当还遵守以下公认的原则:

  • 不更新主键列中的值;
  • 不重用主键列的值;
  • 不在主键列中使用可能会更改的值。(例如,如果使用一个
    名字作为主键以标识某个供应商,当该供应商合并和更改其
    名字时,必须更改这个主键。)

主键生成策略

自增ID

使用数据库的自动增长(auto_increment),是比较简单和常见的ID生成方案,数据库内部可以确保生成id的唯一性。

优点:

1、数据库自动编号,速度快,而且是增量增长,聚集型主键按顺序存放,对于检索非常有利。

2、 数字型,占用空间小,易排序,在程序中传递方便。

缺点:

1、不支持水平分片架构,水平分片的设计当中,这种方法显然不能保证全局唯一。

2、对数据库有依赖,每种数据库可能实现不一样,数据库切换时候,涉及到代码的修改,不利于扩展

结论:

自增id做主键适用于非分布式架构。


UUID

UUID:通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息数目的一个128位标识符,还有相关的术语:全局唯一标识符(GUID)。 根据标准方法生成,不依赖中央机构的注册和分配,UUID具有唯一性,这与其他大多数编号方案不同。重复UUID码概率接近零,可以忽略不计。UUID是由一组32位数的16进制数字所构成,标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的32个字符。示例:
550e8400-e29b-41d4-a716-446655440000
到目前为止业界一共有5种方式生成UUID,详情可见IETF发布的UUID规范A Universally Unique IDentifier (UUID) URN Namespace

优点:

性能非常高:本地生成,没有网络消耗。

在这里插入图片描述

缺点:

1、不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。

2、信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

3、ID作为主键时在特定的环境会存在一些问题,比如需要排序的时候——UUID是无序的。

4、MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求。

5、对MySQL索引不利:作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。

关于MySQL 使用自增ID主键和UUID 作为主键的性能比较可以查看参考【8】。

结论:

1、uuid做主键适用于小规模分布式架构用。

2、在使用uuid作为主键的时候,最好设计createtime(创建时间)列和modifytime(修改时间)列以应付可能的排序等场景。


自建的id生成器

Twitter的snowflake算法

Twitter的snowflake算法的核心把时间戳,工作机器id,序列号组合在一起。

在这里插入图片描述
除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。

具体可以查看:https://github.com/twitter-archive/snowflake.git (但是最近一次的提交是6年前,显示已经停止了对初始版snowflake的支持)

源码如下:

package com.yjd.comm.util;/**
 * Created by pc on 2017/8/16 0016.
 */

/**
 * Twitter_Snowflake<br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 */
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 ^ (-1L << workerIdBits);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private final long maxDatacenterId = -1L ^ (-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 ^ (-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 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================

    /**
     * 测试
     */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
        long startime = System.currentTimeMillis();
        for (int i = 0; i < 4000000; i++) {
            long id = idWorker.nextId();
//            System.out.println(Long.toBinaryString(id));
//            System.out.println(id);
        }
        System.out.println(System.currentTimeMillis() - startime);
    }
}


优点:

1、毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
2、 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
3、可以根据自身业务特性分配bit位,非常灵活。

缺点:

强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

结论:

用自建的id生成器做主键适用于大规模分布式架构



参考:

【1】:红心李 :MySQL主键设计
【2】:Uncle Nucky :MySQL数据库主键设计原则
【3】:ellis:设计套路:Mysql主键的选取
【4】:路人甲Java:分布式系统生成唯一id常见方案
【5】:《MySQL必知必会》
【6】:美团技术团队:Leaf——美团点评分布式ID生成系统
【7】:UUID performance in MySQL?
【8】:alex.shu:MySQL 使用自增ID主键和UUID 作为主键的优劣比较详细过程(从百万到千万表记录测试)
【9】:咖啡拿铁:如果再有人问你分布式ID,这篇文章丢给他
【10】:漫漫路:Twitter-Snowflake,64位自增ID算法详解

以上是关于MySQL主键设计盘点的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# MySQL的普通索引和唯一索引到底什么区别?

#yyds干货盘点#MySQL学习-索引的基础篇(下)

#yyds干货盘点# 图解MySQL索引下推

mysql表设计的一些面试题

mysql表设计的一些面试题

MySQL调优--07---淘宝数据库,主键如何设计的?