分布式ID重复问题

Jimmy Lee

技术分享|Apr 16, 2021|Last edited: 2022-7-21|
icon
Update time
Jul 21, 2022 02:59 PM
Internal status
password

背景

线上存在数据表主键重复的错误
通过阿里云日志搜索以下关键词可以查询到
"Duplicate" and "primary"
日志例子
log:2021-04-22 13:19:31,896 ERROR [http-nio2-8088-exec-100] [ac140db2-knscphtx-437669] [steam-trade-boot] c.x.s.t.s.b.i.TradeOrderLogServiceImpl - 插入日志冲突 org.springframework.dao.DuplicateKeyException: ### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '859225024379092992' for key 'PRIMARY' ### The error may exist in com/xingchao/steam/trade/dal/mapper/TradeOrderLogMapper.java (best guess) ### The error may involve com.xingchao.steam.trade.dal.mapper.TradeOrderLogMapper.insertSelective-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO trade_order_log ( id,order_asset_id,type,before_status,after_status,event,content,create_time,update_time ) VALUES( ?,?,?,?,?,?,?,?,? ) ### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '859225024379092992' for key 'PRIMARY'

代码例子

//项目中所有主键使用的SnowFlakeUtil这个组件生成id logDO.setId(SnowFlakeUtil.getId()); tradeOrderLogMapper.insertSelective(logDO);

表现

针对这种频繁插入的情况,容易出现id重复的问题,与当初预想的不一样

分析

分布式id采用的twitter开源的snowflake算法,其本身是作为一个id服务集群运行的,其中关键的配置dataCenterId,workerId,需要在不同数据中心,不同的机器设置成不一样的
但是这种方案,一是需要一些额外的资源来运行此服务,我们不想耗费这个资源,二是需要远程调用,存在性能问题,于是采用的本机生成的方案
本地生成的方案关键在于如何设置这两个参数,我们看之前的实现
// 生成worker_id        long workId;        try {            StringBuilder sbInternet = new StringBuilder();            Enumeration<NetworkInterface> enumInter = NetworkInterface.getNetworkInterfaces();            while (enumInter.hasMoreElements()) {                sbInternet.append(enumInter.nextElement().toString());           }            int machinePiece = sbInternet.toString().hashCode() << 16;            workId = (long) (machinePiece % 32);       } catch (Exception e) {            // 如果获取失败,则使用随机数备用            workId = RandomUtils.nextLong(0, 31);       }        // 生成data_centerId        int processPiece = ManagementFactory.getRuntimeMXBean().getName().hashCode() & 0xFFFF;        long dataCenterId = (long) (processPiece % 32);
看代码意思是其中workId想用网卡信息来计算,datacenterId想用jvm管理器的对象来计算
bug1:
String str="wejwjeuwewewje"; int hashCode=str.hashCode(); //hashCode: -418012329 int xx=hashCode<<16; //xx:-1554579456 因为左移了16位,等于乘以2^16, int mod = xx % 32; //mod:0 //必然可以被32整除,余数永远为0
bug2:
StringBuilder sbInternet = new StringBuilder();           Enumeration<NetworkInterface> enumInter = NetworkInterface.getNetworkInterfaces();            while (enumInter.hasMoreElements()) {                sbInternet.append(enumInter.nextElement().toString());           }
容器1 / # ifconfig eth0     Link encap:Ethernet HWaddr 02:42:AC:11:00:02         inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0         UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1         RX packets:1348 errors:0 dropped:0 overruns:0 frame:0         TX packets:1686 errors:0 dropped:0 overruns:0 carrier:0         collisions:0 txqueuelen:0         RX bytes:218719 (213.5 KiB) TX bytes:589400 (575.5 KiB) lo       Link encap:Local Loopback         inet addr:127.0.0.1 Mask:255.0.0.0         UP LOOPBACK RUNNING MTU:65536 Metric:1         RX packets:0 errors:0 dropped:0 overruns:0 frame:0         TX packets:0 errors:0 dropped:0 overruns:0 carrier:0         collisions:0 txqueuelen:1000         RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) 容器2 /# ifconfig eth0     Link encap:Ethernet HWaddr 02:42:AC:11:00:03         inet addr:172.17.0.3 Bcast:172.17.255.255 Mask:255.255.0.0         UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1         RX packets:776 errors:0 dropped:0 overruns:0 frame:0         TX packets:959 errors:0 dropped:0 overruns:0 carrier:0         collisions:0 txqueuelen:0         RX bytes:128968 (125.9 KiB) TX bytes:322602 (315.0 KiB) lo       Link encap:Local Loopback         inet addr:127.0.0.1 Mask:255.0.0.0         UP LOOPBACK RUNNING MTU:65536 Metric:1         RX packets:0 errors:0 dropped:0 overruns:0 frame:0         TX packets:0 errors:0 dropped:0 overruns:0 carrier:0         collisions:0 txqueuelen:1000         RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
因为我们的应用在docker中运行,因此以上代码取到的网卡名永远一样,所以hashcode也永远一样,就算修复了第一个问题,取模的结果也一样
以上问题导致workId永远为0,造成重复的可能性大大加大
修改成如下代码
// 生成worker_id        long workId;        try {            StringBuilder sbInternet = new StringBuilder();            Enumeration<NetworkInterface> enumInter = NetworkInterface.getNetworkInterfaces();            //改成取硬件地址,也就是mac,得到的字符串肯定不一样            while (enumInter.hasMoreElements()) {               sbInternet.append(Arrays.toString(enumInter.nextElement().getHardwareAddress()));           }            //hashcode可能为负数,与运算去除符号            int machinePiece = sbInternet.toString().hashCode() & Integer.MAX_VALUE;            workId = machinePiece % 32;       } catch (Exception e) {            // 如果获取失败,则使用随机数备用            workId = RandomUtils.nextLong(0, 31);       }
 

开始订阅我的关于终生学习, 生产力以及知识管理的文章. 订阅后, 您将收到我的精选文章.

©2014-2024 Jimmy Lee. All rights reserved. 公众号: 技术管理方法论
Powered By My Lovely Children