使用 zebra 对数据库表进行水平拆分
zebra 是美团点评开发的数据库访问层中间件,代码维护在 GitHub:Meituan-Dianping/Zebra
Zebra是一个基于JDBC API协议上开发出的高可用、高性能的数据库访问层解决方案,是美团点评内部使用的数据库访问层中间件。具有以下的功能点:
- 配置集中管理,动态刷新
- 支持读写分离、分库分表
- 丰富的监控信息在CAT上展现
- 异步化数据库请求,多数据源支持
zebra 的 QuickStart 提供了完整的使用说明文档,这里就不再赘述用法,而是直接提供一个使用 zebra 进行数据库分表的 demo。
数据库目标表结构
数据库表为签到表(sign_in),表结构如下:
签到表用于记录用户签到数据,CUSTOMER_ID 为用户 id,其余字段是为了 demo 测试而建,无特殊含义。
主维度选取
选择签到表的 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) & (crc32(#customer_id#) ^ (crc32(#customer_id#) >>> 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 原理,路由规则思路如下:
- 主表:sign_in
- 维度:key
- 表下标偏移:offset
- 表数量:len
- 散列值:hash = crc32(key)
- 扰动函数:ha = (hash ^ (hash >>> 16))
- 取模运算:in = (len - 1) & ha
- 映射到的表下标:index = offset + in
- 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
数据迁移测试实施方案