引言 在做系统开发时,系统唯一ID是我们在设计一个系统的时候经常遇到的问题,也常常为这个问题纠结。生成ID的方法有很多,适应不同的场景、需求及性能要求。所以有些比较复杂的系统会有多个ID生成策略。在这里总结一下常用到的ID生成策略。
数据库自增长序列或字段 最常见的方式,利用数据库,全表中唯一。如MySQL的AUTO_INCREMENT
。
优点
简单,代码方便,性能可以接受。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点
不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
在性能达不到要求的情况下,比较难于扩展。
如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
分表分库的时候会有麻烦。
优化方案 针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1, 4, 7, 10,Master2生成的是2, 5, 8, 11,Master3生成的是3, 6, 9, 12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。
UUID 常见的方式。可以利用数据库也可以利用程序生成,一般来说全球唯一。
优点
简单,代码方便。
生成ID性能非常好,基本不会有性能问题。
全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
缺点
没有排序,无法保证趋势递增。
UUID往往是使用字符串存储,查询的效率比较低。
存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
传输数据量大。
不可读。
有些时候我们希望能使用简单一些的 ID,并且希望 ID 能够按照时间有序生成,为了解决这个问题,Twitter 发明了 SnowFlake 算法,不依赖第三方介质例如 Redis、数据库,本地生成程序生成分布式自增 ID,这个 ID 只能保证在工作组中的机器生成的 ID 唯一,不能像 UUID 那样保证时空唯一。
算法原理
除了最高位bit标记为不可用以外,其余三组bit占位均可浮动,看具体的业务需求而定。默认情况下41bit的时间戳可以支持该算法使用到2082年,10bit的工作机器id可以支持1023台机器,序列号支持1毫秒产生4095个自增序列id。
SnowFlake – 时间戳 这里时间戳的细度是毫秒级 ,建议使用64位linux系统机器,因为有vdso,gettimeofday()在用户态就可以完成操作,减少了进入内核态的损耗。
SnowFake – 工作机器ID 严格意义上来说这个bit段的使用可以是进程级 ,机器级的话你可以使用MAC地址来唯一标示工作机器,工作进程级可以使用IP+Path来区分工作进程。如果工作机器比较少,可以使用配置文件来设置这个id是一个不错的选择,如果机器过多配置文件的维护是一个灾难性的事情。
SnowFlake – 序列号 序列号就是一系列的自增id(多线程建议使用atomic),为了处理在同一毫秒内需要给多条消息分配id,若同一毫秒把序列号用完了,则 “等待至下一毫秒”。
具体实现 Sequence类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 public class Sequence { private final long twepoch = 1288834974657L ; private final long workerIdBits = 5L ; private final long datacenterIdBits = 5L ; private final long maxWorkerId = -1L ^ (-1L << workerIdBits); private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); private final long sequenceBits = 12L ; private final long workerIdShift = sequenceBits; private final long datacenterIdShift = sequenceBits + workerIdBits; private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private final long sequenceMask = -1L ^ (-1L << sequenceBits); private long workerId; private long datacenterId; private long sequence = 0L ; private long lastTimestamp = -1L ; public Sequence () { datacenterId = getDatacenterId(maxDatacenterId); workerId = getMaxWorkerId(datacenterId, maxWorkerId); } public Sequence (long workerId, long datacenterId) { 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; } protected static long getMaxWorkerId (long datacenterId, long maxWorkerId) { StringBuilder mpid = new StringBuilder(); mpid.append(datacenterId); String name = ManagementFactory.getRuntimeMXBean().getName(); if (name != null && !"" .equals(name)) { mpid.append(name.split("@" )[0 ]); } return (mpid.toString().hashCode() & 0xffff ) % (maxWorkerId + 1 ); } protected static long getDatacenterId (long maxDatacenterId) { long id = 0L ; try { InetAddress ip = InetAddress.getLocalHost(); NetworkInterface network = NetworkInterface.getByInetAddress(ip); if (network == null ) { id = 1L ; } else { byte [] mac = network.getHardwareAddress(); if (null != mac) { id = ((0x000000FF & (long ) mac[mac.length - 1 ]) | (0x0000FF00 & (((long ) mac[mac.length - 2 ]) << 8 ))) >> 6 ; id = id % (maxDatacenterId + 1 ); } } } catch (Exception e) { System.err.println(" getDatacenterId: " + e.getMessage()); } return id; } public synchronized long nextId () { long timestamp = timeGen(); if (timestamp < lastTimestamp) { long offset = lastTimestamp - timestamp; if (offset <= 5 ) { try { wait(offset << 1 ); timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds" , offset)); } } catch (Exception e) { throw new RuntimeException(e); } } else { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds" , offset)); } } 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 SystemClock.now(); } }
SystemClock类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class SystemClock { private final long period; private final AtomicLong now; private SystemClock (long period) { this .period = period; this .now = new AtomicLong(System.currentTimeMillis()); scheduleClockUpdating(); } private static class InstanceHolder { public static final SystemClock INSTANCE = new SystemClock(1 ); } private static SystemClock instance () { return InstanceHolder.INSTANCE; } private void scheduleClockUpdating () { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { public Thread newThread (Runnable runnable) { Thread thread = new Thread(runnable, "System Clock" ); thread.setDaemon(true ); return thread; } }); scheduler.scheduleAtFixedRate(new Runnable() { public void run () { now.set(System.currentTimeMillis()); } }, period, period, TimeUnit.MILLISECONDS); } private long currentTimeMillis () { return now.get(); } public static long now () { return instance().currentTimeMillis(); } public static String nowDate () { return new Timestamp(instance().currentTimeMillis()).toString(); } }
测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class IdGen { private static Sequence sequence = new Sequence(); public static String sequenceId () { long nextId = sequence.nextId(); return String.valueOf(nextId); } public static void main (String[] args) { for (int i = 0 ; i < 1000 ; i++) { long id = sequenceId(); System.out.println(id); } }
SnowFlake算法可以根据自身项目的需要进行一定的修改。比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的bit数。
优点
不依赖于数据库,灵活方便,且性能优于数据库。
ID按照时间在单机上是递增的。
缺点 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。
总结 在项目中SnowFlake算法生成ID是第一选择,兼具性能和灵活性。