Mybatis的分表实战

 

前言:
  以前写代码, 关于mysql的分库分表已被中间件服务所支持, 业务代码涉及的sql已规避了这块. 它对扩展友好, 你也不知道到底他分为多少库, 多少表, 一切都是透明的.
  不过对于小的团队/工作室而言, 可能就没有那么强大的分布式中间件的基础设施支持了, 而当数据库上去的时候, 分库分表就需要客户端client这边去支持维护了. 如何优雅地使用mybatis支持分表, 这就是本文的主题.

 

系列相关文章:
  1. spring+mybatis的多源数据库配置实战 
  参考的博文:
  1. MyBatis拦截器原理探究 
  2. SpringMVC + MyBatis分库分表方案 

  3. 利用Mybatis拦截器对数据库水平分表  

 

mybatis插件机制:
  mybatis支持插件(plugin), 讲得通俗一点就是拦截器(interceptor). 它支持ParameterHandler/StatementHandler/Executor/ResultSetHandler这四个级别进行拦截.
  总体概况为:

  • 拦截参数的处理(ParameterHandler)
  • 拦截Sql语法构建的处理(StatementHandler)
  • 拦截执行器的方法(Executor)
  • 拦截结果集的处理(ResultSetHandler)

  比如sql rewrite, 它属于StatementHandler的阶段. 以分表实践为例, 它可以简单理解为把table名称替换为分表table名称的过程.

 

模拟实战:
  让我们模拟实战一回, 假定我们有个需求, 就是把重要的业务日志数据, 导入到表tb_record中.

1
2
3
4
5
CREATE TABLE `tb_record` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `logs` varchar(128) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

  但是现在随着业务数据暴增, 单表支撑不了这么多数据. 因此决定把tb_record做水平切分, 按天来做切分tb_record_{yyyyMMdd}, 比如2018/07/26这天的数据, 就导入到表tb_record_20180726中.
  之前的mapper接口类如下:

1
2
3
4
5
6
public interface RecordMapper {
 
    @Insert("INSERT INTO tb_record(logs) VALUES(#{logs})")
    int addRecord(@Param("logs") String logs);
 
}

  在不改变代码的前提下, 如何支持分表的无感知实现.

 

代码编写:
  由于mybatis的拦截器是全局的, 因此这边引入特定的注解用于区分目标/非目标对象(数据库表).
  定义分表策略接口和具体的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 分表的策略类
public interface ITableShardStrategy {
 
    String tableShard(String tableName);
 
}
 
// 按天切分的分表策略类
public class DateTableShardStrategy implements ITableShardStrategy {
 
    private static final String DATE_PATTERN = "yyyyMMdd";
 
    @Override
    public String tableShard(String tableName) {
        SimpleDateFormat sdf = new SimpleDateFormat(DATE_PATTERN);
        return tableName + "_" + sdf.format(new Date());
    }
 
}

  定义注解:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {
 
    // 要替换的表名
    String tableName();
 
    // 对应的分表策略类
    Class<? extends ITableShardStrategy> shardStrategy();
 
}

  编写具体的mybatis拦截器实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Intercepts({
        @Signature(
            type = StatementHandler.class,
            method = "prepare",
            args = { Connection.class, Integer.class }
        )
})
public class TableShardInterceptor implements Interceptor {
 
    private static final ReflectorFactory defaultReflectorFactory = new DefaultReflectorFactory();
 
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
 
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = MetaObject.forObject(statementHandler,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                defaultReflectorFactory
        );
 
        MappedStatement mappedStatement = (MappedStatement)
                metaObject.getValue("delegate.mappedStatement");
 
        String id = mappedStatement.getId();
        id = id.substring(0, id.lastIndexOf('.'));
        Class clazz = Class.forName(id);
 
        // 获取TableShard注解
        TableShard tableShard = (TableShard)clazz.getAnnotation(TableShard.class);
        if ( tableShard != null ) {
            String tableName = tableShard.tableName();
            Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();
            ITableShardStrategy strategy = strategyClazz.newInstance();
            String newTableName = strategy.tableShard(tableName);
            // 获取源sql
            String sql = (String)metaObject.getValue("delegate.boundSql.sql");
            // 用新sql代替旧sql, 完成所谓的sql rewrite
            metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableName, newTableName));
        }
 
        // 传递给下一个拦截器处理
        return invocation.proceed();
    }
 
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
 
    @Override
    public void setProperties(Properties properties) {
    }
 
}

  注: 不同mybatis的版本, 具体的api略有出入, 当前mybatis版本为(3.4.6).

  配置plugin标签, 注意要在mybatis-config.xml(mybatis全局属性配置文件)中进行配置

1
2
3
<plugins>
    <plugin interceptor="com.springapp.mvc.mybatis.TableShardInterceptor"></plugin>
</plugins>

  

测试:
  对原来的RecordMapper添加@TableShard注解:

1
2
3
4
5
6
7
@TableShard(tableName = "tb_record", shardStrategy = DateTableShardStrategy.class)
public interface RecordMapper {
 
    @Insert("INSERT INTO tb_record(logs) VALUES(#{logs})")
    int addRecord(@Param("logs") String logs);
 
}

  编写简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:application-context.xml"})
public class RecordMapperTest {
 
    @Resource
    private RecordMapper recordMapper;
 
    @Test
    public void testAddRecord() {
        String logs = "hello lilei";
        recordMapper.addRecord(logs);
    }
 
}

  查看数据库进行数据验证:
  


后记:
  总的来说, mybatis的拦截器给开发者很大的自由度, 像这边的分表实践是很好的例子. 但分表的策略有很多, 很多都是基于特定的维度进行散列, 总觉得在拦截器中实现, 多少有些侵入性, 要做到无感透明, 其实还是挺难的.

 

posted on   mumuxinfei  阅读(13055)  评论(1编辑  收藏  举报

编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2014-07-26 HBase 实战(2)--时间序列检索和面检索的应用场景实战

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示