MySQL主键设计

在项目过程当中遇到一个看似极为基础的问题,可是在深刻思考后仍是引出了很多问题,以为有必要把这一学习过程进行记录。mysql

MySQL主键设计原则

  • MySQL主键应当是对用户没有意义的。
  • MySQL主键应该是单列的,以便提升链接和筛选操做的效率
  • 永远也不要更新MySQL主键
  • MySQL主键不该包含动态变化的数据,如时间戳、建立时间列、修改时间列等
  • MySQL主键应当有计算机自动生成。

主键设计的经常使用方案

自增ID

优势spring

一、数据库自动编号,速度快,并且是增量增加,汇集型主键按顺序存放,对于检索很是有利。sql

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

缺点
一、不支持水平分片架构,水平分片的设计当中,这种方法显然不能保证全局惟一。
二、表锁缓存

在MySQL5.1.22以前,InnoDB自增值是经过其自己的自增加计数器来获取值,该实现方式是经过表锁机制来完成的(AUTO-INC LOCKING)。锁不是在每次事务完成后释放,而是在完成对自增加值插入的SQL语句后释放,要等待其释放才能进行后续操做。好比说当表里有一个auto_increment字段的时候,innoDB会在内存里保存一个计数器用来记录auto_increment的值,当插入一个新行数据时,就会用一个表锁来锁住这个计数器,直到插入结束。若是大量的并发插入,表锁会引发SQL堵塞。
在5.1.22以后,InnoDB为了解决自增主键锁表的问题,引入了参数innodb_autoinc_lock_mode:安全

  • 0:经过表锁的方式进行,也就是全部类型的insert都用AUTO-inc locking(表锁机制)。
  • 1:默认值,对于simple insert 自增加值的产生使用互斥量对内存中的计数器进行累加操做,对于bulk insert 则仍是使用表锁的方式进行。
  • 2:对全部的insert-like 自增加值的产生使用互斥量机制完成,性能最高,并发插入可能致使自增值不连续,可能会致使Statement 的 Replication 出现不一致,使用该模式,须要用 Row Replication的模式。

三、自增主键不连续服务器

Create Table: CREATE TABLE `tmp_auto_inc` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `talkid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=gbk
1 row in set (0.00 sec)

当插入10条记录的时候,由于AUTO_INCREMENT=16,因此下次再插入的时候,主键就会不连续。架构

UUID

优势
一、全局惟一性、安全性、可移植性。并发

二、可以保证独立性,程序能够在不一样的数据库间迁移,效果不受影响。

三、保证生成的ID不只是表独立的,并且是库独立的,在你切分数据库的时候尤其重要

缺点
一、针对InnoDB引擎会徒增IO压力,InnoDB为汇集主键类型的引擎,数据会按照主键进行排序,因为UUID的无序性,InnoDB会产生巨大的IO压力。InnoDB主键索引和数据存储位置相关(簇类索引),uuid 主键可能会引发数据位置频繁变更,严重影响性能。
二、UUID长度过长,一个UUID占用128个比特(16个字节)。主键索引KeyLength长度过大,而影响可以基于内存的索引记录数量,进而影响基于内存的索引命中率,而基于硬盘进行索引查询性能不好。严重影响数据库服务器总体的性能表现。

自定义序列表

所谓自定义序列表,就是在库中建一张用于生成序列的表来存储序列信息,序列生成的策略经过程序层面来实现。以下所示,构建一张序列表:

CREATE TABLE `sequence` (
    `name` varchar(50) NOT NULL,
    `id` bigint(20) unsigned NOT NULL DEFAULT '0',
    PRIMARY KEY (`name`)
) ENGINE=InnoDB;

注意区别,id字段不是自增的,也不是主键。在使用前,咱们须要先插入一些初始化数据:

INSERT INTO `sequence` (`name`) VALUES 
('users'), ('photos'), ('albums'), ('comments');

接下来,咱们能够经过执行下面的SQL语句来得到新的照片ID:

UPDATE `sequence` SET `id` = LAST_INSERT_ID(`id` + 1) WHERE `name` = 'photos';
SELECT LAST_INSERT_ID();

咱们执行了一个更新操做,将id字段增长1,并将增长后的值传递到LAST_INSERT_ID函数, 从而指定了LAST_INSERT_ID的返回值。

实际上,咱们不必定须要预先指定序列的名字。若是咱们如今须要一种新的序列,咱们能够直接执行下面的SQL语句:

INSERT INTO `sequence` (`name`) VALUES('new_business') ON DUPLICATE KEY UPDATE `id` = LAST_INSERT_ID(`id` + 1);
SELECT LAST_INSERT_ID();

这种方案的问题在于序列生成的逻辑脱离了数据库层,由应用层负责,增长了开发复杂度。固然,其实能够用spring来解决这一问题,由于在spring JDBC中已经对这种序列生成逻辑进行了简单的封装。
咱们能够看一下spring的相关源代码:MySQLMaxValueIncrementer.

@Override
    protected synchronized long getNextKey() throws DataAccessException {
        if (this.maxId == this.nextId) {
            /*
            * Need to use straight JDBC code because we need to make sure that the insert and select
            * are performed on the same connection (otherwise we can't be sure that last_insert_id()
            * returned the correct value)
            */
            Connection con = DataSourceUtils.getConnection(getDataSource());
            Statement stmt = null;
            try {
                stmt = con.createStatement();
                DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
                // Increment the sequence column...
                String columnName = getColumnName();
                stmt.executeUpdate("update "+ getIncrementerName() + " set " + columnName +
                        " = last_insert_id(" + columnName + " + " + getCacheSize() + ")");
                // Retrieve the new max of the sequence column...
                ResultSet rs = stmt.executeQuery(VALUE_SQL);
                try {
                    if (!rs.next()) {
                        throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update");
                    }
                    this.maxId = rs.getLong(1);
                }
                finally {
                    JdbcUtils.closeResultSet(rs);
                }
                this.nextId = this.maxId - getCacheSize() + 1;
            }
            catch (SQLException ex) {
                throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", ex);
            }
            finally {
                JdbcUtils.closeStatement(stmt);
                DataSourceUtils.releaseConnection(con, getDataSource());
            }
        }
        else {
            this.nextId++;
        }
        return this.nextId;
    }

spring的实现也就是经过update语句对incrementerName表里的columnName 列进行递增,并经过mysql的last_insert_id()返回最近生成的值。并保证了事务性及方法的并发支持。只是这个实现有些过于简单,好比:一个表对应一个序列的作法在实际应用开发中显得过于零碎,因此在实际应用中须要对其实现进行修改,实现一条记录对应一个序列的策略。另外对水平分片的支持并不在这一实现考虑范围内。同时,这种作法依然没法回避表锁的机制,因此这里经过CacheSize()的作法,实现了一次申请并缓存在内存中,以减小表锁的发生频率。

如何解决水平分片的需求

UUID

因为UUID出现重复的几率基本能够忽略,因此对分片是天生支持的。

独立的序列库

单独创建一个库用来生成ID,在Shard中的每张表在这个ID库中都有一个对应的表,而这个对应的表只有一个字段, 这个字段是自增的。当咱们须要插入新的数据,咱们首先在ID库中的相应表中插入一条记录,以此获得一个新的ID, 而后将这个ID做为插入到Shard中的数据的主键。这个方法的缺点就是须要额外的插入操做,若是ID库变的很大, 性能也会随之下降。因此必定要保证ID库的数据集不要太大,一个办法是按期清理前面的记录

复合标识符

这种作法是经过联合主键的策略,即经过两个字段来生成一个惟一标识,前半部分是分片标识符,后半部分是本地生成的标识符(好比使用AUTO_INCREMENT生成)

带分库策略的自定义序列表

这种作法能够基于上面提到的自定义序列表的方法的基础上,作一些技巧性的调整。即以下:

UPDATE `sequence` SET `id` = LAST_INSERT_ID(`id` + 1) WHERE `name` = 'photos';
SELECT LAST_INSERT_ID();

这里的id初始值设定上要求不一样的分片取不一样的值,且必须连续。同时将每次递增的步长设定为服务器数目。
好比有3台机器,那么咱们只要将初始值分别设置为1,2,3. 而后执行下面的语句便可:

UPDATE `sequence` SET `id` = LAST_INSERT_ID(`id` + 3) WHERE `name` = 'photos';
SELECT LAST_INSERT_ID();

这就能够解决主键生成冲突的问题。可是若是在运行一段时间后要进行动态扩充分片数的时候,须要对序列初始值作一次调整,以确保其连续性,不然依然可能存在冲突的可能。固然这些逻辑能够封装在数据访问层的代码中。

主键的必要性

表中每一行都应该有能够惟一标识本身的一列(或一组列)。虽然并不老是都须要主键,但大多数数据库设计人员都应保证他们建立的每一个表有一个主键,以便于之后数据操纵和管理。其实即便你不建主键,MySQL(InnoDB引擎)也会本身创建一个隐藏6字节的ROWID做为主键列,详细能够参见[这里]

由于,InnoDB引擎使用汇集索引,数据记录自己被存于主索引(一颗B+Tree)的叶子节点上。这就要求同一个叶子节点内(大小为一个内存页或磁盘页)的各条数据记录按主键顺序存放,所以每当有一条新的记录插入时,MySQL 会根据其主键将其插入适当的节点和位置,若是页面达到装载因子(InnoDB默认为15/16),则开辟一个新的页(节点)

因此在使用innoDB表时要避免随机的(不连续且值的分布范围很是大)聚簇索引,特别是针对I/O密集型的应用。例如:从性能角度考虑,使用UUID的方案就会致使聚簇索引的插入变得彻底随机。

主键的数据类型选择

关于主键的类型选择上最多见的争论是用整型仍是字符型的问题,关于这个问题《高性能MySQL》一书中有明确论断:
整数一般是标识列的最好选择,由于它很快且可使用AUTO_INCREAMENT,若是可能,应该避免使用字符串类型做为标识列,由于很消耗空间,且一般比数字类型慢。

若是是使用MyISAM,则就更不能用字符型,由于MyISAM默认会对字符型采用压缩引擎,从而致使查询变得很是慢。 参考: 一、http://www.cnblogs.com/lsx1993/p/4663147.html 二、http://www.cnblogs.com/zhoujinyi/p/3433823.html 三、http://www.zolazhou.com/posts/primary-key-selection-in-database-partition-design/ 四、《高性能MySQL》 五、《高可用MySQL》

相关文章
相关标签/搜索