使用 zebra 对数据库表进行水平拆分

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/aimeimeiTS/article/details/84871364

zebra 是美团点评开发的数据库访问层中间件,代码维护在 GitHub:Meituan-Dianping/Zebra

Zebra是一个基于JDBC API协议上开发出的高可用、高性能的数据库访问层解决方案,是美团点评内部使用的数据库访问层中间件。具有以下的功能点:

  • 配置集中管理,动态刷新
  • 支持读写分离、分库分表
  • 丰富的监控信息在CAT上展现
  • 异步化数据库请求,多数据源支持

zebra 的 QuickStart 提供了完整的使用说明文档,这里就不再赘述用法,而是直接提供一个使用 zebra 进行数据库分表的 demo。

数据库目标表结构

数据库表为签到表(sign_in),表结构如下:
签到表用于记录用户签到数据,CUSTOMER_ID 为用户 id,其余字段是为了 demo 测试而建,无特殊含义。

image.png

主维度选取

选择签到表的 CUSTOMER_ID 作为维度。

选取维度时需要考虑下面的情况:
在 sql 语句中,顾客 CUSTOMER_ID 是在 CRUD 操作中作为查询目标属性和条件属性中出现次数最多的字段,这样能满足 zebra 要求中的维度必须出现在 sql 语句中的限制;同时很多查询,检索的 sql 都是基于同一顾客进行的,方便业务逻辑变更;此外,顾客 id 在签到表中与主键直接相关,一条签到记录必然有一个顾客 id ,根据顾客 id 能更快的定位签到记录所在范围。

路由规则

<?xml version="1.0" encoding="UTF-8"?>
<router-rule>
    <table-shard-rule table="sign_in" generatedPK="id">
        <shard-dimension
                dbRule="#customer_id# * 0"
                dbIndexes="db"
                tbRule="0 + ((4 - 1) &amp; (crc32(#customer_id#) ^ (crc32(#customer_id#) &gt;&gt;&gt; 16)))"
                tbSuffix="everydb:[0,4]"
                isMaster="true">
        </shard-dimension>

    </table-shard-rule>
</router-rule>

通过维度和路由规则定位到对应表的过程,在针对签到表的 CRUD 操作中,通过维度 CUSTOMER_ID 字段及其值,经路由规则计算后能定位到对应表。

路由规则说明

数据库的路由规则

dbRule="#CUSTOMER_ID# * 0"
单库,无需定义规则,指出 [维度] 即可。

数据库名后缀

dbIndexes=“db”
与zebra配置中的ShardDataSource数据源的key相同即可。

表的路由规则

参考 jdk8 HashMap 原理,路由规则思路如下:

  1. 主表:sign_in
  2. 维度:key
  3. 表下标偏移:offset
  4. 表数量:len
  5. 散列值:hash = crc32(key)
  6. 扰动函数:ha = (hash ^ (hash >>> 16))
  7. 取模运算:in = (len - 1) & ha
  8. 映射到的表下标:index = offset + in
  9. tbRule: offset + ((len - 1) & (crc32(key) ^ (crc32(key) >>> 16)))

如:offset = 3,len = 2,key 为 #customer_id#
tbRule = “3 + ((2 - 1) & (crc32(#customer_id#) ^ ((crc32(#customer_id#) >>> 16)))”
在表sign_in3,sign_in4中路由。

注意:

  • tbRule 格式必须严格相同
  • len只能为2的整次幂,否则路由分布不均匀,表现形式之一为插入大量数据,路由到的各个表中新增数据量分别不均匀。
  • offset 不能小于 0,即最小为 0

说明:

  • 扰动函数:使碰撞(hash冲突)更平均,即 hash 映射到的 index 在可选 index 中被选中的概率趋于相同。
  • 取模运算:高位全部归零,只保留低位,用作下标访问
  • 表数量取2的整数次幂:长度减1,便于取模。
表名后缀

tbSuffix=“everydb:[0,999]”
everydb:[a,b]:a始终为0,b不小于实际表的最大index即可。

主维度

isMaster=“true”
只有一个维度

测试

部分测试 sql 如下:

    <!--记得将【维度】明确表示-->
    <insert id="insert" parameterType="SignInEntity">
        insert into sign_in (id, customer_id, date, current_sign_in_store_id, type, create_eid, create_date)
            value (#{id}, #{customerId}, #{date}, #{currentSignInStoreId}, #{type}, #{createEid}, now())
    </insert>

    <select id="listByCustomerId" resultType="SignInEntity">
        select *
        from sign_in
        where customer_id = #{value};
    </select>

    <!--Select、Update或者Delete,该SQL对所有的库和表进行执行,因为没有带维度-->
    <select id="get" resultType="SignInEntity">
        select *
        from sign_in
        where id = #{value}
    </select>

指定 CustomerId ,执行 sql 时将被路由到具体的分表中。

    private void testInsert() {

        SignInEntity entity = new SignInEntity();
        Random r = new Random();
        // 路由规则将在三张表中插入数据,数据将平均分布
        for (int i = 1; i <= 10 * 10000; i++) {
            entity.setId(i);
            entity.setType(r.nextInt(3));
            entity.setCustomerId(r.nextInt(9999));
            entity.setCurrentSignInStoreId(r.nextInt(9999));
            entity.setCreateEid(r.nextInt(9999));
            entity.setDate(new Timestamp(System.currentTimeMillis()));
            signInDao.insert(entity);
        }

    }

完整代码上传 GitHub,你可以在 这里 找到

参考文章

JDK 源码中 HashMap 的 hash 方法原理是什么
java 集合 3 - HashMap
数据迁移测试实施方案

猜你喜欢

转载自blog.csdn.net/aimeimeiTS/article/details/84871364