背景
线上存在数据表主键重复的错误
通过阿里云日志搜索以下关键词可以查询到
“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);
}