雪花算法(SnowFlake)
简介
现在的服务基本是分布式、微服务形式的,而且大数据量也导致分库分表的产生,对于水平分表就需要保证表中 id 的全局唯一性。
对于 MySQL 而言,一个表中的主键 id 一般使用自增的方式,但是如果进行水平分表之后,多个表中会生成重复的 id 值。那么如何保证水平分表后的多张表中的 id 是全局唯一性的呢?
如果还是借助数据库主键自增的形式,那么可以让不同表初始化一个不同的初始值,然后按指定的步长进行自增。例如有3张拆分表,初始主键值为1,2,3,自增步长为3。
当然也有人使用 UUID 来作为主键,但是 UUID 生成的是一个无序的字符串,对于 MySQL 推荐使用增长的数值类型值作为主键来说不适合。
也可以使用 Redis 的自增原子性来生成唯一 id,但是这种方式业内比较少用。
当然还有其他解决方案,不同互联网公司也有自己内部的实现方案。雪花算法是其中一个用于解决分布式 id 的高效方案,也是许多互联网公司在推荐使用的。
SnowFlake 雪花算法
SnowFlake 中文意思为雪花,故称为雪花算法。最早是 Twitter 公司在其内部用于分布式环境下生成唯一 ID。在2014年开源 scala 语言版本。
雪花算法的原理就是生成一个的 64 位比特位的 long 类型的唯一 id。
最高 1 位固定值 0,因为生成的 id 是正整数,如果是 1 就是负数了。
接下来 41 位存储毫秒级时间戳,2^41/(1000606024365)=69,大概可以使用 69 年。
再接下 10 位存储机器码,包括 5 位 datacenterId 和 5 位 workerId。最多可以部署 2^10=1024 台机器。
最后 12 位存储序列号。同一毫秒时间戳时,通过这个递增的序列号来区分。即对于同一台机器而言,同一毫秒时间戳下,可以生成 2^12=4096 个不重复 id。
可以将雪花算法作为一个单独的服务进行部署,然后需要全局唯一 id 的系统,请求雪花算法服务获取 id 即可。
对于每一个雪花算法服务,需要先指定 10 位的机器码,这个根据自身业务进行设定即可。例如机房号+机器号,机器号+服务号,或者是其他可区别标识的 10 位比特位的整数值都行。
实际应用
MybatisPlus实现
依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.1</version> </dependency>
yml配置:
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: worker-id: ${random.int(1,31)} datacenter-id: ${random.int(1,31)}
测试实体:
@Data @TableName("test_content") public class TestContent { /** * ID */ @TableId(type = IdType.ASSIGN_ID) private Long id; /** * 数据内容 */ private String content; /** * 部门id */ private Integer deptId; }
测试控制层:
@GetMapping("/test2") public String add() { TestContent testContent = new TestContent(); testContent.setContent(new Random().nextInt() + "自定义添加内容"); testContent.setDeptId(1); int insert = testContentService.getBaseMapper().insert(testContent); log.info("插入成功:{}", testContent.getId()); return "插入成功"; }
插入测试:
非ID字段需要id时可使用Idwork
testContent.setId(IdWorker.getId());
源码解析:
IdWorker提供获取id的基本方法,底层通过DefaultIdentifierGenerator生成序列Sequence类用于生成雪花id
public class IdWorker { private static IdentifierGenerator IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(); public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); public IdWorker() { } public static long getId() { return getId(new Object()); } public static long getId(Object entity) { return IDENTIFIER_GENERATOR.nextId(entity).longValue(); } public static String getIdStr() { return getIdStr(new Object()); } public static String getIdStr(Object entity) { return IDENTIFIER_GENERATOR.nextId(entity).toString(); } public static String getMillisecond() { return LocalDateTime.now().format(MILLISECOND); } public static String getTimeId() { return getMillisecond() + getIdStr(); } public static void initSequence(long workerId, long dataCenterId) { IDENTIFIER_GENERATOR = new DefaultIdentifierGenerator(workerId, dataCenterId); } public static void setIdentifierGenerator(IdentifierGenerator identifierGenerator) { IDENTIFIER_GENERATOR = identifierGenerator; } public static String get32UUID() { ThreadLocalRandom random = ThreadLocalRandom.current(); return (new UUID(random.nextLong(), random.nextLong())).toString().replace("-", ""); } }
Sequence类:主要构造方法包含两个参数 类比雪花算法的机器ID和服务ID,集群模式下最好不要重复,否则可能会造成生成的Id重复,两个参数可在YML文件中配置
public class Sequence { private static final Log logger = LogFactory.getLog(Sequence.class); private final long twepoch = 1288834974657L; private final long workerIdBits = 5L; private final long datacenterIdBits = 5L; private final long maxWorkerId = 31L; private final long maxDatacenterId = 31L; private final long sequenceBits = 12L; private final long workerIdShift = 12L; private final long datacenterIdShift = 17L; private final long timestampLeftShift = 22L; private final long sequenceMask = 4095L; private final long workerId; private final long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; public Sequence() { this.datacenterId = getDatacenterId(31L); this.workerId = getMaxWorkerId(this.datacenterId, 31L); } public Sequence(long workerId, long datacenterId) { Assert.isFalse(workerId > 31L || workerId < 0L, String.format("worker Id can't be greater than %d or less than 0", 31L), new Object[0]); Assert.isFalse(datacenterId > 31L || datacenterId < 0L, String.format("datacenter Id can't be greater than %d or less than 0", 31L), new Object[0]); 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 (StringUtils.isNotBlank(name)) { mpid.append(name.split("@")[0]); } return (long)(mpid.toString().hashCode() & 'uffff') % (maxWorkerId + 1L); } 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 = (255L & (long)mac[mac.length - 1] | 65280L & (long)mac[mac.length - 2] << 8) >> 6; id %= maxDatacenterId + 1L; } } } catch (Exception var7) { logger.warn(" getDatacenterId: " + var7.getMessage()); } return id; } public synchronized long nextId() { long timestamp = this.timeGen(); if (timestamp < this.lastTimestamp) { long offset = this.lastTimestamp - timestamp; if (offset > 5L) { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset)); } try { this.wait(offset << 1); timestamp = this.timeGen(); if (timestamp < this.lastTimestamp) { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset)); } } catch (Exception var6) { throw new RuntimeException(var6); } } if (this.lastTimestamp == timestamp) { this.sequence = this.sequence + 1L & 4095L; if (this.sequence == 0L) { timestamp = this.tilNextMillis(this.lastTimestamp); } } else { this.sequence = ThreadLocalRandom.current().nextLong(1L, 3L); } this.lastTimestamp = timestamp; return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence; } protected long tilNextMillis(long lastTimestamp) { long timestamp; for(timestamp = this.timeGen(); timestamp <= lastTimestamp; timestamp = this.timeGen()) { } return timestamp; } protected long timeGen() { return SystemClock.now(); } }