Mybatis的分表实战
前言:
以前写代码, 关于mysql的分库分表已被中间件服务所支持, 业务代码涉及的sql已规避了这块. 它对扩展友好, 你也不知道到底他分为多少库, 多少表, 一切都是透明的.
不过对于小的团队/工作室而言, 可能就没有那么强大的分布式中间件的基础设施支持了, 而当数据库上去的时候, 分库分表就需要客户端client这边去支持维护了. 如何优雅地使用mybatis支持分表, 这就是本文的主题.
系列相关文章:
1. spring+mybatis的多源数据库配置实战
参考的博文:
1. MyBatis拦截器原理探究
2. SpringMVC + 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 2018-07-26 18:57 mumuxinfei 阅读(13055) 评论(1) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .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)--时间序列检索和面检索的应用场景实战