目录html
在项目过程当中遇到一个看似极为基础的问题,可是在深刻思考后仍是引出了很多问题,以为有必要把这一学习过程进行记录。mysql
优势:spring
一、数据库自动编号,速度快,并且是增量增加,汇集型主键按顺序存放,对于检索很是有利。sql
二、 数字型,占用空间小,易排序,在程序中传递方便。数据库
缺点:
一、不支持水平分片架构,水平分片的设计当中,这种方法显然不能保证全局惟一。
二、表锁缓存
在MySQL5.1.22以前,InnoDB自增值是经过其自己的自增加计数器来获取值,该实现方式是经过表锁机制来完成的(AUTO-INC LOCKING)。锁不是在每次事务完成后释放,而是在完成对自增加值插入的SQL语句后释放,要等待其释放才能进行后续操做。好比说当表里有一个auto_increment字段的时候,innoDB会在内存里保存一个计数器用来记录auto_increment的值,当插入一个新行数据时,就会用一个表锁来锁住这个计数器,直到插入结束。若是大量的并发插入,表锁会引发SQL堵塞。
在5.1.22以后,InnoDB为了解决自增主键锁表的问题,引入了参数innodb_autoinc_lock_mode:安全
三、自增主键不连续服务器
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,因此下次再插入的时候,主键就会不连续。架构
优势
一、全局惟一性、安全性、可移植性。并发
二、可以保证独立性,程序能够在不一样的数据库间迁移,效果不受影响。
三、保证生成的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出现重复的几率基本能够忽略,因此对分片是天生支持的。
单独创建一个库用来生成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》