Mybatis的分表實戰

 

前言:
  之前寫代碼, 關於mysql的分庫分表已被中間件服務所支持, 業務代碼涉及的sql已規避了這塊. 它對擴展友好, 你也不知道到底他分爲多少庫, 多少表, 一切都是透明的.
  不過對於小的團隊/工做室而言, 可能就沒有那麼強大的分佈式中間件的基礎設施支持了, 而當數據庫上去的時候, 分庫分表就須要客戶端client這邊去支持維護了. 如何優雅地使用mybatis支持分表, 這就是本文的主題.html

 

系列相關文章:
  1. spring+mybatis的多源數據庫配置實戰 
  參考的博文:
  1. MyBatis攔截器原理探究 
  2. SpringMVC + MyBatis分庫分表方案 java

  3. 利用Mybatis攔截器對數據庫水平分表  mysql

 

mybatis插件機制:
  mybatis支持插件(plugin), 講得通俗一點就是攔截器(interceptor). 它支持ParameterHandler/StatementHandler/Executor/ResultSetHandler這四個級別進行攔截.
  整體概況爲:spring

  • 攔截參數的處理(ParameterHandler)
  • 攔截Sql語法構建的處理(StatementHandler)
  • 攔截執行器的方法(Executor)
  • 攔截結果集的處理(ResultSetHandler)

  好比sql rewrite, 它屬於StatementHandler的階段. 以分表實踐爲例, 它能夠簡單理解爲把table名稱替換爲分表table名稱的過程.sql

 

模擬實戰:
  讓咱們模擬實戰一回, 假定咱們有個需求, 就是把重要的業務日誌數據, 導入到表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接口類以下:api

public interface RecordMapper {

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

}

  在不改變代碼的前提下, 如何支持分表的無感知實現.mybatis

 

代碼編寫:
  因爲mybatis的攔截器是全局的, 所以這邊引入特定的註解用於區分目標/非目標對象(數據庫表).
  定義分表策略接口和具體的實現類:mvc

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

}

  定義註解:app

@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的攔截器給開發者很大的自由度, 像這邊的分表實踐是很好的例子. 但分表的策略有不少, 不少都是基於特定的維度進行散列, 總以爲在攔截器中實現, 多少有些侵入性, 要作到無感透明, 其實仍是挺難的.

相關文章
相關標籤/搜索