UUID/GUID
UUID,适合小规模的分布式环境
•大部分数据库系统都支持uuid
•优势
•可以实现跨表,跨库,甚至跨服务器的唯一标识
•多数据库之间数据汇总简单方便
•可以多服务器,分布式部署
•可以独立于数据库单独产生
•能够实现多种复制方案
•不足
•占用空间大,16byte
•产生的ID,可读性差,无法排序
在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能
优点:搭建比较简单,不需要为主键唯一性的处理。
缺点:占用两倍的存储空间(在云上光存储一块就要多花2倍的钱),后期读写性能下降厉害。
Sharding-JDBC
DefaultKeyGenerator,默认的主键生成器。该生成器采用 Twitter Snowflake 算法实现,生成 64 Bits 的 Long 型编号。国内另外一款数据库中间件 MyCAT 分布式主键也是基于该算法实现。国内很多大型互联网公司发号器服务基于该算法加部分改造实现。所以 DefaultKeyGenerator 必须是根正苗红。
Redis 生成 ID
要知道redis的EVAL,EVALSHA命令:
当使用数据库来生成 ID 性能不够要求的时候,可以尝试使用 Redis 来生成ID。这主要依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID。可以用 Redis 原子操作 INCR 和 INCRBY 来实现。可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有 5台 Redis,可以初始化每台 Redis 的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为 :
A : 1,6,11,16,21...
B : 2,7,12,17,22...
C : 3,8,13,18,23...
D : 4,9,14,19,24...
E : 5,10,15,20,25...
随便负载到哪个机确定好,未来很难做修改。但是 3-5 台服务器基本能够满足器上,都可以获得不同的 ID。但是步长和初始值一定要事先确定,使用Redis集群也可以防止单点故障的问题。另外,比较适合使用 Redis 来生成每天从 0 开始的流水号,比如 订单号=日期+当日自增长号。可以每天在 Redis 中生成一个 Key,使用 INCR进行累加
优点 : 1> 不依赖于数据库,灵活方便,且性能优于数据库
2> 数字ID天然排序,对分页或者需要排序的结果很有帮助
缺点 : 1> 如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度
2> 需要编码和配置的工作量比较大
来自Flicker的解决方案
自增ID主键+步长,适合中等规模的分布式场景
优点是:实现简单,后期维护简单,对应用透明。
缺点是:第一次设置相对较为复杂,因为要针对未来业务的发展而计算好足够的步长;
因为MySQL本身支持auto_increment操作,很自然地,我们会想到借助这个特性来实现这个功能。
Flicker在解决全局ID生成方案里就采用了MySQL自增长ID的机制(auto_increment + replace into + MyISAM)。一个生成64位ID方案具体就是这样的:
先创建单独的数据库(eg:ticket),然后创建一个表:
CREATE
TABLE
Tickets64 (
id
bigint
(20) unsigned
NOT
NULL
auto_increment,
stub
char
(1)
NOT
NULL
default
''
,
PRIMARY
KEY
(id),
UNIQUE
KEY
stub (stub)
) ENGINE=MyISAM
当我们插入记录后,执行SELECT * from Tickets64,查询结果就是这样的:
+-------------------+------+
| id | stub |
+-------------------+------+
| 72157623227190423 | a |
+-------------------+------+
在我们的应用端需要做下面这两个操作,在一个事务会话里提交:
REPLACE
INTO
Tickets64 (stub)
VALUES
(
'a'
);
SELECT
LAST_INSERT_ID();
这样我们就能拿到不断增长且不重复的ID了。
到上面为止,我们只是在单台数据库上生成ID,从高可用角度考虑,接下来就要解决单点故障问题:Flicker启用了两台数据库服务器来生成ID,通过区分auto_increment的起始值和步长来生成奇偶数的ID。
TicketServer1:
auto-increment-increment = 2
auto-increment-offset = 1
TicketServer2:
auto-increment-increment = 2
auto-increment-offset = 2
最后,在客户端只需要通过轮询方式取ID就可以了。
优点:充分借助数据库的自增ID机制,提供高可靠性,生成的ID有序。
缺点:占用两个独立的MySQL实例,有些浪费资源,成本较高。
雪花算法自造全局自增ID,适合大数据环境的分布式场景
由twitter公布的开源的分布式id算法snowflake(Java版本)
IdWorker.java:
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();
}
}
测试生成ID的测试类,IdWorkerTest.java:
import java.util.HashSet;
import java.util.Set;
public class IdWorkerTest {
static class IdWorkThread implements Runnable {
private Set<Long> set;
private IdWorker idWorker;
public IdWorkThread(Set<Long> set, IdWorker idWorker) {
this.set = set;
this.idWorker = idWorker;
}
public void run() {
while (true) {
long id = idWorker.nextId();
System.out.println(" real id:" + id);
if (!set.add(id)) {
System.out.println("duplicate:" + id);
}
}
}
}
public static void main(String[] args) {
Set<Long> set = new HashSet<Long>();
final IdWorker idWorker1 = new IdWorker(0, 0);
final IdWorker idWorker2 = new IdWorker(1, 0);
Thread t1 = new Thread(new IdWorkThread(set, idWorker1));
Thread t2 = new Thread(new IdWorkThread(set, idWorker2));
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结
(1)单实例或者单节点组:
经过500W、1000W的单机表测试,自增ID相对UUID来说,自增ID主键性能高于UUID,磁盘存储费用比UUID节省一半的钱。所以在单实例上或者单节点组上,使用自增ID作为首选主键。
(2)分布式架构场景:
20个节点组下的小型规模的分布式场景,为了快速实现部署,可以采用多花存储费用、牺牲部分性能而使用UUID主键快速部署;
20到200个节点组的中等规模的分布式场景,可以采用自增ID+步长的较快速方案。
200以上节点组的大数据下的分布式场景,可以借鉴类似twitter雪花算法构造的全局自增ID作为主键。