昨天在与别人的交流中我得知一个劝告:“设计数据库,你最好将自增id作为主键,而不是别的”。我想,为什么呢?为什么非得用一个与业务无关的自增ID作为主键呢?他还没有我的业务编号作为主键查找方便呢。于是,我带着疑虑,开始了慢慢求证之路。
一、从数据在数据库的存储角度来看,自增id 是int 型,一般比自定义的属性(比如员工号,员工名字,或者用uuid等)作为主键,所占的磁盘空间都小很多。这样的话,在大型数据的查询、读写,前者的效率是比后者的效率高很多的。感兴趣的小伙伴可以用上百万的数据测试一下。
二、从数据库的设计来看,mysql的底层是InnoDB,它的数据结构是B+树。所以对于InnoDB的主键,尽量用整型,而且是递增的整型。这样在存储/查询上都是非常高效的。
对InnoDB来说
1: 主键索引既存储索引值,又在叶子节点中存储行的数据,也就是说数据文件本身就是按照b+树方式存放数据的。
2: 如果没有定义主键,则会使用非空的UNIQUE键做主键 ; 如果没有非空的UNIQUE键,则系统生成一个6字节的rowid做主键;
聚簇索引中,N行形成一个页(一页通常大小为16K)。如果碰到不规则数据插入时,为了保持B+树的平衡,会造成频繁的页分裂和页旋转,插入速度比较慢。所以聚簇索引的主键值应尽量是连续增长的值,而不是随机值(不要用随机字符串或UUID)。
如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。 总的来说就是可以提高查询和插入的性能。
三、从数据库表的设计来看,用自增id作为主键是存在一些问题的。特别是在分布式架构中,多个实例中要保持一个表的主键的唯一性,而普通单表的自增id主键就满足不了这点。通俗点说,在分布式架构中,在一个高并发的环境下,会有两个或者多个数据同时生成且他们的ID相同,这就不符合主键的要求了。
那如何解决这个问题呢,是要抛弃自增ID的高效率改用自定义的属性作为主键了吗?
如果你只有一个主数据库,那么自增id是不会重复的,但这个地方会成为系统的瓶颈,而且也容易成为一个单点故障。
如果你是主/从数据库,这可以解决单点的问题,但不会解决生成id瓶颈的问题。而且在主数据库挂掉,进行主从切换的时候,这个自增id是可能出问题的。比较能接受的集群方案是TiDB这种一-批一批生 成id的做法,这样id可以保证不重复,不过就不能保证依次连续(sequential) 了。如果不保证连续的话,我们其实没必要把这个工作非要交给数据库了。
所以在一般载流量极大的互联网应用中我们就不推荐使用数据库的自增id了。
很多替代方案比如:uuid或者雪花算法。
下面我就分别介绍一下这三种方法:
第一种,自增ID主键+步长,适合中等规模的分布式场景
在每个集群节点组的master上面,设置(auto_increment_increment),让目前每个集群的起始点错开 1,步长选择大于将来基本不可能达到的切分集群数,达到将 ID 相对分段的效果来满足全局唯一的效果。 优点是:实现简单,后期维护简单,对应用透明。 缺点是:第一次设置相对较为复杂,因为要针对未来业务的发展而计算好足够的步长;
规划:
比如计划总共N个节点组,那么第i个节点组的my.cnf的配置为:
auto_increment_offset i auto_increment_increment N
假如规划48个节点组,N为48,现在配置第8个节点组,这个i为8,第8个节点组的my.cnf里面的配置为:
auto_increment_offset 8 auto_increment_increment 48 REPLACE INTO Tickets64 (stub) VALUES ('a'); SELECT LAST_INSERT_ID();
第二种,UUID,适合小规模的分布式环境
对于InnoDB这种聚集主键类型的引擎来说,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力,而且由于索引和数据存储在一起,字符串做主键会造成存储空间增大一倍。
在存储和检索的时候,innodb会对主键进行物理排序,这对auto_increment_int是个好消息,因为后一次插入的主键位置总是在最后。但是对uuid来说,这却是个坏消息,因为uuid是杂乱无章的,每次插入的主键位置是不确定的,可能在开头,也可能在中间,在进行主键物理排序的时候,势必会造成大量的 IO操作影响效率,在数据量不停增长的时候,特别是数据量上了千万记录的时候,读写性能下降的非常厉害。
优点:搭建比较简单,不需要为主键唯一性的处理。
缺点:占用两倍的存储空间(在云上光存储一块就要多花2倍的钱),后期读写性能下降厉害。
第三种、雪花算法自造全局自增ID,适合大数据环境的分布式场景
由twitter公布的开源的分布式id算法snowflake(Java版本)
这个算法要翻墙才能看,我在网上找到了,一个版本。你们看看:
IdWorker.java:
package com.demo.elk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IdWorker {
protected static final Logger LOG = LoggerFactory.getLogger(IdWorker.class);
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long twepoch = 1288834974657L;
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
private long sequenceBits = 12L;
private long workerIdShift = sequenceBits;
private long datacenterIdShift = sequenceBits + workerIdBits;
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
public IdWorker(long workerId, long datacenterId) {
// sanity check for workerId
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;
LOG.info(String.format("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d", timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId));
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
LOG.error(String.format("clock is moving backwards. Rejecting requests until %d.", 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;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
}
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
protected long timeGen() {
return System.currentTimeMillis();
}
}