赞
踩
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中记录当前值及最大值。
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保证生成的 ID 肯定是唯一有序的。
Twitter 利用 zookeeper 实现了一个全局ID生成的服务 Snowflake:https://github.com/twitter-archive/snowflake
如上图的所示,Twitter 的 Snowflake 算法由下面几部分组成:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用的ID一般都是正数,所以最高位为 0。
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年。
包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024 s个节点。超过这个数量,生成的ID就有可能会冲突。
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
UidGenerator是百度开源的分布式ID生成器,基于于snowflake算法的实现,看起来感觉还行。不过,国内开源的项目维护性真是担忧。
具体可以参考官网说明:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。
依赖于Zookeeper增加了系统的复杂度。
具体可以参考官网说明:https://tech.meituan.com/2017/04/21/mt-leaf.html
时钟回拨
避免不同服务器workerId(0-31) 和 datacenterId (0-31)的值相同
组合起来最多也就是最多支持1024台机器
新版的mybatisplus 分布式默认情况下,并不需要我们主动去配置datacenterId和workerId的值。mybatis-plus框架会根据应用所在服务器IP地址来生成datacenterId和workerId
此算法依赖MAC地址后两位散列来保证DataCenterId不重复,同时由于打包镜像的原因,WorkerId的随机效果无效,同一服务在数量较多时,DataCenterId和WorkerId相同概率极高,数量>32时,一定会产生DataCenterId和WorkerId相同
在CI流程中,手动指定要启动的Pod(服务)的DataCenterId和WorkerId,写入环境变量,服务内部接收环境变量并用此参数初始化Sequence对象,MybatisPlus提供了对应的自定义配置。 但是这样很鸡肋每次加机器的时候都需要配置,如果忘记了会造成很大的生产事故
比如:参考美团的唯一ID生成器,使用ZK配合虚拟节点来获取当前已经被占用的WorkerId,计算本服务的WorkerId,并使用心跳保持。 使用了ZK 带来了系统复杂度 需要额外去维护。 还有其他方案也可以参考
这里使用redis的方案去维护workerId 和 datacenterId
我是在mybatisplus的配置类加载后去定义我们的workerId, datacenterId的值的。需要借助redis和redisson分布式锁。原理就是workerId, datacenterId会逐个递增,直到两个值都最大,然后都归零,重新开始。只要服务数量不大于900(30x30)多,理论上重启就不会重复。
public void changeIdWorker(){ RLock lock = redisson.getLock(RedisKeyConstant.SYS_REDIS_LOCK_PREFIX + "id:worker"); try{ lock.lock(); long workId = 0; long dataId; while(true){ //redis中取值 Object workIdStr = redisTemplate.opsForValue().get(RedisKeyConstant.SYS_WORK_INCREMENT); if(workIdStr != null){ workId = workIdStr instanceof Integer ? (Integer) workIdStr : (Long) workIdStr; } //判断值 dataId = redisTemplate.opsForValue().increment(RedisKeyConstant.SYS_DATA_INCREMENT, 1); if(dataId > 30){ redisTemplate.opsForValue().set(RedisKeyConstant.SYS_DATA_INCREMENT,0); workId = redisTemplate.opsForValue().increment(RedisKeyConstant.SYS_WORK_INCREMENT, 1); if(workId > 30){ redisTemplate.opsForValue().set(RedisKeyConstant.SYS_WORK_INCREMENT,0); } continue; } break; } log.info("changeIdWorker workId:{},dataId:{}",workId,dataId); IdWorker.initSequence(workId,dataId); }finally{ lock.unlock(); } }
这种方案解决了 重启 可能会造成DataCenterId和WorkerId 重复的问题
雪花id是19位的 会导致前端js获取进度问题,为了避免前后端改动大,进行了对雪花算法的魔改生成16位的id(解决了前端js进度丢失的问题)
public class CustomIdGenerator implements IdentifierGenerator { private IdGenerator idGenerator; private Redisson redisson = SpringUtils.getBean("redisson"); private RedisTemplate redisTemplate = SpringUtils.getBean("redisTemplate"); public CustomIdGenerator() { RLock lock = redisson.getLock(RedisKeyConstant.SYS_REDIS_LOCK_PREFIX + "id:worker"); try{ lock.lock(); long workId = 0; long dataId; while(true){ //redis中取值 Object workIdStr = redisTemplate.opsForValue().get(RedisKeyConstant.SYS_WORK_INCREMENT); if(workIdStr != null){ workId = workIdStr instanceof Integer ? (Integer) workIdStr : (Long) workIdStr; } //判断值 dataId = redisTemplate.opsForValue().increment(RedisKeyConstant.SYS_DATA_INCREMENT, 1); if(dataId > 30){ redisTemplate.opsForValue().set(RedisKeyConstant.SYS_DATA_INCREMENT,0); workId = redisTemplate.opsForValue().increment(RedisKeyConstant.SYS_WORK_INCREMENT, 1); if(workId > 30){ redisTemplate.opsForValue().set(RedisKeyConstant.SYS_WORK_INCREMENT,0); } continue; } break; } log.info("changeIdWorker workId:{},dataId:{}",workId,dataId); idGenerator = new IdGenerator(workId,dataId); }finally{ lock.unlock(); } }
魔改后的雪花算法生成策略(16位)
public class IdGenerator { private static final Log logger = LogFactory.getLog(IdGenerator.class); /** * 起始的时间戳 */ private static final long START_TIMESTAMP = 1622505600000L; // 各部分占用的位数 private static final long SEQUENCE_BITS = 6; private static final long WORKER_ID_BITS = 5; private static final long DATA_CENTER_ID_BITS = 5; // 各部分的最大值 private static final long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BITS); private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS); private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS); // 各部分的位移 private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; // 工作节点ID private final long workerId; // 数据中心ID private final long dataCenterId; // 序列号 private long sequence = 0L; // 上次生成ID的时间戳 private long lastTimestamp = -1L; //IP地址 private InetAddress inetAddress; public IdGenerator(long workerId, long dataCenterId) { this.workerId = workerId; this.dataCenterId = dataCenterId; } public IdGenerator() throws UnknownHostException { this.inetAddress = InetAddress.getLocalHost(); this.dataCenterId = getDatacenterId(MAX_DATA_CENTER_ID); this.workerId = getMaxWorkerId(dataCenterId, MAX_WORKER_ID); logger.warn(" IdGenerator init success. dataCenterId:" + dataCenterId + " workerId:" + workerId + " InetAddress:" + inetAddress); } /** * 获取 maxWorkerId */ protected long getMaxWorkerId(long datacenterId, long maxWorkerId) { StringBuilder mpid = new StringBuilder(); mpid.append(datacenterId); String name = ManagementFactory.getRuntimeMXBean().getName(); if (StringUtils.isNotBlank(name)) { /* * GET jvmPid */ mpid.append(name.split(StringPool.AT)[0]); } /* * MAC + PID 的 hashcode 获取16个低位 */ return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1); } /** * 数据标识id部分 */ protected long getDatacenterId(long maxDatacenterId) { long id = 0L; try { if (null == this.inetAddress) { this.inetAddress = InetAddress.getLocalHost(); } NetworkInterface network = NetworkInterface.getByInetAddress(this.inetAddress); if (null == network) { id = 1L; } else { byte[] mac = network.getHardwareAddress(); if (null != mac) { id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6; id = id % (maxDatacenterId + 1); } } } catch (Exception e) { logger.warn(" getDatacenterId: " + e.getMessage()); } return id; } public synchronized long generateId() { long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate ID."); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & MAX_SEQUENCE; if (sequence == 0) { currentTimestamp = getNextTimestamp(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence; } private long getNextTimestamp(long lastTimestamp) { long currentTimestamp = System.currentTimeMillis(); while (currentTimestamp <= lastTimestamp) { currentTimestamp = System.currentTimeMillis(); } return currentTimestamp; } }
前提是在生成 16位 不维护workid 和 dataid 的 前提下
16位 单机测试 2000个线程同时压测
1台机器压测 work dataid 为 1,1
2台机器压测 同环境 workid dataid 为1,1
可以看出错误率为3.16% 0.03164556962025317
3 台机器压测 同环境 workid dataid 为1,1
可以看出错误率为3.93% 0.03929384965831435
根据上面 private static final long SEQUENCE_BITS = 6; 来看 每毫秒能生成 26(64)个不同的id
单机情况下只要并发不是很大的情况下 就不会发生重复
以此类推,说明在同一台机器上面部署的机器越多,重复的概率就越高
由于用到了第三方服务 redis 考虑到第三方服务不稳定 导致服务不可用
现有如下的解决方案:
但是会产生一个弊端 由于随机的浮动概率不大 容易发生碰撞 导致 workid 和 dataid 重复
也会存在跟上面一样有概率出现重复的问题,概率应该不大
对于分布式系统部署之前,记得先进行系统时钟的校准同步,这样在部署之后不用再进行校准,对于大并发的场景不会产生分布式ID重复的异常。
附,系统时钟调整校准命令,在部署业务系统之前执行:
yum install ntpdate -y
ntpdate time.windows.com
Number精度是16位(雪花ID是19位的),So:JS的Number数据类型导致的精度丢失。
https://github.com/baomidou/mybatis-plus/issues/3077
需要多租户的类才加对应的注解
对应的DO类加上
@TableId(type = IdType.ASSIGN_ID)
这样就表示当前模块使用分布式id了
https://blog.csdn.net/w1014074794/article/details/125607205
https://github.com/baomidou/mybatis-plus/issues/3077
https://github.com/baomidou/mybatis-plus/issues/212
https://www.modb.pro/db/150947
https://mp.weixin.qq.com/s/e-hEYALJXVKAP18WheJjRg
https://www.kancloud.cn/hanxt/mybatisplus/1898543
https://www.tpfuture.top/views/issue/MybatisPlusSequenceCollision.html#datacenterid生成逻辑
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。