Práctica de subtabla de base de datos

Tabla de contenido

1. Antecedentes

2, proxy de base de datos, proxy JDBC

3. Selección de la solución de proxy JDBC

fragmentación-jdbc

Mybatis

4. Migración de datos


1. Antecedentes

Dado que el volumen de datos de una sola tabla de la biblioteca de páginas ha superado las decenas de millones, la más grande ha alcanzado el nivel de 100 millones. El rendimiento de la tabla única de páginas ha disminuido drásticamente debido a la cantidad excesiva de datos. La lógica principal detrás esto es que la cantidad de datos excede un cierto tamaño, B + Árbol La altura del índice aumentará, y cada vez que aumentes la altura de una capa, todo el escaneo del índice tendrá un IO más. Por lo tanto, para mejorar rendimiento, esta parte de la tabla debe dividirse en tablas, y algunas tablas se dividirán de acuerdo con las funciones comerciales en el futuro. Complete la operación de la subbiblioteca.

2, proxy de base de datos, proxy JDBC

Dado que la tabla debe dividirse, la distribución de datos y el enrutamiento deben procesarse, y se divide en tres capas de abajo hacia arriba, a saber, la capa de base de datos, la capa intermedia y la capa de aplicación. La mayoría de las soluciones se implementan en la capa intermedia, y la capa intermedia se divide en proxy DB y proxy JDBC según sea DB o sesgo de aplicación.

Proxy de base de datos : tomando mycat como ejemplo, se debe implementar y mantener un servicio de middleware, y luego la capa de aplicación solo debe prestar atención al código comercial, y mycat maneja completamente la lectura, escritura y fragmentación de la base de datos.

  • Ventajas: el middleware es responsable de la administración del clúster, los cambios de nodos en el clúster no necesitan ser notificados a cada cliente; es conveniente realizar una identificación única global y administración de transacciones; los metadatos se administran de manera centralizada y la estrategia de fragmentación puede ser flexible personalizado
  • Desventajas: todo el enlace es demasiado largo y cada capa aumentará el tiempo de respuesta; el middleware suele ser un solo punto y la alta disponibilidad debe lograrse por otros medios

Inserte la descripción de la imagen aquí

Proxy JDBC : tome sharding-jdbc como ejemplo. Solo necesita introducir el paquete jar en la capa de aplicación, encapsular jdbc y usarlo para leer, escribir y dividir la base de datos.

  • Ventajas: baja pérdida de rendimiento; el estado del cliente de cada aplicación es el mismo, proporcionando alta disponibilidad
  • Desventajas: limitación de idioma; acceso engorroso; distribución global de claves primarias, cambios de clúster, gestión de transacciones, etc.requieren comunicación entre nodos

Inserte la descripción de la imagen aquí

Probablemente pueda ver que el proxy de base de datos todavía es relativamente pesado, basándonos en la consideración, todavía decidimos usar el proxy JDBC.

3. Selección de la solución de proxy JDBC

Las opciones alternativas son sharding-jdbc y mybatis. Dado que solo necesitamos realizar operaciones de fragmentación de tablas para algunas tablas, sharding-jdbc es global, que puede ser menos controlable

Nuestro objetivo principal aquí es dividir la tabla user_test en user_test_0 y user_test_1, y crear estas dos tablas:

create table user_test_0
(
	id serial not null
		constraint user_0_pk
			primary key,
	name varchar,
	tenant_id varchar
);

create table user_test_1
(
	id serial not null
		constraint user_1_pk
			primary key,
	name varchar,
	tenant_id varchar
);

insert into user_test_0(name, tenant_id) values ('王五', 'alibaba');
insert into user_test_0(name, tenant_id) values ('赵六', 'alibaba');
insert into user_test_0(name, tenant_id) values ('张三', 'baidu');
insert into user_test_1(name, tenant_id) values ('李四', 'jd');

Para crear directamente datos perezosos, se demuestran las siguientes dos soluciones:

fragmentación-jdbc

Sharding-jdbc es un middleware de agente cliente de código abierto de Dangdang. Incluye funciones de fragmentación de bibliotecas y separación de lectura y escritura. No es intrusivo para el código de la aplicación y casi no tiene cambios. Es compatible con los marcos de trabajo de orm y los grupos de conexiones de bases de datos convencionales. ShardingSphere es actualmente un proyecto incubadora de Apache.

Dirección del documento oficial: http://shardingsphere.apache.org/document/legacy/2.x/cn/00-overview/

dirección de github: https://github.com/apache/shardingsphere

Código:

La ventaja de sharding-jdbc es que no es intrusivo para el código. Básicamente, no necesitamos tocar nuestro código original, solo cambiar la configuración de la conexión de la base de datos relacionada a la configuración de fragmentación.

La configuración original cuando la mesa no está dividida:

spring:
  datasource:
    url: "jdbc:postgresql://xxx:5432/xx?currentSchema=xx"
    username: xx
    password: xx
    driver-class-name: org.postgresql.Driver

Configuración después de usar fragmentación:

spring:
  shardingsphere:
    datasource:
      # 数据源名称,多数据源以逗号分隔
      names: maycur-pro
      maycur-pro:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: org.postgresql.Driver
        jdbc-url: jdbc:postgresql://192.168.95.143:5432/maycur-pro?currentSchema=team4
        username: team4
        password: maycur
    sharding:
      tables:
        # 表名
        user_test:
          # inline表达式,${begin..end} 表示范围区间
          actual-data-nodes: maycur-pro.user_test_$->{0..1}
          # 分表配置,根据tenantId分表
          table-strategy:
            standard:
              precise-algorithm-class-name: com.database.subtable.segment.MyPreciseShardingAlgorithm
              sharding-column: tenant_id
#            inline:
#              sharding-column: tenant_id
#              # 分表表达式采用groovy语法
#              algorithm-expression: user_test_$->{tenant_id % 2}

#          # 配置字段的生成策略,column为字段名,type为生成策略,sharding默认提供SNOWFLAKE和UUID两种,可以自己实现其他策略
#          key-generator:
#            column: tenantId
#            type: SNOWFLAKE
    # 属性配置(可选)
    props:
      # 是否开启sql显示,默认false
      sql:
        show: true

El algoritmo de división de tablas anterior no usa expresiones en línea, sino una clase de implementación de algoritmo personalizado MyPreciseShardingAlgorithm, que usa el algoritmo de división de tablas principal (hash + mod), basado principalmente en el valor hash del campo de la subtabla y luego en el número de subtabla tablas Tome el módulo para obtener el número de serie específico de la subtabla y corresponda cada solicitud a user_test_0 o user_test_1. El código es el siguiente:

public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {

    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) {
        for (String tableName : availableTargetNames) {
            if (tableName.endsWith(Math.abs(shardingValue.hashCode() % 2) + "")) {
                return tableName;
            }
        }
        throw new IllegalArgumentException();
    }
}

Cuando comencemos a consultar, la fragmentación se dividirá automáticamente para nosotros, por supuesto, todo sql será así, y nuestras necesidades más primitivas pueden ser solo para ciertas tablas, lo que puede causar algunos peligros ocultos en proyectos grandes, y la fragmentación es complicada para algo de SQL también es un poco incompatible e incompatible, por lo que puede que no sea adecuado. La consulta SQL es la siguiente:

Mybatis

Mybatis implementa la operación de subtabla al admitir complementos. El más popular es el interceptor. Admite los cuatro niveles de ParameterHandler / StatementHandler / Executor / ResultSetHandler para la interceptación, que se puede resumir brevemente como:

  • Procesamiento de parámetros de intercepción (ParameterHandler)
  • Interceptar el procesamiento de la construcción de sintaxis SQL (StatementHandler)
  • Método de interceptación del ejecutor (ejecutor)
  • Interceptar el procesamiento del conjunto de resultados (ResultSetHandler)

Por ejemplo, sql rewrite, que pertenece a la etapa StatementHandler, es en realidad el proceso de reemplazar el nombre de la tabla original con el nombre de la tabla de la subtabla para la subtabla.

Código:

Dado que el interceptor de mybatis es global, es necesario introducir anotaciones específicas para distinguir entre objetos objetivo / no objetivo (tablas de base de datos), primero defina la interfaz de estrategia de tabla y clases de implementación específicas:

public interface ShardTableStrategy {

    /**
     * 分表算法
     * @param statementHandler
     * @return 替换后的表名
     */
    String shardAlgorithm(StatementHandler statementHandler);
}




public class UserStrategy implements ShardTableStrategy{

    /**
     * 原始表名
     */
    private final static String USER_ORIGIN_TABLE_NAME = "user_test";
    /**
     * 下划线
     */
    private final static String TABLE_LINE = "_";
    /**
     * 分表数量
     */
    public final static Integer USER_TABLE_NUM = 2;
    /**
     * 分表字段
     */
    private final static String USER_TABLE_SUB_FIELD = "tenantId";

    @Override
    public String shardAlgorithm(StatementHandler statementHandler) {
        // 可以增加前置判断是否需要分表
        BoundSql boundSql = statementHandler.getBoundSql();
        Object parameterObject = boundSql.getParameterObject();
        // 参数值
        Map param2ValeMap = JSONObject.parseObject(JSON.toJSONString(parameterObject), Map.class);

        Object subFieldValue = param2ValeMap.get(USER_TABLE_SUB_FIELD);
        if (param2ValeMap.size() == 0 || subFieldValue == null) {
            throw new RuntimeException("User is subTable so must have subFiledValue!");
        }

        return USER_ORIGIN_TABLE_NAME + TABLE_LINE + Math.abs(subFieldValue.hashCode() % USER_TABLE_NUM);
    }
}

Anotación de definición (definida aquí como una matriz porque algunos archivos del asignador pueden tener varias tablas diferentes que deben dividirse en tablas):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SegmentTable {

    /**
     * 表名
     */
    String[] tableName();


    /**
     * 算法策略
     */
    Class<? extends ShardTableStrategy>[] strategyClazz();
}

Escribe un interceptor específico:

@Intercepts(@Signature(type = StatementHandler.class,method = "prepare",args = {Connection.class,Integer.class}))
public class ShardTableInterceptor implements Interceptor {

    private final static Logger logger = LoggerFactory.getLogger(ShardTableInterceptor.class);

    private final static String BOUND_SQL_NAME = "delegate.boundSql.sql";

    private final static String MAPPED_STATEMENT_NAME = "delegate.mappedStatement";

    @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, new DefaultReflectorFactory());
        // @SegmentTable
        SegmentTable segmentTable = getSegmentTable(metaObject);
        if (segmentTable == null) {
            return invocation.proceed();
        }
        // 校验注解:表名与算法必须一致
        Class[] classes = segmentTable.strategyClazz();
        String[] tableNames = segmentTable.tableName();
        if(classes.length != tableNames.length){
            throw new RuntimeException("SegmentTable annotation's subTable tableNames and classes must same length!");
        }

        // 获取表名与算法的映射
        Map<String, Class> tableName2StrategyClazzMap = buildTableName2StrategyClazzMap(classes,tableNames);
        // 处理sql
        String sql = handleSql(statementHandler, metaObject, tableName2StrategyClazzMap);
        // 替换sql
        metaObject.setValue(BOUND_SQL_NAME, sql);
        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) {

    }

    private SegmentTable getSegmentTable(MetaObject metaObject) throws ClassNotFoundException {
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(MAPPED_STATEMENT_NAME);
        // 在命名空间中唯一的标识符
        String id = mappedStatement.getId();
        id = id.substring(0, id.lastIndexOf("."));
        Class cls = Class.forName(id);
        SegmentTable segmentTable = (SegmentTable) cls.getAnnotation(SegmentTable.class);
        logger.info("ShardTableInterceptor  getSegmentTable SegmentTable={}", JSON.toJSONString(segmentTable));
        return segmentTable;
    }

    private Map<String, Class> buildTableName2StrategyClazzMap(Class[] classes, String[] tableNames) {
        Map<String, Class> tableName2StrategyClazzMap = new HashMap<>();
        for (int i = 0; i < classes.length; i++) {
            tableName2StrategyClazzMap.put(tableNames[i], classes[i]);
        }

        logger.info("ShardTableInterceptor  buildTableName2StrategyClazzMap tableName2StrategyClazzMap={}", JSON.toJSONString(tableName2StrategyClazzMap));
        return tableName2StrategyClazzMap;
    }

    private String handleSql(StatementHandler statementHandler, MetaObject metaObject, Map<String, Class> tableName2StrategyClazzMap) throws InstantiationException, IllegalAccessException {
        String sql = (String) metaObject.getValue(BOUND_SQL_NAME);
        logger.info("ShardTableInterceptor  original sql={}", sql);
        for (Map.Entry<String, Class> entry : tableName2StrategyClazzMap.entrySet()) {
            String tableName = entry.getKey();
            Class strategyClazz = entry.getValue();
            // 没有分表名就不走算法
            if (!sql.contains(tableName)) {
                continue;
            }
            // 1.对value进行算法 -> 确定表名
            ShardTableStrategy strategy = (ShardTableStrategy) strategyClazz.newInstance();
            String replaceTableName = strategy.shardAlgorithm(statementHandler);
            // 2.替换分表表名
            sql = sql.replaceAll(tableName, replaceTableName);
            logger.info("ShardTableInterceptor handleSql sql={},tableName = {},replaceTableName={}", sql, tableName, replaceTableName);
        }
        return sql;
    }
}

Finalmente, configure en el archivo de configuración mybatis (mybatis-config.xml) y agregue comentarios al archivo mapeador:

<plugins>
        <plugin interceptor="com.database.subtable.segment.ShardTableInterceptor"/>
    </plugins>

@SegmentTable(tableName = {"user_test"}, strategyClazz = {UserStrategy.class})
public interface UserMapper {

    List<User> listUsers(@Param("tenantId") String tenantId);

}

Haga clic en la consulta, de acuerdo con el registro impreso, puede ver que la operación de la subtabla se ha implementado

4. Migración de datos

Determinar las reglas de división de tablas es en realidad solo el primer paso de la división de tablas. Lo problemático es la migración de datos, o cómo hacer la migración de datos con el menor impacto en el negocio. Dado que confiamos en Alibaba Cloud, decidimos migrar los datos a través de DataWorks cuando nos conectamos a altas horas de la noche. Todo el proceso tomó menos de una hora.

Cree una función hash_code (texto) personalizada en postgreSql. Esta función es coherente con el algoritmo java hashCode () para garantizar que la migración de datos de DataWorks se base en el hash del campo clave para bloquear la tabla correcta

DROP FUNCTION IF EXISTS  hash_code(text);
CREATE FUNCTION hash_code(text) RETURNS integer
    LANGUAGE plpgsql
AS
$$
DECLARE
    i integer := 0;
    DECLARE
    h bigint  := 0;
BEGIN
    FOR i IN 1..length($1)
        LOOP
            h = (h * 31 + ascii(substring($1, i, 1))) & 4294967295;
        END LOOP;
    RETURN cast(cast(h AS bit(32)) AS int4);
END;
$$;


Si la aplicación no permite escenas que no se pueden usar durante un período de tiempo, también puede escribir todos los datos recién generados en la subtabla después de que se inicie la transformación de la subtabla, pero la operación en los datos históricos todavía está en la tabla anterior, y solo necesita hacerlo antes de operar los datos.Un juicio de enrutamiento, cuando se generan suficientes datos nuevos (por ejemplo, dos o tres meses), casi todas las operaciones en este momento son para subtablas, y luego comienzan migración de datos. Una vez completada la migración de datos, se puede eliminar el juicio de enrutamiento original.

Supongo que te gusta

Origin blog.csdn.net/m0_38001814/article/details/109427511
Recomendado
Clasificación