当前位置: 首页 > news >正文

这样使用雪花算法被客户喷了!

👉这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料:

  • 《项目实战(视频)》:从书中学,往事上“练”

  • 《互联网高频面试题》:面朝简历学习,春暖花开

  • 《架构 x 系统设计》:摧枯拉朽,掌控面试高频场景题

  • 《精进 Java 学习指南》:系统学习,互联网主流技术栈

  • 《必读 Java 源码专栏》:知其然,知其所以然

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本

来源:juejin.cn/post/7474427694630436914

  • 代码分析

  • 潜在问题:高并发下序列号重复的原因

    • 1.序列号重置机制问题

    • 2.CAS 操作的竞争条件

    • 3.时间窗口

  • 改进方案:避免重复生成序列号

    • 1. 引入时间戳检查

    • 2. 改用更大范围的序列号

    • 3. 时间窗口重试机制

    • 4. 使用雪花算法

  • 啥是雪花算法?

    • 共同点:模仿雪花算法的元素

    • 差异:与雪花算法的本质区别

    • 总结


项目里面有个日志流水号,是通过雪花算法实现的,客户在评审我们的代码时,说是在高并发的场景下可能会存在序列号重复的情况。

如下,是我的源代码实现逻辑

public class TraceIdGenerator { public static String getInetAddress() { try { Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); InetAddress address = null; while (interfaces.hasMoreElements()) { NetworkInterface ni = interfaces.nextElement(); Enumeration<InetAddress> addresses = ni.getInetAddresses(); while (addresses.hasMoreElements()) { address = addresses.nextElement(); if (!address.isLoopbackAddress() && address.getHostAddress().indexOf(":") == -1) { return address.getHostAddress(); } } } returnnull; } catch (Throwable t) { returnnull; } } privatestatic String IP_16 = "ffffffff"; privatestatic String P_ID_CACHE; privatestatic AtomicInteger count = new AtomicInteger(1000); static { try { String ipAddress = getInetAddress(); if (ipAddress != null) { IP_16 = getIP_16(ipAddress); } } catch (Throwable e) { /* * empty catch block */ } } /** * This method can be a better way under JDK9, but in the current JDK version, it can only be implemented in this way. * <p> * In Mac OS , JDK6,JDK7,JDK8 ,it's OK * In Linux OS,JDK6,JDK7,JDK8 ,it's OK * * @return Process ID */ public static String getPID() { //check pid is cached if (P_ID_CACHE != null) { return P_ID_CACHE; } try { String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); if (StringUtils.isBlank(processName)) { return StringUtils.EMPTY; } String[] processSplitName = processName.split("@"); if (processSplitName.length == 0) { return StringUtils.EMPTY; } String pid = processSplitName[0]; if (StringUtils.isBlank(pid)) { return StringUtils.EMPTY; } P_ID_CACHE = pid; return pid; } catch (Throwable e) { //ignore } return StringUtils.EMPTY; } private static String getTraceId(String ip, long timestamp, int nextId) { StringBuilder appender = new StringBuilder(30); appender.append(ip).append(timestamp).append(nextId).append(getPID()); return appender.toString(); } public static String generate() { try { return getTraceId(IP_16, System.currentTimeMillis(), getNextId()); } catch (Throwable e) { return UUID.fastUUID().toString(); } } private static String getIP_16(String ip) { String[] ips = ip.split("\\."); StringBuilder sb = new StringBuilder(); for (String column : ips) { String hex = Integer.toHexString(Integer.parseInt(column)); if (hex.length() == 1) { sb.append('0').append(hex); } else { sb.append(hex); } } return sb.toString(); } private static int getNextId() { for (; ; ) { int current = count.get(); int next = (current > 9000) ? 1000 : current + 1; if (count.compareAndSet(current, next)) { return next; } } } public static void main(String[] args) { String generate = generate(); System.out.println(generate); } }

其中getNextId()方法利用AtomicInteger来生成序列号。看似使用了 CAS(比较并交换)机制来确保线程安全,但在高并发场景下仍然可能存在 序列号重复 的问题。下面我将详细分析这个问题的根本原因。

代码分析

private static int getNextId() { for (;;) { int current = count.get(); // 获取当前序列号 int next = (current > 9000) ? 1000 : current + 1; // 如果序列号超过9000,重置为1000 if (count.compareAndSet(current, next)) { // 如果当前值等于current,才会更新为next return next; // 返回生成的序列号 } } }

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

潜在问题:高并发下序列号重复的原因

1.序列号重置机制问题

代码中定义了序列号的范围为 1000 到 9000,一旦当前的 count 超过 9000,它就会重置为 1000。问题的关键在于,如果多个线程在同一毫秒内高频率地调用该方法,并且当前的 count 达到了 9000,那么它们会尝试重置 count 为 1000,导致重复的序列号生成。

具体场景如下:假设当前 count 为 9000,多个线程几乎同时执行getNextId()方法,它们会都看到 count 为 9000,然后根据条件生成next = 1000,然后通过compareAndSet更新 count。由于compareAndSet依赖于比较当前值 current 和 count,当多个线程几乎同时读取到current = 9000时,可能会成功更新 count 为 1000 多次,导致多个线程生成了相同的序列号 1000。

这种情况发生的概率随着并发量的增加而增加,尤其是在高并发环境下,线程调度不可控,可能会出现多个线程的 CAS 成功,导致生成重复的序列号。

2.CAS 操作的竞争条件

AtomicInteger使用了 CAS 来保证对 count 的原子性更新,但它并没有完全消除 并发更新的竞态条件。即多个线程读取到相同的 current 值后,如果它们同时调用compareAndSet(current, next),其中某些线程的操作会成功,而其他线程则会失败(因为 count 的值已被更新),这并不能确保每个线程都能生成唯一的序列号。

例如:线程 A 和线程 B 同时读取 count 的值,发现是 9000,然后都试图将 count 更新为 1000。因为compareAndSet的成功与否取决于 count 是否与 current 相等,而 count 在多线程并发情况下可能会变成不一致的状态,导致多个线程成功更新 count,进而生成重复的序列号。

3.时间窗口

如果系统的请求频率非常高,并且调用频繁,多个线程可能会在极短的时间内(比如同一毫秒内)频繁访问getNextId(),即使compareAndSet有原子性保障,但由于重置逻辑非常简单,多个线程会同时重置序列号并生成相同的 1000。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

改进方案:避免重复生成序列号

为了解决这一问题,我们可以通过以下几种方法来优化getNextId()

1. 引入时间戳检查

在生成序列号时,可以通过检查当前的时间戳(System.currentTimeMillis())来区分不同的毫秒,从而避免在同一毫秒内重置序列号。

private staticlong lastTimestamp = -1L; privatestatic AtomicInteger count = new AtomicInteger(1000); private static int getNextId() { long currentTime = System.currentTimeMillis(); // 如果时间戳发生变化,则重置序列号 if (currentTime != lastTimestamp) { synchronized (TraceIdGenerator.class) { if (currentTime != lastTimestamp) { count.set(1000); lastTimestamp = currentTime; } } } for (;;) { int current = count.get(); int next = (current > 9000) ? 1000 : current + 1; if (count.compareAndSet(current, next)) { return next; } } }

通过 时间戳检查,每次进入新的一毫秒时,序列号会重置为 1000,这样能够确保同一毫秒内不会因为重置操作导致序列号重复。

2. 改用更大范围的序列号

如果是为了处理更高的并发,可以考虑扩展序列号的范围,例如使用更大的序列号区间(例如 0 到 100,000),以减少在同一毫秒内序列号用尽的概率。这样,即便在高并发场景下,也能有更多的序列号可用。

3. 时间窗口重试机制

如果在同一毫秒内序列号用尽,可以考虑引入一个等待机制,在同一毫秒内序列号用完时,线程会等待到下一个毫秒再继续生成 ID。

4. 使用雪花算法

如果业务需要保证全局唯一性和高并发性能,可以考虑使用雪花算法(Snowflake),它不仅能保证 ID 唯一性,还能够处理高并发和分布式场景,避免重复序列号的问题。

啥是雪花算法?

可以说我代码中的算法在某些方面模仿了雪花算法,但并不是一个完全的雪花算法实现。它借用了雪花算法的 时间戳 + 序列号递增 的概念,但在细节和全局唯一性的保障上,存在明显差异。以下是你代码和雪花算法的对比:

共同点:模仿雪花算法的元素

时间戳:你代码中通过System.currentTimeMillis()获取当前时间戳,作为生成 ID 的一部分,这与雪花算法中的时间戳部分是相似的。

序列号递增:每次生成一个 ID,序列号递增(例如,当 count 达到 9000 时重置为 1000),这也是雪花算法中常见的做法。雪花算法中的序列号部分是 12 位,能够在同一毫秒内生成大量唯一 ID。

差异:与雪花算法的本质区别

1.全局唯一性保障不足:

雪花算法通过机器 ID 和数据中心 ID 来保证 ID 在分布式环境中的全局唯一性。每台机器都有一个独立的机器 ID,这在多节点(分布式)环境下非常重要。

你的代码并没有涉及到机器 ID 或者数据中心 ID。它假设整个系统只有一个节点(或单机模式),这就意味着它并没有解决 分布式系统中的 ID 唯一性 问题。如果在多个服务或应用实例中同时使用这种生成方式,就可能会出现 ID 冲突。

2.时间戳粒度和范围:

雪花算法的时间戳部分是 41 位,能够支持较长的时间范围(大约 69 年),并且粒度是毫秒级的,这样能够确保长时间运行而不会产生重复 ID。

你的算法使用的是System.currentTimeMillis(),其粒度也是毫秒级别,但它没有雪花算法那样精确的控制和范围。在同一毫秒内,由于你只使用了简单的序列号递增,可能会出现序列号冲突的情况,尤其是在高并发场景下。

3.重置机制:

雪花算法中的序列号是在每毫秒内递增,直到最大值为止。如果超出最大值,系统会等待下一毫秒,确保序列号不会重复。雪花算法本身设计时避免了序列号重置的问题。

你的代码在 count 达到 9000 时会 重置为 1000,这就可能导致 在高并发场景下,多个线程生成相同的 ID,从而引发 ID 冲突。

4.ID 结构:

雪花算法使用了 64 位的 ID,包含了符号位、时间戳、机器 ID、数据中心 ID 和序列号,结构清晰且易于扩展。

你的代码生成的 ID 并没有采用类似雪花算法的结构化设计,而是直接拼接了 IP 地址、时间戳 和序列号,这样的生成方式不够灵活,无法很好地适应分布式环境。

总结

我的代码确实借鉴了雪花算法的一些思想,尤其是时间戳和序列号递增的部分,但它并没有实现雪花算法的完整机制(如机器 ID、数据中心 ID、分布式保证等)。因此,它只能算是对雪花算法的一种简单模仿,适用于单机场景下的 ID 生成,但在 分布式系统 或 高并发 环境中,存在生成重复 ID 的风险。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。 谢谢支持哟 (*^__^*)
http://www.jsqmd.com/news/466543/

相关文章:

  • 大润发购物卡回收别乱选!这波操作让闲置卡秒变现金流 - 京顺回收
  • 【SSP之路-5-重要节点】LFU
  • PageHelper 解析及实现原理
  • 对比VBA学习Python,让办公更自动化!
  • 很多人卡在这一步:OpenClaw不会安装?这个一键版解决了
  • 【Linux】进程 PCB、task_struct、fork初识
  • 基于二进制的遗传算法的考虑排放目标和输电损耗的经济调度研究(Python代码实现)
  • 扫地机机器人研发岗深度解析与技术指南
  • 140个企业级实战场景剖析以及AI大模型项目实战
  • 函数式编程思想
  • 2026钻床市场热门:这些工厂钻床受追捧,目前优质的钻床品牌技术引领与行业解决方案解析 - 品牌推荐师
  • 汇源全屋定制作为全屋定制专业制造商,价格大概多少钱? - 工业推荐榜
  • 基于改进粒子群算法的含碳捕集微网多时间尺度低碳经济调度(Matlab代码实现)
  • Flutter 三方库 system_resources_2 的鸿蒙化适配指南 - 实时监控鸿蒙端侧 CPU 负载、内存占用与系统资源动态感知
  • 星焰家居这个不锈钢全屋定制厂商品牌靠不靠谱,值得推荐吗? - myqiye
  • 2026年热门的CNC 精密压铸加工公司推荐:医疗设备精密压铸加工/智能家居精密压铸加工采购指南厂家怎么选 - 行业平台推荐
  • # 发散创新:WebHID 在浏览器端实现外设通信的全新实践 在现代Web 开
  • 2026年评价高的储能弹簧工厂推荐:耐腐蚀弹簧/小家电电磁阀弹簧/高压直流继电器弹簧精选厂家推荐 - 行业平台推荐
  • Python开发英语记忆单词软件 - 优化
  • FFMpeg + WebSocket + JSMpeg 搭建低延迟视频系统(总览篇)
  • 2026年01月深圳CE:加速寿命试验/合规类/国内外认证/机构类/测试服务/温度老化试验/电子电气检测/腐蚀试验/选择指南 - 优质品牌商家
  • 2026国内小白纹绣培训重实操机构推荐榜:野生眉学校、零基础学纹眉、零基础小白、零基础纹眉学校、零结痂雾眉、韩式定妆学校选择指南 - 优质品牌商家
  • PAT 乙级 1078
  • 谁懂啊!OpenClaw(小龙虾)爆火不是没道理
  • Python基于flask的博客系统设计与实现
  • 总结AI蓝牙音箱生产厂,国内靠谱厂家Top10有哪些? - 工业品网
  • Flutter 三方库 shelf_cors_headers 的鸿蒙化适配指南 - 实现具备跨域安全访问策略的服务端拦截器、支持端侧微服务网关与分布式请求治理实战
  • 聊聊扬州月子中心按需定制,哪家品牌靠谱又有高性价比? - 工业设备
  • win11下解决eNSP AR启动40/41错误解决方案
  • Flutter 三方库 health_connector_core 的鸿蒙化适配指南 - 实现具备跨平台标准的数据采集与同步架构、支持端侧健康指标建模与设备总线协同实战