国产成人精品18p,天天干成人网,无码专区狠狠躁天天躁,美女脱精光隐私扒开免费观看

Mybatis實(shí)現分表插件

發(fā)布時(shí)間:2021-07-05 18:40 來(lái)源:腳本之家 閱讀:0 作者:程序猿阿星 欄目: 開(kāi)發(fā)技術(shù)

背景

事情是醬紫的,阿星的上級leader負責記錄信息的業(yè)務(wù),每日預估數據量是15萬(wàn)左右,所以引入sharding-jdbc做分表。

上級leader完成業(yè)務(wù)的開(kāi)發(fā)后,走了一波自測,git push后,就忙其他的事情去了。

項目的框架是SpringBoot+Mybaits

出問(wèn)題了

阿星負責的業(yè)務(wù)也開(kāi)發(fā)完了,熟練的git pull,準備自測,單元測試run一下,上個(gè)廁所回來(lái)收工,就是這么自信。

回來(lái)后,看下控制臺,人都傻了,一片紅,內心不禁感嘆“如果這是股票基金該多好”。

出了問(wèn)題就要解決,隨著(zhù)排查深入,我的眉頭一皺發(fā)現事情并不簡(jiǎn)單,怎么以前的一些代碼都報錯了?

隨著(zhù)排查深入,最后跟到了Mybatis源碼,發(fā)現罪魁禍首是sharding-jdbc引起的,因為數據源是sharding-jdbc的,導致后續執行sql的是ShardingPreparedStatement。

這就意味著(zhù),sharding-jdbc影響項目的所有業(yè)務(wù)表,因為最終數據庫交互都由ShardingPreparedStatement去做了,歷史的一些sql語(yǔ)句因為sql函數或者其他寫(xiě)法,使得ShardingPreparedStatement無(wú)法處理而出現異常。

關(guān)鍵代碼如下

發(fā)現問(wèn)題后,阿星馬上就反饋給leader了。

唉,本來(lái)還想摸魚(yú)的,看來(lái)摸魚(yú)的時(shí)間是沒(méi)了,還多了一項任務(wù)。

分析

竟然交給阿星來(lái)做了,就擼起袖子開(kāi)干吧,先看看分表功能的需求

  • 支持自定義分表策略
  • 能控制影響范圍
  • 通用性

分表會(huì )提前建立好,所以不需要考慮表不存在的問(wèn)題,核心邏輯實(shí)現,通過(guò)分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql。

分表策略

為了支持分表策略,我們需要先定義分表策略抽象接口,定義如下

/**
 * @Author 程序猿阿星
 * @Description 分表策略接口
 * @Date 2021/5/9
 */
public interface ITableShardStrategy {


    /**
     * @author: 程序猿阿星
     * @description: 生成分表名
     * @param tableNamePrefix 表前綴名
     * @param value 值
     * @date: 2021/5/9
     * @return: java.lang.String
     */
    String generateTableName(String tableNamePrefix,Object value);

    /**
     * 驗證tableNamePrefix
     */
    default void verificationTableNamePrefix(String tableNamePrefix){
        if (StrUtil.isBlank(tableNamePrefix)) {
            throw new RuntimeException("tableNamePrefix is null");
        }
    }
}

generateTableName函數的任務(wù)就是生成分表名,入參有tableNamePrefix、value,tableNamePrefix為分表前綴,value作為生成分表名的邏輯參數。

verificationTableNamePrefix函數驗證tableNamePrefix必填,提供給實(shí)現類(lèi)使用。

為了方便理解,下面是id取模策略代碼,取模兩張表

/**
 * @Author 程序猿阿星
 * @Description 分表策略id
 * @Date 2021/5/9
 */
@Component
public class TableShardStrategyId implements ITableShardStrategy {
    @Override
    public String generateTableName(String tableNamePrefix, Object value) {
        verificationTableNamePrefix(tableNamePrefix);
        if (value == null || StrUtil.isBlank(value.toString())) {
            throw new RuntimeException("value is null");
        }
        long id = Long.parseLong(value.toString());
        //此處可以緩存優(yōu)化
        return tableNamePrefix + "_" + (id % 2);
    }
}

傳入進(jìn)來(lái)的valueid值,用tableNamePrefix拼接id取模后的值,得到分表名返回。

控制影響范圍

分表策略已經(jīng)抽象出來(lái),下面要考慮控制影響范圍,我們都知道Mybatis規范中每個(gè)Mapper類(lèi)對應一張業(yè)務(wù)主體表,Mapper類(lèi)的函數對應業(yè)務(wù)主體表的相關(guān)sql。

阿星想著(zhù),可以給Mapper類(lèi)打上注解,代表該Mpaaer類(lèi)對應的業(yè)務(wù)主體表有分表需求,從規范來(lái)說(shuō)Mapper類(lèi)的每個(gè)函數對應的主體表都是正確的,但是有些同學(xué)可能不會(huì )按規范來(lái)寫(xiě)。

假設Mpaaer類(lèi)對應的是B表,Mpaaer類(lèi)的某個(gè)函數寫(xiě)著(zhù)A表的sql,甚至是歷史遺留問(wèn)題,所以注解不僅僅可以打在Mapper類(lèi)上,同時(shí)還可以打在Mapper類(lèi)的任意一個(gè)函數上,并且保證小粒度覆蓋粗粒度。

阿星這里自定義分表注解,代碼如下

/**
 * @Author 程序猿阿星
 * @Description 分表注解
 * @Date 2021/5/9
 */
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {

    // 表前綴名
    String tableNamePrefix();

    //值
    String value() default "";

    //是否是字段名,如果是需要解析請求參數改字段名的值(默認否)
    boolean fieldFlag() default false;

    // 對應的分表策略類(lèi)
    Class<? extends ITableShardStrategy> shardStrategy();


}


注解的作用范圍是類(lèi)、接口、函數,運行時(shí)生效。

tableNamePrefixshardStrategy屬性都好理解,表前綴名和分表策略,剩下的valuefieldFlag要怎么理解,分表策略分兩類(lèi),第一類(lèi)依賴(lài)表中某個(gè)字段值,第二類(lèi)則不依賴(lài)。

根據企業(yè)id取模,屬于第一類(lèi),此處的value設置企業(yè)id入參字段名,fieldFlagtrue,意味著(zhù),會(huì )去解析獲取企業(yè)id字段名對應的值。

根據日期分表,屬于第二類(lèi),直接在分表策略實(shí)現類(lèi)里面寫(xiě)就行了,不依賴(lài)表字段值,valuefieldFlag無(wú)需填寫(xiě),當然你value也可以設置時(shí)間格式,具體看分表策略實(shí)現類(lèi)的邏輯。

通用性

抽象分表策略與分表注解都搞定了,最后一步就是根據分表注解信息,去執行分表策略得到分表名,再把分表名動(dòng)態(tài)替換到sql中,同時(shí)具有通用性。

Mybatis框架中,有攔截器機制做擴展,我們只需要攔截StatementHandler#prepare函數,即StatementHandle創(chuàng )建Statement之前,先把sql里面的表名動(dòng)態(tài)替換成分表名。

Mybatis分表攔截器流程圖如下

Mybatis分表攔截器代碼如下,有點(diǎn)長(cháng)哈,主流程看intercept函數就好了。

/**
 * @Author 程序員阿星
 * @Description 分表攔截器
 * @Date 2021/5/9
 */
@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 {

        // MetaObject是mybatis里面提供的一個(gè)工具類(lèi),類(lèi)似反射的效果
        MetaObject metaObject = getMetaObject(invocation);
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        MappedStatement mappedStatement = (MappedStatement)
                metaObject.getValue("delegate.mappedStatement");

        //獲取Mapper執行方法
        Method method = invocation.getMethod();

        //獲取分表注解
        TableShard tableShard = getTableShard(method,mappedStatement);

        // 如果method與class都沒(méi)有TableShard注解或執行方法不存在,執行下一個(gè)插件邏輯
        if (tableShard == null) {
            return invocation.proceed();
        }

        //獲取值
        String value = tableShard.value();
        //value是否字段名,如果是,需要解析請求參數字段名的值
        boolean fieldFlag = tableShard.fieldFlag();

        if (fieldFlag) {
            //獲取請求參數
            Object parameterObject = boundSql.getParameterObject();

            if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap類(lèi)型邏輯處理

                MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
                //根據字段名獲取參數值
                Object valueObject = parameterMap.get(value);
                if (valueObject == null) {
                    throw new RuntimeException(String.format("入參字段%s無(wú)匹配", value));
                }
                //替換sql
                replaceSql(tableShard, valueObject, metaObject, boundSql);

            } else { //單參數邏輯

                //如果是基礎類(lèi)型拋出異常
                if (isBaseType(parameterObject)) {
                    throw new RuntimeException("單參數非法,請使用@Param注解");
                }

                if (parameterObject instanceof Map){
                    Map<String,Object>  parameterMap =  (Map<String,Object>)parameterObject;
                    Object valueObject = parameterMap.get(value);
                    //替換sql
                    replaceSql(tableShard, valueObject, metaObject, boundSql);
                } else {
                    //非基礎類(lèi)型對象
                    Class<?> parameterObjectClass = parameterObject.getClass();
                    Field declaredField = parameterObjectClass.getDeclaredField(value);
                    declaredField.setAccessible(true);
                    Object valueObject = declaredField.get(parameterObject);
                    //替換sql
                    replaceSql(tableShard, valueObject, metaObject, boundSql);
                }
            }

        } else {//無(wú)需處理parameterField
            //替換sql
            replaceSql(tableShard, value, metaObject, boundSql);
        }
        //執行下一個(gè)插件邏輯
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        // 當目標類(lèi)是StatementHandler類(lèi)型時(shí),才包裝目標類(lèi),否者直接返回目標本身, 減少目標被代理的次數
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }


    /**
     * @param object
     * @methodName: isBaseType
     * @author: 程序員阿星
     * @description: 基本數據類(lèi)型驗證,true是,false否
     * @date: 2021/5/9
     * @return: boolean
     */
    private boolean isBaseType(Object object) {
        if (object.getClass().isPrimitive()
                || object instanceof String
                || object instanceof Integer
                || object instanceof Double
                || object instanceof Float
                || object instanceof Long
                || object instanceof Boolean
                || object instanceof Byte
                || object instanceof Short) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * @param tableShard 分表注解
     * @param value      值
     * @param metaObject mybatis反射對象
     * @param boundSql   sql信息對象
     * @author: 程序猿阿星
     * @description: 替換sql
     * @date: 2021/5/9
     * @return: void
     */
    private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject, BoundSql boundSql) {
        String tableNamePrefix = tableShard.tableNamePrefix();
        //獲取策略class
        Class<? extends ITableShardStrategy> strategyClazz = tableShard.shardStrategy();
        //從spring ioc容器獲取策略類(lèi)

        ITableShardStrategy tableShardStrategy = SpringUtil.getBean(strategyClazz);
        //生成分表名
        String shardTableName = tableShardStrategy.generateTableName(tableNamePrefix, value);
        // 獲取sql
        String sql = boundSql.getSql();
        // 完成表名替換
        metaObject.setValue("delegate.boundSql.sql", sql.replaceAll(tableNamePrefix, shardTableName));
    }

    /**
     * @param invocation
     * @author: 程序猿阿星
     * @description: 獲取MetaObject對象-mybatis里面提供的一個(gè)工具類(lèi),類(lèi)似反射的效果
     * @date: 2021/5/9
     * @return: org.apache.ibatis.reflection.MetaObject
     */
    private MetaObject getMetaObject(Invocation invocation) {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // MetaObject是mybatis里面提供的一個(gè)工具類(lèi),類(lèi)似反射的效果
        MetaObject metaObject = MetaObject.forObject(statementHandler,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                defaultReflectorFactory
        );

        return metaObject;
    }

    /**
     * @author: 程序猿阿星
     * @description: 獲取分表注解
     * @param method
     * @param mappedStatement
     * @date: 2021/5/9
     * @return: com.xing.shard.interceptor.TableShard
     */
    private TableShard getTableShard(Method method, MappedStatement mappedStatement) throws ClassNotFoundException {
        String id = mappedStatement.getId();
        //獲取Class
        final String className = id.substring(0, id.lastIndexOf("."));
        //分表注解
        TableShard tableShard = null;
        //獲取Mapper執行方法的TableShard注解
        tableShard = method.getAnnotation(TableShard.class);
        //如果方法沒(méi)有設置注解,從Mapper接口上面獲取TableShard注解
        if (tableShard == null) {
            // 獲取TableShard注解
            tableShard = Class.forName(className).getAnnotation(TableShard.class);
        }
        return tableShard;
    }

}


到了這里,其實(shí)分表功能就已經(jīng)完成了,我們只需要把分表策略抽象接口、分表注解、分表攔截器抽成一個(gè)通用jar包,需要使用的項目引入這個(gè)jar,然后注冊分表攔截器,自己根據業(yè)務(wù)需求實(shí)現分表策略,在給對應的Mpaaer加上分表注解就好了。

實(shí)踐跑起來(lái)

這里阿星單獨寫(xiě)了一套demo,場(chǎng)景是有兩個(gè)分表策略,表也提前建立好了

  • 根據id分表
  • tb_log_id_0
  • tb_log_id_1
  • 根據日期分表
  • tb_log_date_202105
  • tb_log_date_202106

預警:后面都是代碼實(shí)操環(huán)節,請各位讀者大大耐心看完(非Java開(kāi)發(fā)除外)。

TableShardStrategy定義

/**
 * @Author wx
 * @Description 分表策略日期
 * @Date 2021/5/9
 */
@Component
public class TableShardStrategyDate implements ITableShardStrategy {

    private static final String DATE_PATTERN = "yyyyMM";

    @Override
    public String generateTableName(String tableNamePrefix, Object value) {
        verificationTableNamePrefix(tableNamePrefix);
        if (value == null || StrUtil.isBlank(value.toString())) {
            return tableNamePrefix + "_" +DateUtil.format(new Date(), DATE_PATTERN);
        } else {
            return tableNamePrefix + "_" +DateUtil.format(new Date(), value.toString());
        }
    }
}



**
 * @Author 程序猿阿星
 * @Description 分表策略id
 * @Date 2021/5/9
 */
@Component
public class TableShardStrategyId implements ITableShardStrategy {
    @Override
    public String generateTableName(String tableNamePrefix, Object value) {
        verificationTableNamePrefix(tableNamePrefix);
        if (value == null || StrUtil.isBlank(value.toString())) {
            throw new RuntimeException("value is null");
        }
        long id = Long.parseLong(value.toString());
        //可以加入本地緩存優(yōu)化
        return tableNamePrefix + "_" + (id % 2);
    }
}

Mapper定義

Mapper接口

/**
 * @Author 程序猿阿星
 * @Description
 * @Date 2021/5/8
 */
@TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class)
public interface LogDateMapper {

    /**
     * 查詢(xún)列表-根據日期分表
     */
    List<LogDate> queryList();

    /**
     * 單插入-根據日期分表
     */
    void  save(LogDate logDate);

}


-------------------------------------------------------------------------------------------------


/**
 * @Author 程序猿阿星
 * @Description
 * @Date 2021/5/8
 */
@TableShard(tableNamePrefix = "tb_log_id",value = "id",fieldFlag = true,shardStrategy = TableShardStrategyId.class)
public interface LogIdMapper {

    /**
     * 根據id查詢(xún)-根據id分片
     */
    LogId queryOne(@Param("id") long id);

    /**
     * 單插入-根據id分片
     */
    void save(LogId logId);
}

Mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xing.shard.mapper.LogDateMapper">
    
    //對應LogDateMapper#queryList函數
    <select id="queryList" resultType="com.xing.shard.entity.LogDate">
        select
        id as id,
        comment as comment,
        create_date as createDate
        from
        tb_log_date
    </select>
    
    //對應LogDateMapper#save函數
    <insert id="save" >
        insert into tb_log_date(id, comment,create_date)
        values (#{id}, #{comment},#{createDate})
    </insert>
</mapper>

-------------------------------------------------------------------------------------------------

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xing.shard.mapper.LogIdMapper">
    
    //對應LogIdMapper#queryOne函數
    <select id="queryOne" resultType="com.xing.shard.entity.LogId">
        select
        id as id,
        comment as comment,
        create_date as createDate
        from
        tb_log_id
        where
        id = #{id}
    </select>
    
    //對應save函數
    <insert id="save" >
        insert into tb_log_id(id, comment,create_date)
        values (#{id}, #{comment},#{createDate})
    </insert>

</mapper>

執行下單元測試

日期分表單元測試執行

    @Test
    void test() {
        LogDate logDate = new LogDate();
        logDate.setId(snowflake.nextId());
        logDate.setComment("測試內容");
        logDate.setCreateDate(new Date());
        //插入
        logDateMapper.save(logDate);
        //查詢(xún)
        List<LogDate> logDates = logDateMapper.queryList();
        System.out.println(JSONUtil.toJsonPrettyStr(logDates));
    }


輸出結果

id分表單元測試執行

    @Test
    void test() {
        LogId logId = new LogId();
        long id = snowflake.nextId();
        logId.setId(id);
        logId.setComment("測試");
        logId.setCreateDate(new Date());
        //插入
        logIdMapper.save(logId);
        //查詢(xún)
        LogId logIdObject = logIdMapper.queryOne(id);
        System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));
    }

輸出結果

小結一下

本文可以當做對Mybatis進(jìn)階的使用教程,通過(guò)Mybatis攔截器實(shí)現分表的功能,滿(mǎn)足基本的業(yè)務(wù)需求,雖然比較簡(jiǎn)陋,但是Mybatis這種擴展機制與設計值得學(xué)習思考。

有興趣的讀者也可以自己寫(xiě)一個(gè),或基于阿星的做改造,畢竟是簡(jiǎn)陋版本,還是有很多場(chǎng)景沒(méi)有考慮到。

另外分表的demo項目,阿星放到了Gitee,大家按需自取

Gitee地址:

項目結構:

到此這篇關(guān)于Mybatis實(shí)現分表插件的文章就介紹到這了,更多相關(guān)Mybatis 分表插件內容請搜索腳本之家以前的文章或繼續瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng )、來(lái)自互聯(lián)網(wǎng)轉載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權請聯(lián)系QQ:712375056 進(jìn)行舉報,并提供相關(guān)證據,一經(jīng)查實(shí),將立刻刪除涉嫌侵權內容。

欧美牲交黑粗硬大| 亚洲综合精品香蕉久久网| 国产手机在线无码播放视频| 国产一区二区三区免费观看在线| 国产精品久久久久久久免费看| 久久精品国产99国产电影网|