基于Mybatis手?jǐn)]一個(gè)分表插件


背景
leader負(fù)責(zé)記錄信息的業(yè)務(wù),每日預(yù)估數(shù)據(jù)量是15萬左右,所以引入sharding-jdbc做分表。leader完成業(yè)務(wù)的開發(fā)后,走了一波自測,git push后,就忙其他的事情去了。SpringBoot+Mybaits出問題了
git pull,準(zhǔn)備自測,單元測試run一下,上個(gè)廁所回來收工,就是這么自信。

Mybatis源碼,發(fā)現(xiàn)罪魁禍?zhǔn)资?/span>sharding-jdbc引起的,因?yàn)閿?shù)據(jù)源是sharding-jdbc的,導(dǎo)致后續(xù)執(zhí)行sql的是ShardingPreparedStatement。sharding-jdbc影響項(xiàng)目的所有業(yè)務(wù)表,因?yàn)樽罱K數(shù)據(jù)庫交互都由ShardingPreparedStatement去做了,歷史的一些sql語句因?yàn)?/span>sql函數(shù)或者其他寫法,使得ShardingPreparedStatement無法處理而出現(xiàn)異常。
leader了。
分析
支持自定義分表策略 能控制影響范圍 通用性
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);
/**
* 驗(yàn)證tableNamePrefix
*/
default void verificationTableNamePrefix(String tableNamePrefix){
if (StrUtil.isBlank(tableNamePrefix)) {
throw new RuntimeException("tableNamePrefix is null");
}
}
}
generateTableName函數(shù)的任務(wù)就是生成分表名,入?yún)⒂?/span>tableNamePrefix、value,tableNamePrefix為分表前綴,value作為生成分表名的邏輯參數(shù)。verificationTableNamePrefix函數(shù)驗(yàn)證tableNamePrefix必填,提供給實(shí)現(xiàn)類使用。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);
}
}
value是id值,用tableNamePrefix拼接id取模后的值,得到分表名返回。控制影響范圍
Mybatis規(guī)范中每個(gè)Mapper類對應(yīng)一張業(yè)務(wù)主體表,Mapper類的函數(shù)對應(yīng)業(yè)務(wù)主體表的相關(guān)sql。Mapper類打上注解,代表該Mpaaer類對應(yīng)的業(yè)務(wù)主體表有分表需求,從規(guī)范來說Mapper類的每個(gè)函數(shù)對應(yīng)的主體表都是正確的,但是有些同學(xué)可能不會(huì)按規(guī)范來寫。Mpaaer類對應(yīng)的是B表,Mpaaer類的某個(gè)函數(shù)寫著A表的sql,甚至是歷史遺留問題,所以注解不僅僅可以打在Mapper類上,同時(shí)還可以打在Mapper類的任意一個(gè)函數(shù)上,并且保證小粒度覆蓋粗粒度。/**
* @Author 程序猿阿星
* @Description 分表注解
* @Date 2021/5/9
*/
@Target(value = {ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {
// 表前綴名
String tableNamePrefix();
//值
String value() default "";
//是否是字段名,如果是需要解析請求參數(shù)改字段名的值(默認(rèn)否)
boolean fieldFlag() default false;
// 對應(yīng)的分表策略類
Class<? extends ITableShardStrategy> shardStrategy();
}
tableNamePrefix與shardStrategy屬性都好理解,表前綴名和分表策略,剩下的value與fieldFlag要怎么理解,分表策略分兩類,第一類依賴表中某個(gè)字段值,第二類則不依賴。id取模,屬于第一類,此處的value設(shè)置企業(yè)id入?yún)⒆侄蚊?/span>fieldFlag為true,意味著,會(huì)去解析獲取企業(yè)id字段名對應(yīng)的值。value與fieldFlag無需填寫,當(dāng)然你value也可以設(shè)置時(shí)間格式,具體看分表策略實(shí)現(xiàn)類的邏輯。通用性
sql中,同時(shí)具有通用性。Mybatis框架中,有攔截器機(jī)制做擴(kuò)展,我們只需要攔截StatementHandler#prepare函數(shù),即StatementHandle創(chuàng)建Statement之前,先把sql里面的表名動(dòng)態(tài)替換成分表名。Mybatis分表攔截器流程圖如下
Mybatis分表攔截器代碼如下,有點(diǎn)長哈,主流程看intercept函數(shù)就好了。/**
* @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è)工具類,類似反射的效果
MetaObject metaObject = getMetaObject(invocation);
BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
MappedStatement mappedStatement = (MappedStatement)
metaObject.getValue("delegate.mappedStatement");
//獲取Mapper執(zhí)行方法
Method method = invocation.getMethod();
//獲取分表注解
TableShard tableShard = getTableShard(method,mappedStatement);
// 如果method與class都沒有TableShard注解或執(zhí)行方法不存在,執(zhí)行下一個(gè)插件邏輯
if (tableShard == null) {
return invocation.proceed();
}
//獲取值
String value = tableShard.value();
//value是否字段名,如果是,需要解析請求參數(shù)字段名的值
boolean fieldFlag = tableShard.fieldFlag();
if (fieldFlag) {
//獲取請求參數(shù)
Object parameterObject = boundSql.getParameterObject();
if (parameterObject instanceof MapperMethod.ParamMap) { //ParamMap類型邏輯處理
MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
//根據(jù)字段名獲取參數(shù)值
Object valueObject = parameterMap.get(value);
if (valueObject == null) {
throw new RuntimeException(String.format("入?yún)⒆侄?s無匹配", value));
}
//替換sql
replaceSql(tableShard, valueObject, metaObject, boundSql);
} else { //單參數(shù)邏輯
//如果是基礎(chǔ)類型拋出異常
if (isBaseType(parameterObject)) {
throw new RuntimeException("單參數(shù)非法,請使用@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 {
//非基礎(chǔ)類型對象
Class<?> parameterObjectClass = parameterObject.getClass();
Field declaredField = parameterObjectClass.getDeclaredField(value);
declaredField.setAccessible(true);
Object valueObject = declaredField.get(parameterObject);
//替換sql
replaceSql(tableShard, valueObject, metaObject, boundSql);
}
}
} else {//無需處理parameterField
//替換sql
replaceSql(tableShard, value, metaObject, boundSql);
}
//執(zhí)行下一個(gè)插件邏輯
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 當(dāng)目標(biāo)類是StatementHandler類型時(shí),才包裝目標(biāo)類,否者直接返回目標(biāo)本身, 減少目標(biāo)被代理的次數(shù)
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
/**
* @param object
* @methodName: isBaseType
* @author: 程序員阿星
* @description: 基本數(shù)據(jù)類型驗(yàn)證,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容器獲取策略類
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è)工具類,類似反射的效果
* @date: 2021/5/9
* @return: org.apache.ibatis.reflection.MetaObject
*/
private MetaObject getMetaObject(Invocation invocation) {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// MetaObject是mybatis里面提供的一個(gè)工具類,類似反射的效果
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執(zhí)行方法的TableShard注解
tableShard = method.getAnnotation(TableShard.class);
//如果方法沒有設(shè)置注解,從Mapper接口上面獲取TableShard注解
if (tableShard == null) {
// 獲取TableShard注解
tableShard = Class.forName(className).getAnnotation(TableShard.class);
}
return tableShard;
}
}
jar包,需要使用的項(xiàng)目引入這個(gè)jar,然后注冊分表攔截器,自己根據(jù)業(yè)務(wù)需求實(shí)現(xiàn)分表策略,在給對應(yīng)的Mpaaer加上分表注解就好了。
實(shí)踐跑起來
demo,場景是有兩個(gè)分表策略,表也提前建立好了根據(jù) id分表tb_log_id_0tb_log_id_1根據(jù)日期分表 tb_log_date_202105tb_log_date_202106
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定義
/**
* @Author 程序猿阿星
* @Description
* @Date 2021/5/8
*/
@TableShard(tableNamePrefix = "tb_log_date",shardStrategy = TableShardStrategyDate.class)
public interface LogDateMapper {
/**
* 查詢列表-根據(jù)日期分表
*/
List<LogDate> queryList();
/**
* 單插入-根據(jù)日期分表
*/
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 {
/**
* 根據(jù)id查詢-根據(jù)id分片
*/
LogId queryOne(@Param("id") long id);
/**
* 單插入-根據(jù)id分片
*/
void save(LogId logId);
}
<?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">
//對應(yīng)LogDateMapper#queryList函數(shù)
<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>
//對應(yīng)LogDateMapper#save函數(shù)
<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">
//對應(yīng)LogIdMapper#queryOne函數(shù)
<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>
//對應(yīng)save函數(shù)
<insert id="save" >
insert into tb_log_id(id, comment,create_date)
values (#{id}, #{comment},#{createDate})
</insert>
</mapper>
執(zhí)行下單元測試
@Test
void test() {
LogDate logDate = new LogDate();
logDate.setId(snowflake.nextId());
logDate.setComment("測試內(nèi)容");
logDate.setCreateDate(new Date());
//插入
logDateMapper.save(logDate);
//查詢
List<LogDate> logDates = logDateMapper.queryList();
System.out.println(JSONUtil.toJsonPrettyStr(logDates));
}

id分表單元測試執(zhí)行@Test
void test() {
LogId logId = new LogId();
long id = snowflake.nextId();
logId.setId(id);
logId.setComment("測試");
logId.setCreateDate(new Date());
//插入
logIdMapper.save(logId);
//查詢
LogId logIdObject = logIdMapper.queryOne(id);
System.out.println(JSONUtil.toJsonPrettyStr(logIdObject));
}

小結(jié)一下
Mybatis進(jìn)階的使用教程,通過Mybatis攔截器實(shí)現(xiàn)分表的功能,滿足基本的業(yè)務(wù)需求,雖然比較簡陋,但是Mybatis這種擴(kuò)展機(jī)制與設(shè)計(jì)值得學(xué)習(xí)思考。demo項(xiàng)目,阿星放到了Gitee,大家按需自取
一鍵三連「分享」、「點(diǎn)贊」和「在看」
技術(shù)干貨與你天天見~
評論
圖片
表情
