• springboot+mybatis拦截器实现水平分表操作


    1.前言

    业务飞速发展导致了数据规模的急速膨胀,单机数据库已经无法适应互联网业务的发展。由于MySQL采用 B+树索引,数据量超过阈值时,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;高并发访问请求也使得集中式数据库成为系统的最大瓶颈。我们团队结合公司业务背景商议最终选定一份表的形式进行解决这一瓶颈问题,由我作为主要开发主导完成,故写篇博客记录沉淀。

    2.MyBatis 允许使用插件来拦截的方法

    1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
    2. ParameterHandler (getParameterObject, setParameters)
    3. ResultSetHandler (handleResultSets, handleOutputParameters)
    4. StatementHandler (prepare, parameterize, batch, update, query)

    总体概括为:

    1. 拦截执行器的方法
    2. 拦截参数的处理
    3. 拦截结果集的处理
    4. 拦截Sql语法构建的处理

    这4各方法在MyBatis的一个操作(新增,删除,修改,查询)中都会被执行到,执行的先后顺序是Executor,ParameterHandler,ResultSetHandler,StatementHandler。

    3、Interceptor接口

    1. package org.apache.ibatis.plugin;
    2. import java.util.Properties;
    3. public interface Interceptor {
    4. //intercept方法就是要进行拦截的时候要执行的方法。
    5. Object intercept(Invocation invocation) throws Throwable;
    6. //plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。
    7. Object plugin(Object target);
    8. //setProperties方法是用于在Mybatis配置文件中指定一些属性的。
    9. void setProperties(Properties properties);
    10. }

     4分表实现

    4.1、大体思路

    分表的表结构已经预设完毕,所以现在我们只需要在进行增删改查的时候直接一次锁定目标表,然后替换目标sql。

    4.2、逐步实现

    4.2.1 Mybatis如何找到我们新增的拦截服务

    对于拦截器Mybatis为我们提供了一个Interceptor接口,前面有提到,通过实现该接口就可以定义我们自己的拦截器。自定义的拦截器需要交给Mybatis管理,这样才能使得Mybatis的执行与拦截器的执行结合在一起,即,利用springboot把自定义拦截器注入。

    1. package com.shinemo.insurance.common.config;
    2. import org.apache.ibatis.plugin.Interceptor;
    3. import org.springframework.context.annotation.Bean;
    4. import org.springframework.context.annotation.Configuration;
    5. @Configuration
    6. public class TableShardConfig {
    7. /**
    8. * 注册插件
    9. */
    10. @Bean
    11. public Interceptor tableShardInterceptor() {
    12. return new TableShardInterceptor();
    13. }
    14. }

    4.2.2 应该拦截什么样的对象

    因为拦截器是全局拦截的,我们只需要拦截我们需要拦截的mapper,故需要用注解进行标识

    1. package com.shinemo.insurance.common.annotation;
    2. import java.lang.annotation.ElementType;
    3. import java.lang.annotation.Retention;
    4. import java.lang.annotation.RetentionPolicy;
    5. import java.lang.annotation.Target;
    6. @Target(value = { ElementType.TYPE, ElementType.METHOD })
    7. @Retention(RetentionPolicy.RUNTIME)
    8. public @interface TableShard {
    9. // 表前缀名
    10. String tableNamePrefix();
    11. // 值
    12. String value() default "";
    13. // 是否是字段名,如果是需要解析请求参数改字段名的值(默认否)
    14. boolean fieldFlag() default false;
    15. }

    我们只需要把这个注解标识在我们要拦截的mapper上

    1. @Mapper
    2. @TableShard(tableNamePrefix = "t_insurance_video_people_", value = "deviceId", fieldFlag = true)
    3. public interface InsuranceVideoPeopleMapper {
    4. //VideoPeople对象中包含deviceId字段
    5. int insert(VideoPeople videoPeople);
    6. }

    4.2.3 实现自定义拦截器

    1. package com.shinemo.insurance.common.config;
    2. import java.lang.reflect.Field;
    3. import java.lang.reflect.Method;
    4. import java.sql.Connection;
    5. import java.util.Map;
    6. import com.shinemo.insurance.common.annotation.TableShard;
    7. import com.shinemo.insurance.common.util.HashUtil;
    8. import org.apache.ibatis.binding.MapperMethod;
    9. import org.apache.ibatis.executor.statement.StatementHandler;
    10. import org.apache.ibatis.mapping.BoundSql;
    11. import org.apache.ibatis.mapping.MappedStatement;
    12. import org.apache.ibatis.plugin.Interceptor;
    13. import org.apache.ibatis.plugin.Intercepts;
    14. import org.apache.ibatis.plugin.Invocation;
    15. import org.apache.ibatis.plugin.Plugin;
    16. import org.apache.ibatis.plugin.Signature;
    17. import org.apache.ibatis.reflection.DefaultReflectorFactory;
    18. import org.apache.ibatis.reflection.MetaObject;
    19. import org.apache.ibatis.reflection.ReflectorFactory;
    20. import org.apache.ibatis.reflection.SystemMetaObject;
    21. @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,
    22. Integer.class }) })
    23. public class TableShardInterceptor implements Interceptor {
    24. private static final ReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    25. @Override
    26. public Object intercept(Invocation invocation) throws Throwable {
    27. // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
    28. MetaObject metaObject = getMetaObject(invocation);
    29. BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
    30. MappedStatement mappedStatement = (MappedStatement) metaObject
    31. .getValue("delegate.mappedStatement");
    32. // 获取Mapper执行方法
    33. Method method = invocation.getMethod();
    34. // 获取分表注解
    35. TableShard tableShard = getTableShard(method, mappedStatement);
    36. // 如果method与class都没有TableShard注解或执行方法不存在,执行下一个插件逻辑
    37. if (tableShard == null) {
    38. return invocation.proceed();
    39. }
    40. //获取值,此值就是拿的注解上value值,注解上value设定的值,并在传入对象中获取,根据业务可以选择适当的值即可,我选取此值的目的是同一台设备的值存入一张表中,有hash冲突的值也存在一张表中
    41. String value = tableShard.value();
    42. //value是否字段名,如果是,需要解析请求参数字段名的值
    43. boolean fieldFlag = tableShard.fieldFlag();
    44. if (fieldFlag) {
    45. //获取请求参数
    46. Object parameterObject = boundSql.getParameterObject();
    47. if (parameterObject instanceof MapperMethod.ParamMap) {
    48. // ParamMap类型逻辑处理
    49. MapperMethod.ParamMap parameterMap = (MapperMethod.ParamMap) parameterObject;
    50. // 根据字段名获取参数值
    51. Object valueObject = parameterMap.get(value);
    52. if (valueObject == null) {
    53. throw new RuntimeException(String.format("入参字段%s无匹配", value));
    54. }
    55. //替换sql
    56. replaceSql(tableShard, valueObject, metaObject, boundSql);
    57. } else {
    58. // 单参数逻辑
    59. //如果是基础类型抛出异常
    60. if (isBaseType(parameterObject)) {
    61. throw new RuntimeException("单参数非法,请使用@Param注解");
    62. }
    63. if (parameterObject instanceof Map) {
    64. Map parameterMap = (Map) parameterObject;
    65. Object valueObject = parameterMap.get(value);
    66. //替换sql
    67. replaceSql(tableShard, valueObject, metaObject, boundSql);
    68. } else {
    69. //非基础类型对象
    70. Class parameterObjectClass = parameterObject.getClass();
    71. Field declaredField = parameterObjectClass.getDeclaredField(value);
    72. declaredField.setAccessible(true);
    73. Object valueObject = declaredField.get(parameterObject);
    74. //替换sql
    75. replaceSql(tableShard, valueObject, metaObject, boundSql);
    76. }
    77. }
    78. } else {//无需处理parameterField
    79. //替换sql
    80. replaceSql(tableShard, value, metaObject, boundSql);
    81. }
    82. //把原有的简单查询语句替换为分表查询语句了,现在是时候将程序的控制权交还给Mybatis下一个拦截器处理
    83. return invocation.proceed();
    84. }
    85. /**
    86. * @description:
    87. * @param target
    88. * @return: Object
    89. */
    90. @Override
    91. public Object plugin(Object target) {
    92. // 当目标类是StatementHandler类型时,才包装目标类,否者直接返回目标本身, 减少目标被代理的次数
    93. if (target instanceof StatementHandler) {
    94. return Plugin.wrap(target, this);
    95. } else {
    96. return target;
    97. }
    98. }
    99. /**
    100. * @description: 基本数据类型验证,true是,false否
    101. * @param object
    102. * @return: boolean
    103. */
    104. private boolean isBaseType(Object object) {
    105. if (object.getClass().isPrimitive() || object instanceof String || object instanceof Integer
    106. || object instanceof Double || object instanceof Float || object instanceof Long
    107. || object instanceof Boolean || object instanceof Byte || object instanceof Short) {
    108. return true;
    109. } else {
    110. return false;
    111. }
    112. }
    113. /**
    114. * @description: 替换sql
    115. * @param tableShard 分表注解
    116. * @param value 值
    117. * @param metaObject mybatis反射对象
    118. * @param boundSql sql信息对象
    119. * @return: void
    120. */
    121. private void replaceSql(TableShard tableShard, Object value, MetaObject metaObject,
    122. BoundSql boundSql) {
    123. String tableNamePrefix = tableShard.tableNamePrefix();
    124. // // 获取策略class
    125. // Class strategyClazz = tableShard.shardStrategy();
    126. // // 从spring ioc容器获取策略类
    127. // ITableShardStrategy tableShardStrategy = SpringBeanUtil.getBean(strategyClazz);
    128. // 生成分表名
    129. String shardTableName = generateTableName(tableNamePrefix, (String) value);
    130. // 获取sql
    131. String sql = boundSql.getSql();
    132. // 完成表名替换
    133. metaObject.setValue("delegate.boundSql.sql",
    134. sql.replaceAll(tableNamePrefix, shardTableName));
    135. }
    136. /**
    137. * 生成表名
    138. *
    139. * @param tableNamePrefix 表名前缀
    140. * @param value 价值
    141. * @return {@link String}
    142. */
    143. private String generateTableName(String tableNamePrefix, String value) {
    144. //我们分了1024张表
    145. int prime = 1024;
    146. //hash取模运算过后,锁定目标表
    147. int rotatingHash = HashUtil.rotatingHash(value, prime);
    148. return tableNamePrefix + rotatingHash;
    149. }
    150. /**
    151. * @description: 获取MetaObject对象-mybatis里面提供的一个工具类,类似反射的效果
    152. * @param invocation
    153. * @return: MetaObject
    154. */
    155. private MetaObject getMetaObject(Invocation invocation) {
    156. StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
    157. // MetaObject是mybatis里面提供的一个工具类,类似反射的效果
    158. MetaObject metaObject = MetaObject.forObject(statementHandler,
    159. SystemMetaObject.DEFAULT_OBJECT_FACTORY,
    160. SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
    161. return metaObject;
    162. }
    163. /**
    164. * @description: 获取分表注解
    165. * @param method
    166. * @param mappedStatement
    167. * @return: TableShard
    168. */
    169. private TableShard getTableShard(Method method,
    170. MappedStatement mappedStatement) throws ClassNotFoundException {
    171. String id = mappedStatement.getId();
    172. // 获取Class
    173. final String className = id.substring(0, id.lastIndexOf("."));
    174. // 分表注解
    175. TableShard tableShard = null;
    176. // 获取Mapper执行方法的TableShard注解
    177. tableShard = method.getAnnotation(TableShard.class);
    178. // 如果方法没有设置注解,从Mapper接口上面获取TableShard注解
    179. if (tableShard == null) {
    180. // 获取TableShard注解
    181. tableShard = Class.forName(className).getAnnotation(TableShard.class);
    182. }
    183. return tableShard;
    184. }
    185. }

  • 相关阅读:
    记录一次数据库CPU被打满的排查过程
    (持续整理)Windows快捷键
    python+flask计算机毕业设计web的智慧云医疗的设计与实现(程序+开题+论文)
    Vue-cli前端工程配置
    Android . java中解析json数据中文变成问号
    项目经理之识别项目干系人
    DH、DHE、ECDHE加密算法
    U++ 插件学习笔记
    【Java-LangChain:使用 ChatGPT API 搭建系统-11】用 ChatGPT API 构建系统 总结篇
    高等数学(第七版)同济大学 总习题六 个人解答
  • 原文地址:https://blog.csdn.net/studyhard_/article/details/126237400