线上使用雪花算法生成id重复问题

news2024/10/7 10:16:02

项目中使用的是hutool工具类库提供的雪花算法生成id方式,版本使用的是5.3.1

 		<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.1</version>
        </dependency>

雪花算法生成id方式提供了getSnowflake(workerId,datacenterId)获取单例的Snowflake对象,并对生成id的方法nextId()进行了synchronized加锁处理。

IdUtil

	public static Snowflake getSnowflake(long workerId, long datacenterId) {
		return Singleton.get(Snowflake.class, workerId, datacenterId);
	}

Snowflake

	public synchronized long nextId() {
		long timestamp = genTime();
		if (timestamp < lastTimestamp) {
			// 如果服务器时间有问题(时钟后退) 报错。
			throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", 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;
	}

项目中使用雪花算法
IdUtils

public class IdUtils {
    private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(1, 1);
    public static Long getNextId() {
        return SNOWFLAKE.nextId();
    }
}

举例controller
UserController

@Slf4j
@RestController
@RequestMapping("/id")
public class UserController {
    @Autowired
    private IUserService userService;


    @GetMapping("/next")
    public Long next() {
        Long id = IdUtils.getNextId();
        User user = new User().setId(id);
        boolean save = userService.save(user);
        if (save) {
            return id;
        }
        return 0L;
    }
}

线上环境报例如:BatchUpdateException: Duplicate entry ‘1531683498452185090’ for key ‘PRIMARY’ 插入主键冲突问题。

分析代码,定位到雪花算法生成id时出现了问题
首先排除时钟回退的情况,因为在5.3.1版本如果服务器时间有问题(时钟后退) 直接报错。

1单机

排除单机情况下出现id重复问题,SNOWFLAKE 是单例的,并且生成id的方法被synchronized修饰。

2集群环境下

需要手动设置dataCenterId 和 workerId值,不同机器相同时间戳要想保证生成的id不重复,那么dataCenterId 和workerId的组合必须是唯一的

private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(workerId , dataCenterId );

Mybatis-Plus v3.4.2 雪花算法实现类 Sequence,提供了两种构造方法:无参构造,自动生成 dataCenterId 和 workerId;有参构造,创建 Sequence 时明确指定标识位

Hutool v5.7.9 参照了 Mybatis-Plus dataCenterId 和 workerId 生成方案,提供了默认实现
一起看下 Sequence 的创建默认无参构造,如何生成 dataCenterId 和 workerId

public static long getDataCenterId(long maxDatacenterId) {
    long id = 1L;
    final byte[] mac = NetUtil.getLocalHardwareAddress();
    if (null != mac) {
        id = ((0x000000FF & (long) mac[mac.length - 2])
                | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6;
        id = id % (maxDatacenterId + 1);
    }
 
    return id;
}

入参 maxDatacenterId 是一个固定值,代表数据中心 ID 最大值,默认值 31

为什么最大值要是 31?因为 5bit 的二进制最大是 11111,对应十进制数值 31

获取 dataCenterId 时存在两种情况,一种是网络接口为空,默认取 1L;另一种不为空,通过 Mac 地址获取 dataCenterId

可以得知,dataCenterId 的取值与 Mac 地址有关

接下来再看看 workerId

public static long getWorkerId(long datacenterId, long maxWorkerId) {
    final StringBuilder mpid = new StringBuilder();
    mpid.append(datacenterId);
    try {
        mpid.append(RuntimeUtil.getPid());
    } catch (UtilException igonre) {
        //ignore
    }
    return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1);
}

入参 maxWorkderId 也是一个固定值,代表工作机器 ID 最大值,默认值 31;datacenterId 取自上述的 getDatacenterId 方法

name 变量值为 PID@IP,所以 name 需要根据 @ 分割并获取下标 0,得到 PID

通过 MAC + PID 的 hashcode 获取16个低位,进行运算,最终得到 workerId
分配标识位
Mybatis-Plus 标识位的获取依赖 Mac 地址和进程 PID,虽然能做到尽量不重复,但仍有小几率

当然了我们也可以自己实现生成workerId、datacenterId的策略
如下,但并未测试过

@Configuration
public class SnowFlakeIdConfig {

    @Bean
    public SnowFlakeIdUtil propertyConfigurer() {
        return new SnowFlakeIdUtil(getWorkId(), getDataCenterId(), 10);
    }


    /**
     * workId使用IP生成
     * @return workId
     */
    private static Long getWorkId() {
        try {
            String hostAddress = Inet4Address.getLocalHost().getHostAddress();
            int[] ints = StringUtils.toCodePoints(hostAddress);
            int sums = 0;
            for (int b : ints) {
                sums = sums + b;
            }
            return (long) (sums % 32);
        }
        catch (UnknownHostException e) {
            // 失败就随机
            return RandomUtils.nextLong(0, 31);
        }
    }


    /**
     * dataCenterId使用hostName生成
     * @return dataCenterId
     */
    private static Long getDataCenterId() {
        try {
            String hostName = SystemUtils.getHostName();
            int[] ints = StringUtils.toCodePoints(hostName);
            int sums = 0;
            for (int i: ints) {
                sums = sums + i;
            }
            return (long) (sums % 32);
        }
        catch (Exception e) {
            // 失败就随机
            return RandomUtils.nextLong(0, 31);
        }
    }
}

很显然这些方法都依赖于获取ip 等信息,比如ip并非连续,甚至获取不到ip等信息时,还是有可能出现id重复问题

3docker容器

就比如在docker容器中,一般ip都是随机的,并且未经过设置还无法获得ip信息。
docker容器和宿主机环境是隔离的,但是可以在启动docker容器时将宿主机的主机名以环境变量的形式传入,代码在容器中获取该值即可。

这里采用另一种方法,我们可以手动设置workid生成规则,并存到redis中。
这里只设置了workId,保证workId和dataCenterId的组合不重复就可以。

workId的生成是系统每次启动,第一次获取Snowflake 对象时才会进行,

public class IdUtils {
    private static StringRedisTemplate stringRedisTemplate = ApplicationContextHolder.getBean(StringRedisTemplate.class);
    private static String SNOWFLAKE_WORKID = "snowflake:workid";
    private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(getWorkerId(SNOWFLAKE_WORKID), 1);


    public static Long getNextId() {
        return SNOWFLAKE.nextId();
    }


    /**
     * 容器环境生成workid 并redis缓存
     * @param key
     * @return
     */
    public static Long getWorkerId(String key) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/redis_worker_id.lua")));
        redisScript.setResultType(Long.class);
        return stringRedisTemplate.execute(redisScript, Collections.singletonList(key));
    }
 }

ApplicationContext对象的获取 ,解决使用注解获取不到bean的问题

@Component
public class ApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        ApplicationContextHolder.applicationContext = applicationContext;
    }

    /**
     * 全局的applicationContext对象
     * @return applicationContext
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String beanName) {
        return (T) applicationContext.getBean(beanName);
    }

    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

}

lua脚本 redis_worker_id.lua
workId初始为0 ,每次获取后+1,知道获取到1023后重置为0
为什么上限是1024呢,因为workId默认占5bit

local isExist = redis.call('exists', KEYS[1])
if isExist == 1
then
    local workerId = redis.call('get', KEYS[1])
    workerId = (workerId + 1) % 1024
    redis.call('set', KEYS[1], workerId)
    return workerId
else
    redis.call('set', KEYS[1], 0)
    return 0
end

测试

使用nginx 端口8080

        location /api {  
            default_type  application/json;
            #internal;  
            keepalive_timeout   30s;  
            keepalive_requests  1000;  
            #支持keep-alive  
            proxy_http_version 1.1;  
            rewrite /api(/.*) $1 break;  
            proxy_pass_request_headers on;
            #more_clear_input_headers Accept-Encoding;  
            proxy_next_upstream error timeout;  
            #proxy_pass http://127.0.0.1:8081;
            proxy_pass http://backend;
        }
    }

    upstream backend {
        server 127.0.0.1:8081 max_fails=5 fail_timeout=10s weight=1;
        server 127.0.0.1:8082 max_fails=5 fail_timeout=10s weight=1;
    }  

代理访问8081、8082两个项目
将自己的项目端口号设置为8081,并复制Copy Configuration ,VM options设置

-Dserver.port=8082
这样启动nginx8080,项目8081、8082
然后使用Jmeter进行压测,比如1000个线程 循环10次进行插入数据库
访问路径
在这里插入图片描述
不再出现id重复问题

参考
https://blog.csdn.net/weixin_36586120/article/details/118018414

https://www.cnblogs.com/hzzjj/p/15117771.html
https://blog.csdn.net/nickDaDa/article/details/89357667

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/83971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

10分钟数仓实战之kettle整合Hadoop

1.写在前面 很多朋友在做数仓的ETL的动作的时候&#xff0c;还是喜欢比较易上手的kettle 前面章节有介绍过安装kettle&#xff0c;可以参考 ETL工具--安装kettle_老码试途的博客-CSDN博客_spoon.bat 安装 kettle在Windows系统中对数据的转换、表和文件的转换等&#xff0c;…

Blender 3D环境场景创建教程

Blender 3D环境场景创建教程 学习 Blender 3.2&#xff0c;探索几何节点并创建美妙的 3D 环境 课程英文名&#xff1a;Creating 3D Environments in Blender 2.81 by Rob Tuytel (2019) 此视频教程共8.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印&#xff0c;…

腾讯云从业者基础认证完整笔记

腾讯云从业者基础认证完整笔记 就考这些&#xff0c;干就完事儿了&#xff01;不要介意图多哟&#xff0c;ppt能更好的表达意思呀 一、云计算基础 1.1 数据中心 一般企业要么自建数据中心EDC&#xff0c;EDC分层如下&#xff1a; 要么租用或者托管也就是IDC如下&#xff…

ZYNQ之FPGA学习----EEPROM读写测试实验

1 EEPROM简介 EEPROM (Electrically Erasable Progammable Read Only Memory&#xff0c;E2PROM)即电可擦除可编程只读存储器&#xff0c;是一种常用的非易失性存储器(掉电数据不丢失)。ZYNQ开发板上使用的是AT24C64&#xff0c;通过IIC协议实现读写操作。IIC通信协议基础知识…

Oracle 11g---基于CentOS7

Oracle 11g安装教程 以下步骤基于网络配置完成&#xff0c;并且能连接xshell和xftp工具 文章目录Oracle 11g安装教程1.将oracle压缩包拷贝到安装机器&#xff0c;指定目录中2.安装依赖包3.验证依赖包4.创建oracle用户5.创建oradata目录,解压oracle安装6.修改系统配置参数7.创建…

2023年开始当年授权或转让的知识产权申报高新将不再认可。

前段时间&#xff0c;由国家科技部火炬中心组织全国高新技术企业管理机构召开会议&#xff0c;会议宣导要求加强企业知识产权管理&#xff0c;强调对当年授权或转让的专利&#xff0c;用来申报当年高新将不再认可。 、从多省市反馈的消息显示部分省市执行了该政策。虽然广东暂…

Java 2022圣诞树+2023元旦倒计时打包一起领走

2022最后一个月充满了期待&#xff0c;平安夜、圣诞节、元旦节&#xff1b;2023年也是一个早年&#xff0c;因此关于程序方面的浪漫&#xff0c;大家应该趁早准备。下面我将分享一个元旦的倒计时和圣诞树的绘制核心代码。大家可以依据自身的需求&#xff0c;稍微调整即可用。 …

振弦渗压计怎样安装?振弦式渗压计工作原理

振弦渗压计是一种长期测量混凝土或地基内的孔隙(渗透)水压力&#xff0c;并可同步测量埋设点温度。适用于大坝工程安全监测、尾矿库工程安全监测、各类公路、桥梁、隧洞安全监测、土工建筑物基坑安全监测等。    1、设备介绍 通过不断的生产工艺技术的积累&#xff0c;采用…

vscode给docker内部的的ros工程代码打断点

背景 打断点debug虽然不能直观看到变量在时间轴上的整体变化曲线&#xff0c;但是其针对某一帧问题数据&#xff0c;暂停后一步步单步执行监视每个变量的变化&#xff0c;方便直观的判断每一步逻辑的正确性&#xff0c;即使这个变量结构再复杂也能直接监视&#xff0c;可以准确…

推荐5款压箱底的小工具软件

今天要给大家推荐5款压箱底的宝贝软件了&#xff0c;百度搜索一下就能找到下载链接了。 1.阅读笔记——BookxNote BookxNote 是一款 PDF 和 EPUB 阅读笔记软件&#xff0c;集阅读、笔记、批注、思维导图、划词翻译等于一体&#xff0c;可以边读边记。它的标注功能非常全&…

惊 GitHub首次开源,在国内外都被称为分布式理论+实践的巅峰之作

前言 蓦然回首自己做开发已经十年了&#xff0c;这十年中我获得了很多&#xff0c;技术能力、培训、出国、大公司的经历&#xff0c;还有很多很好的朋友。但再仔细一想&#xff0c;这十年中我至少浪费了五年时间&#xff0c;这五年可以足够让自己成长为一个优秀的程序员&#…

自有服务器(2台)被 kthreaddk木马挖矿解决过程(实操)不重启服务器

第一台服务器&#xff1a; #查看进程和CPU使用情况 top #查找相关联的进程 systemctl status 326858 #查看下所的 端口号和进程&#xff0c;发现有异常端口和进程 netstat -ntpl #杀死关联进程&#xff08;异常进程&#xff09;&#xff0c; kill -9 2900707 #杀死主进程&a…

Redis高可用之主从复制架构(第一部分)

引言 之前的文章 Redis持久化策略AOF、RDB详解及源码分析&#xff0c;我们介绍了Redis中的数据持久化技术&#xff0c;包括 RDB快照 和 AOF日志以及混合持久化 。有了持久化技术&#xff0c;我们就不用担心因Redis所在服务机器宕机&#xff0c;导致数据丢失。但是&#xff0c;…

四阶龙格库塔法求解一次常微分方程组(python实现)

四阶龙格库塔法求解一次常微分方程组一、前言二、RK4求解方程组的要点1. 将方程组转化为RK4求解要求的标准形式2. 注意区分每个方程的独立性三、python实现RK4求解一次常微分方程组1. 使用的方程组2. python代码3. 运行结果一、前言 之前在博客发布了关于使用四阶龙格库塔方法…

字节测试开发最牛教程,全栈Jmeter_性能测试(总结)

Jmeter_性能测试(4)&#xff1a; 性能测试脚本的优化 以PHP论坛为例&#xff1a;http://47.107.178.45/phpwind/ 根据上一篇的性能测试(3&#xff09;的脚本进行优化&#xff1b;见下图&#xff1a; 如上图中&#xff0c;把发帖和回帖的事务添加到随机控制器中&#xff0c;登…

一例cobalt Strike 反射式注入payload的分析

一例cobalt Strike payload 反射式dll注入的分析 QakBot(Qbot)与cobalt Strike恶意流量样本分析 | Demon (ggsec.cn)这篇博客中末尾提到了一个cobastrick的payload&#xff0c;这是一段shellcode&#xff0c;主要功能是的解密出一个dll&#xff0c;采用反射式注入的方式启动这…

EC 中的Keyboard Controller

Keyboard Controller简称KBC,它是EC芯片中一个用于处理Keyboard、Mouse的模块,也可以说,它只是一个通道,因为最后处理数据的还是交给EC 8032处理器去处理。KBC只处理挂在EC PS/2接口上的设备,假如接了个usb键盘或鼠标,那可不关它的事。PS/2设备只有两种,即Keyboard和Mou…

React 的设计理念(React 哲学)

文章目录React 的设计理念 的理解解决 CPU 瓶颈解决 IO 瓶颈React 的设计理念 的理解 从 React 官网中的 React 哲学文档中&#xff0c;可以看出 React 目的是实现快速响应 影响快速响应的因素&#xff1a;计算能力和网络延迟&#xff0c;即 CPU 和 IO 的瓶颈 解决 CPU 瓶颈 …

再见 ETH India 2022 建设者们 让我们一起回顾这个美好的建设周

很难超越的1700名黑客马拉松比赛&#xff0c;但是以太坊社区出现并打破了ETH India 2022 的新记录。来自321个城市的2000名与会者在短短的一个周内构建并部署了多达459个项目到以太坊生态系统中。你可能错过了过去一周发生的一切&#xff0c;但幸运的是&#xff0c;我们收集了所…

智能设备带来全新体验,打造未来智能生活

随着科技的快速发展&#xff0c;我们的生活变得越来越智能化&#xff0c;近年来智能设备已经遍布我们生活的各个领域&#xff0c;推动了生产能力和质量&#xff0c;给人们的生活带来了极大的便利。智能设备的出现和发展是时代进步的必然产物&#xff0c;高效、安全、准确性高&a…