zoukankan      html  css  js  c++  java
  • 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中.

    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接口类如下:

    public interface RecordMapper {
    
        @Insert("INSERT INTO tb_record(logs) VALUES(#{logs})")
        int addRecord(@Param("logs") String logs);
    
    }

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

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

    // 分表的策略类
    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());
        }
    
    }

      定义注解:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TableShard {
    
        // 要替换的表名
        String tableName();
    
        // 对应的分表策略类
        Class<? extends ITableShardStrategy> shardStrategy();
    
    }

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

    @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全局属性配置文件)中进行配置

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

      

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

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

      编写简单的测试代码:

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

  • 相关阅读:
    多语言网站(如何实现网站的多语言版本?)
    宝塔linux面板.txt
    什么是海外镜像点?
    一步一步CCNA之三:路由器全局配置模式
    Kmeans算法 与 KNN算法
    新浪公开课
    shanghaiR
    Announcing Couch Crawler, a CouchDB search engine/crawler
    字符串相似度算法(编辑距离算法 Levenshtein Distance)
    R与SAS、SPSS的比较
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/9371986.html
Copyright © 2011-2022 走看看