springboot+mybatis攔截器方法實(shí)現(xiàn)水平分表操作
1.前言
業(yè)務(wù)飛速發(fā)展導(dǎo)致了數(shù)據(jù)規(guī)模的急速膨脹,單機(jī)數(shù)據(jù)庫(kù)已經(jīng)無(wú)法適應(yīng)互聯(lián)網(wǎng)業(yè)務(wù)的發(fā)展。由于MySQL采用 B+樹(shù)索引,數(shù)據(jù)量超過(guò)閾值時(shí),索引深度的增加也將使得磁盤(pán)訪(fǎng)問(wèn)的 IO 次數(shù)增加,進(jìn)而導(dǎo)致查詢(xún)性能的下降;高并發(fā)訪(fǎng)問(wèn)請(qǐng)求也使得集中式數(shù)據(jù)庫(kù)成為系統(tǒng)的最大瓶頸。我們團(tuán)隊(duì)結(jié)合公司業(yè)務(wù)背景商議最終選定一份表的形式進(jìn)行解決這一瓶頸問(wèn)題,由我作為主要開(kāi)發(fā)主導(dǎo)完成,故寫(xiě)篇博客記錄沉淀。
2.MyBatis 允許使用插件來(lái)攔截的方法
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
總體概括為:
- 攔截執(zhí)行器的方法
- 攔截參數(shù)的處理
- 攔截結(jié)果集的處理
- 攔截Sql語(yǔ)法構(gòu)建的處理
這4各方法在MyBatis的一個(gè)操作(新增,刪除,修改,查詢(xún))中都會(huì)被執(zhí)行到,執(zhí)行的先后順序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。

3、Interceptor接口
package org.apache.ibatis.plugin;
import java.util.Properties;
public interface Interceptor {
//intercept方法就是要進(jìn)行攔截的時(shí)候要執(zhí)行的方法。
Object intercept(Invocation invocation) throws Throwable;
//plugin方法是攔截器用于封裝目標(biāo)對(duì)象的,通過(guò)該方法我們可以返回目標(biāo)對(duì)象本身,也可以返回一個(gè)它的代理。當(dāng)返回的是代理的時(shí)候我們可以對(duì)其中的方法進(jìn)行攔截來(lái)調(diào)用intercept方法,當(dāng)然也可以調(diào)用其他方法。
Object plugin(Object target);
//setProperties方法是用于在Mybatis配置文件中指定一些屬性的。
void setProperties(Properties properties);
} 4分表實(shí)現(xiàn)
4.1、大體思路
分表的表結(jié)構(gòu)已經(jīng)預(yù)設(shè)完畢,所以現(xiàn)在我們只需要在進(jìn)行增刪改查的時(shí)候直接一次鎖定目標(biāo)表,然后替換目標(biāo)sql。
4.2、逐步實(shí)現(xiàn)
4.2.1 Mybatis如何找到我們新增的攔截服務(wù)
對(duì)于攔截器Mybatis為我們提供了一個(gè)Interceptor接口,前面有提到,通過(guò)實(shí)現(xiàn)該接口就可以定義我們自己的攔截器。自定義的攔截器需要交給Mybatis管理,這樣才能使得Mybatis的執(zhí)行與攔截器的執(zhí)行結(jié)合在一起,即,利用springboot把自定義攔截器注入。
package com.shinemo.insurance.common.config;
import org.apache.ibatis.plugin.Interceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TableShardConfig {
/**
* 注冊(cè)插件
*/
@Bean
public Interceptor tableShardInterceptor() {
return new TableShardInterceptor();
}
}4.2.2 應(yīng)該攔截什么樣的對(duì)象
因?yàn)閿r截器是全局?jǐn)r截的,我們只需要攔截我們需要攔截的mapper,故需要用注解進(jìn)行標(biāo)識(shí)
package com.shinemo.insurance.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = { ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface TableShard {
// 表前綴名
String tableNamePrefix();
// 值
String value() default "";
// 是否是字段名,如果是需要解析請(qǐng)求參數(shù)改字段名的值(默認(rèn)否)
boolean fieldFlag() default false;
}我們只需要把這個(gè)注解標(biāo)識(shí)在我們要攔截的mapper上
@Mapper
@TableShard(tableNamePrefix = "t_insurance_video_people_", value = "deviceId", fieldFlag = true)
public interface InsuranceVideoPeopleMapper {
//VideoPeople對(duì)象中包含deviceId字段
int insert(VideoPeople videoPeople);
}4.2.3 實(shí)現(xiàn)自定義攔截器
package com.shinemo.insurance.common.config;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;
import com.shinemo.insurance.common.annotation.TableShard;
import com.shinemo.insurance.common.util.HashUtil;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.SystemMetaObject;
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,
Integer.class }) })
public class TableShardInterceptor implements Interceptor {
private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = 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執(zhí)行方法
Method method = invocation.getMethod();
// 獲取分表注解
TableShard tableShard = getTableShard(method, mappedStatement);
// 如果method與class都沒(méi)有TableShard注解或執(zhí)行方法不存在,執(zhí)行下一個(gè)插件邏輯
if (tableShard == null) {
return invocation.proceed();
}
//獲取值,此值就是拿的注解上value值,注解上value設(shè)定的值,并在傳入對(duì)象中獲取,根據(jù)業(yè)務(wù)可以選擇適當(dāng)?shù)闹导纯桑疫x取此值的目的是同一臺(tái)設(shè)備的值存入一張表中,有hash沖突的值也存在一張表中
String value = tableShard.value();
//value是否字段名,如果是,需要解析請(qǐng)求參數(shù)字段名的值
boolean fieldFlag = tableShard.fieldFlag();
if (fieldFlag) {
//獲取請(qǐng)求參數(shù)
Object parameterObject = boundSql.getParameterObject();
if (parameterObject instanceof MapperMethod.ParamMap) {
// ParamMap類(lèi)型邏輯處理
MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
// 根據(jù)字段名獲取參數(shù)值
Object valueObject = parameterMap.get(value);
if (valueObject == null) {
throw new RuntimeException(String.format("入?yún)⒆侄?s無(wú)匹配", value));
}
//替換sql
replaceSql(tableShard, valueObject, metaObject, boundSql);
} else {
// 單參數(shù)邏輯
//如果是基礎(chǔ)類(lèi)型拋出異常
if (isBaseType(parameterObject)) {
throw new RuntimeException("單參數(shù)非法,請(qǐng)使用@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ǔ)類(lèi)型對(duì)象
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);
}
//把原有的簡(jiǎn)單查詢(xún)語(yǔ)句替換為分表查詢(xún)語(yǔ)句了,現(xiàn)在是時(shí)候?qū)⒊绦虻目刂茩?quán)交還給Mybatis下一個(gè)攔截器處理
return invocation.proceed();
}
/**
* @description:
* @param target
* @return: Object
*/
@Override
public Object plugin(Object target) {
// 當(dāng)目標(biāo)類(lèi)是StatementHandler類(lèi)型時(shí),才包裝目標(biāo)類(lèi),否者直接返回目標(biāo)本身, 減少目標(biāo)被代理的次數(shù)
if (target instanceof StatementHandler) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
/**
* @description: 基本數(shù)據(jù)類(lèi)型驗(yàn)證,true是,false否
* @param object
* @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;
}
}
/**
* @description: 替換sql
* @param tableShard 分表注解
* @param value 值
* @param metaObject mybatis反射對(duì)象
* @param boundSql sql信息對(duì)象
* @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 = SpringBeanUtil.getBean(strategyClazz);
// 生成分表名
String shardTableName = generateTableName(tableNamePrefix, (String) value);
// 獲取sql
String sql = boundSql.getSql();
// 完成表名替換
metaObject.setValue("delegate.boundSql.sql",
sql.replaceAll(tableNamePrefix, shardTableName));
}
/**
* 生成表名
*
* @param tableNamePrefix 表名前綴
* @param value 價(jià)值
* @return {@link String}
*/
private String generateTableName(String tableNamePrefix, String value) {
//我們分了1024張表
int prime = 1024;
//hash取模運(yùn)算過(guò)后,鎖定目標(biāo)表
int rotatingHash = HashUtil.rotatingHash(value, prime);
return tableNamePrefix + rotatingHash;
}
/**
* @description: 獲取MetaObject對(duì)象-mybatis里面提供的一個(gè)工具類(lèi),類(lèi)似反射的效果
* @param invocation
* @return: 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, DEFAULT_REFLECTOR_FACTORY);
return metaObject;
}
/**
* @description: 獲取分表注解
* @param method
* @param mappedStatement
* @return: 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);
// 如果方法沒(méi)有設(shè)置注解,從Mapper接口上面獲取TableShard注解
if (tableShard == null) {
// 獲取TableShard注解
tableShard = Class.forName(className).getAnnotation(TableShard.class);
}
return tableShard;
}
}到此這篇關(guān)于springboot+mybatis攔截器方法實(shí)現(xiàn)水平分表操作的文章就介紹到這了,更多相關(guān)springboot mybatis水平分表操作內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot配置使Mybatis打印SQL執(zhí)行時(shí)的實(shí)際參數(shù)值操作
這篇文章主要介紹了SpringBoot配置使Mybatis打印SQL執(zhí)行時(shí)的實(shí)際參數(shù)值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-12-12
SpringKafka錯(cuò)誤處理(重試機(jī)制與死信隊(duì)列)
Spring Kafka提供了全面的錯(cuò)誤處理機(jī)制,通過(guò)靈活的重試策略和死信隊(duì)列處理,下面就來(lái)介紹一下,具有一定的參考價(jià)值,感興趣的可以了解一下2025-04-04
說(shuō)一說(shuō)java關(guān)鍵字final和transient
這篇文章主要和大家說(shuō)一說(shuō)java關(guān)鍵字final和transient,感興趣的小伙伴們可以參考一下2016-06-06
HttpClient實(shí)現(xiàn)遠(yuǎn)程調(diào)用
這篇文章主要為大家詳細(xì)介紹了HttpClient實(shí)現(xiàn)遠(yuǎn)程調(diào)用的方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08
SpringBoot 對(duì)象存儲(chǔ) MinIO的詳細(xì)過(guò)程
MinIO 是一個(gè)基于 Go 實(shí)現(xiàn)的高性能、兼容 S3 協(xié)議的對(duì)象存儲(chǔ),它適合存儲(chǔ)海量的非結(jié)構(gòu)化的數(shù)據(jù),這篇文章主要介紹了SpringBoot 對(duì)象存儲(chǔ) MinIO,需要的朋友可以參考下2023-07-07
如何用java實(shí)現(xiàn)分頁(yè)查詢(xún)
這篇文章主要介紹了如何用java實(shí)現(xiàn)分頁(yè)查詢(xún),文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06
Java別說(shuō)取余(%)運(yùn)算簡(jiǎn)單你真的會(huì)嗎
這篇文章主要介紹了Java別說(shuō)取余(%)運(yùn)算簡(jiǎn)單你真的會(huì)嗎,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07
Spring Data Jpa如何實(shí)現(xiàn)批量插入或更新
文章總結(jié):本文分享了四種Spring Data JPA批量插入或更新的方法,包括BatchConsumer、QueryParameterBuilder、KeyValue和SqlUtil,旨在為開(kāi)發(fā)者提供實(shí)用的參考2024-12-12
詳解Spring MVC如何測(cè)試Controller(使用springmvc mock測(cè)試)
這篇文章主要介紹了詳解Spring MVC如何測(cè)試Controller(使用springmvc mock測(cè)試),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-12-12

