关于数据库:破局主键重复问题的坎坷路-京东物流技术团队

37次阅读

共计 5844 个字符,预计需要花费 15 分钟才能阅读完成。

随同着业务的一直倒退,逐步由单库单表向分库分表进行倒退。在这个过程中不可避免的一个问题是确保主键要的唯一性,以便于后续的数据聚合、剖析等等场景的应用。在进行分库分表的解决方案中有多种技术选型,大略分为两大类客户端分库分表、服务端分库分表。例如 Sharding-JDBC、ShardingSphere、MyCat、ShardingSphere-Proxy、Jproxy(京东外部已弃用) 等等。

在这个燥热的夏天,又忽然收到告警,分库分表的主键抵触了,这还能忍?不,坚定不能忍,必须解决掉!前面咱们缓缓道来是如何破局的,如何走了一条坎坷路……

翻山第一步

咱们的零碎应用的是 ShardingSphere 进行分库分表的,大略的配置信息如下:_(出于信息的平安思考,暗藏了局部信息,只保留的局部内容,不要在意这些细节能阐明问题即可)_

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sharding="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd http://shardingsphere.apache.org/schema/shardingsphere/sharding http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd">

    <!-- 数据源 -->
    <bean id="database1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    </bean>
    <bean id="database2" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    </bean>
    <bean id="database3" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    </bean>

    <sharding:inline-strategy id="databaseStrategy" sharding-column="cloum1" algorithm-expression="table1_$->{(Math.abs(cloum1.hashCode()) % 512).intdiv(32) }" />
    <sharding:inline-strategy id="orderNoDatabaseStrategy" sharding-column="cloum2" algorithm-expression="table2_$->{(Math.abs(cloum2.hashCode()) % 512).intdiv(32) }" />
    <sharding:inline-strategy id="businessNoDatabaseStrategy" sharding-column="cloum3" algorithm-expression="table3_$->{(Math.abs(cloum3.hashCode()) % 512).intdiv(32) }" />
    <!-- 主键生成策略 - 雪花算法 -->
    <sharding:key-generator id="idKeyGenerator" type="SNOWFLAKE" column="id" props-ref="snowFlakeProperties"/>

    <sharding:data-source id="dataSource">
        <sharding:sharding-rule data-source-names="database1,database2,database3">
            <sharding:table-rules>

                <sharding:table-rule logic-table="table1"
                                     actual-data-nodes="database1_$->{0..15}.table1_$->{0..31}"
                                     database-strategy-ref="orderNoDatabaseStrategy"
                                     table-strategy-ref="order_waybill_tableStrategy"
                                     key-generator-ref="idKeyGenerator"/>

                <sharding:table-rule logic-table="table2"
                                     actual-data-nodes="database2_$->{0..15}.table2_$->{0..31}"
                                     database-strategy-ref="databaseStrategy"
                                     table-strategy-ref="waybill_contacts_tableStrategy"
                                     key-generator-ref="idKeyGenerator"/>

                <sharding:table-rule logic-table="table3"
                                     actual-data-nodes="database3_$->{0..15}.table3->{0..31}"
                                     database-strategy-ref="databaseStrategy"
                                     table-strategy-ref="waybill_tableStrategy"
                                     key-generator-ref="idKeyGenerator"/>
            </sharding:table-rules>
        </sharding:sharding-rule>
    </sharding:data-source>


    <bean id="sqlSessionFactory" class="com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:spring/mybatis-env-setting.xml"/>
        <property name="mapperLocations" value="classpath*:/mapper/*.xml"/>
    </bean>

</beans>



从下面的配置能够看出配置的是 ”SNOWFLAKE” 主键应用的是雪花算法,雪花算法产生的 ID 的组成总计 64 位,第一位为符号位不必,后 41 位为工夫戳用于区别不同的工夫点,在前面 10 位为 workId 用于区别不同的机器,最初 12 位为 sequence 用于同一时刻同一机器的并发数量。

那接下来就看看咱们本人的零碎是怎么配置的吧, 其中的属性 snowFlakeProperties 配置如下, 其中的 max.vibration.offset 配置示意 sequence 的范畴为 1024。依照失常的了解任何单个机器的配置都很难达到这个并发量,难道是这个值没有失效?

    <sharding:key-generator id="idKeyGenerator" type="SNOWFLAKE" column="id" props-ref="snowFlakeProperties"/>



shardingsphere 中实现获取主键的实现源码如下简述,具体的实现类 org.apache.shardingsphere.core.strategy.keygen.SnowflakeShardingKeyGenerator, 从源码看源码居然一个日志都没有,那让咱们怎么去排查呢?怎么证实咱们的猜测是否正确的呢?囧……

真是败也萧何成也萧何,shardingsphere 是通过 java 的 SPI 的形式进行的主键生成策略的扩大。自定义实现形式如下:实现 org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator 接口,如果本人想要实现应用 SPI 形式进行加载即可,那就让咱们本人加日志吧,走你……

既然都本人写实现了,那日志就该加的都加吧,咱这绝不悭吝这几行日志

批改主键抉择生成策略为本人实现的类 type=”MYSNOWFLAKE”

    <sharding:key-generator id="idKeyGenerator" type="MYSNOWFLAKE" column="id" props-ref="snowFlakeProperties"/>



启动看日志,属性中有 max.vibration.offset=1024 这个属性,居然仍旧拿到的还是默认的值 1,诧异中,细细一瞅,究竟发现了问题,在 getProperty(key) 时如果返回的不是 String 类型那么为 null,进而取值默认值 1。从咱们的系统配置中能够看到系统配置的 int 类型的的 1024,所以取值默认值 1 就说通了。

INFO 2023-08-17 14:07:51.062 2174320.63604.16922524693996408 176557 com.jd.las.waybill.center.config.MySnowflakeShardingKeyGenerator.getMaxVibrationOffset(MySnowflakeShardingKeyGenerator.java:107) -- 抉择自定义的雪花算法获取到的 properties={"max.vibration.offset":1024,"worker.id":"217","max.tolerate.time.difference.milliseconds":"3000"}
INFO 2023-08-17 14:07:51.063 2174320.63604.16922524693996408 176558 com.jd.las.waybill.center.config.MySnowflakeShardingKeyGenerator.getMaxVibrationOffset(MySnowflakeShardingKeyGenerator.java:110) -- 抉择自定义的雪花算法获取到的 getMaxVibrationOffset=1



截止到目前主键反复的问题终于能够解释的通了,因为并发反对的是 0~1 总共 2 个并发,所以在生产零碎中尤其呈现生产波次的时候呈现反复值的可能性是存在的, 而后把 1024 变成字符串批改上线,置信零碎前面应该不会产生此类问题了。

越岭第二步

如果生存总是喜爱跟你开玩笑,逗你玩,那你就配合它笑一笑吧。

当上完线后,过了一段时间发现反复主键的问题居然仍旧存在只是频率少了些, 不迷信呀……

从新梳理思路,进行更具体的日志输入,下单进行验证,将接单落库这坨代码一并都加上日志以及触发雪花算法的生成的 ID 也加上日志

通过日志剖析,又又又发现 ” 灵异事件 ”,10 条插入 SQL,只有两条触发了 shardingsphere 的雪花算法,惊讶的很呀~

查看其余 8 张表落库的 ID 数据如下图,ID(1692562397556875266) 都为 1692 结尾且长度 20 位,而 shardingsphere 产生的 ID(899413419526356993)都为 899 结尾且长度 19 位,很显著这 8 张表的主键不是 shardingsphere 生成的,那是这 20 位的数据哪来的呢???从 ID 上看显著也不是自增产生的主键,又不迷信了……

又是一个深夜……

梳理思路从新在锊,主键相干的除了数据自增长、shardingsphere 配置的雪花还有惟一的一个相干的组件那就是 mybatis,因为我的项目是很早之前的利用了,应用的是 baomidou 的 mybaits 插件,如图是插件的入口,MybatisSqlSessionFactoryBean 实现 FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> 几个 Spring 的接口

baomidou 波及该块问题的源码如下:

如果 GlobalConfig 没有配置 workId 和 DataCenterId 会应用无参结构,默认的 workId

baomidou 的雪花算法和 shardingphere 思路统一有一点点区别在于第 12 位和 22 位有 datacenter<<17|workId<<12 获取,且 datacenter 和 workId 须要在 0~31 之间

不同机器的 Name:

所以又解释了为什么不同机器会呈现雷同的主键问题,然而如果有仔细的同学就会问为啥 10 张表中有 8 张表走的是 baomidou 的雪花算法呢,那是因为 baomidou 会判断保留的入参实体 bean 上是否有 id 字段,是否能匹配上该字段,如果存在则在 baomidou 这层就给赋值了 baomidou 雪花算法生产的 ID,后续就不会再次触发 shardingsphere 中 ID 生成,进而导致该问题。

截止到目前终于又解释通了为什么跨机器会产生雷同的主键问题。

问题的解决形式:

baomidou 配置的过程中指定 workId 和 centerDataId,然而须要确保 centerDataId<<17|workId<<12 确保惟一。类比 shardingphere,借用 shardingphere 中的 12~22 位惟一数,前 5 高位给 (centerDataId<<17), 后 5 低位给 workId<<12;

夜已缄默……

生产环境已上线验证通过

作者:京东物流 王义杰

起源:京东云开发者社区 自猿其说 Tech 转载请注明起源

正文完
 0