1. 基本解决方案
1.使用数据库自增Id2.使用redis的incr命令3.使用UUID4.Twitter的snowflake算法5.利用zookeeper生成唯一ID6.MongoDB的ObjectId复制代码
1. 数据库自增id
最常见的方式。利用数据库,全数据库唯一。
优点:
1)简单,代码方便,性能可以接受。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
1)不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
2)在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
3)在性能达不到要求的情况下,比较难于扩展。
4)如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
5)分表分库的时候会有麻烦。
优化方案:
1)针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载
2. redis 的incr 命令
当使用数据库来生成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进行累加
3. UUID
Java 自带的
UUID.randomUUID()复制代码
4. snowflake算法
snowflake 是Twitter开源的分布式ID生成算法,其结果是一个long型的ID;核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit是机器ID),12bit作为每毫秒可以产生4096个ID),最后一个符号位,永远是0
public class SnowFlake { /** * 起始时间 */ private final static long START_STMP = 1480166465631L; /** * 每一部分占用的位数 */ private final static long SEQUENCE_BIT = 12;//序列号占用的位数 private final static long MACHINE_BIT = 5;//机器标识占用的位数 private final static long DATACENTER_BIT = 5;//数据中心占用的位数 /** * 每一部分的最大值 */ private final static long MAX_SEQUENCE = -1L^ (-1L << SEQUENCE_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); /** * 每一部分向左的位移 * @param args */ private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT; private long datacenterId;//数据中心 private long machineId;//机器标识 private long sequence = 0L;//序列号 private long lastStmp = -1L;//上次时间戳 public SnowFlake(long datacenterId,long machineId){ if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; } /** * 生产下一个ID */ public synchronized long nextId(){ long currStmp = getNewstmp(); if(currStmp < lastStmp) throw new RuntimeException("Clock moved backwards. Refusing to generate id\""); if(currStmp == lastStmp){ //相同毫秒内,序列号自增 sequence = (sequence + 1) & MAX_SEQUENCE; // 同一毫秒的序列数已经达到最大 if(sequence == 0L) currStmp = getNextMill(); } else { sequence = 0L;//不同毫秒内,序列号置为0 } lastStmp = currStmp; return (currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT | machineId << MACHINE_LEFT | sequence; } private long getNextMill() { long mill = getNewstmp(); while (mill <= lastStmp) { mill = getNewstmp(); } return mill; } private long getNewstmp() { return System.currentTimeMillis(); } public static void main(String[] args) { SnowFlake snowFlake = new SnowFlake(2, 3); for (int i = 0; i < (1 << 12); i++) { System.out.println(snowFlake.nextId()); } }}复制代码
5. zookeeper生成唯一ID
zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。 很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想
6. MongoDB的ObjectId
如 4e7020cb7cac81af7136236b
Returns a new ObjectId value. The 12-byte ObjectId value consists of:1.a 4-byte value representing the seconds since the Unix epoch,2.a 3-byte machine identifier,3.a 2-byte process id, and4.a 3-byte counter, starting with a random value.复制代码
1.(Time)时间戳。将刚才生成的objectid的前4位进行提取“4e7020cb”,然后按照十六进制转为十进制,变为“1315971275”,这个数字就是一个时间戳。通过时间戳的转换,就成了易看清的时间格式。
2.(Machine)机器。接下来的三个字节就是“7cac81”,这三个字节是所在主机的唯一标识符,一般是机器主机名的散列值,这样就确保了不同主机生成不同的机器hash值,确保在分布式中不造成冲突,这也就是在同一台机器生成的objectId中间的字符串都是一模一样的原因。
3.(PID)进程ID。上面的Machine是为了确保在不同机器产生的objectId不冲突,而pid就是为了在同一台机器不同的mongodb进程产生了objectId不冲突,接下来的“af71”两位就是产生objectId的进程标识符。
4.(INC)自增计数器。前面的九个字节是保证了一秒内不同机器不同进程生成objectId不冲突,这后面的三个字节“36236b”是一个自动增加的计数器,用来确保在同一秒内产生的objectId也不会发现冲突,允许256的3次方等于16777216条记录的唯一性。
具体客户端实现 自增(INC) 的实现
/*** AtomicInteger 无锁CAS,线程安全*/private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt()); /** * Constructs a new instance using the given date. * * @param date the date */ public ObjectId(final Date date) { this(dateToTimestampSeconds(date), MACHINE_IDENTIFIER, PROCESS_IDENTIFIER, NEXT_COUNTER.getAndIncrement(), false);}复制代码