Springboot MyBatis implementa la conmutación de fuentes de datos múltiples y la replicación maestro-esclavo (separación de lectura y escritura)

Introducción

  Este artículo explica principalmente cómo Springboot MyBatis implementa la conmutación de fuentes de datos múltiples y la replicación maestro-esclavo (separación de lectura y escritura) a través de texto y código. Aquí, la información de configuración de la fuente de datos dinámica se obtiene configurando desde la tabla de configuración de la fuente de datos de la base de datos, y se realiza la generación de fuente de datos., interruptor, operación de replicación maestro-esclavo.

Los principios fundamentales de la implementación técnica son: AbstractRoutingDataSource + ThreadLocal + AOP .

AbstractRoutingDataSource:

AbstractRoutingDataSource es una clase de enrutamiento de origen de datos proporcionada por Spring JDBC, que se utiliza para seleccionar el origen de datos de destino correspondiente de acuerdo con diferentes orígenes de datos.

En algunos escenarios complejos, necesitamos conectarnos a varias bases de datos, y estas bases de datos tienen sus propias configuraciones de fuentes de datos. Además, también es necesario seleccionar dinámicamente una fuente de datos específica en función de ciertas condiciones en tiempo de ejecución, como decidir dinámicamente a qué base de datos conectarse en función de los parámetros de solicitud y los permisos de usuario.

En este momento, puede usar la clase AbstractRoutingDataSource para administrar estas fuentes de datos, que proporciona un mecanismo para seleccionar la fuente de datos adecuada de acuerdo con ciertas condiciones, a fin de realizar la configuración y administración de fuentes de datos dinámicas.

Específicamente, podemos heredar la clase AbstractRoutingDataSource y reescribir el método determineCurrentLookupKey, que devuelve dinámicamente la clave de enrutamiento de la fuente de datos de destino en función de información como la solicitud actual o los permisos del usuario, y luego selecciona la fuente de datos correspondiente para las operaciones de conexión basadas en esto. llave

Hilo local:

ThreadLocal es una clase muy práctica en Java, que se utiliza para resolver el problema de las variables compartidas en situaciones de subprocesos múltiples. Su función es proporcionar variables locales en el subproceso. En un entorno de subprocesos múltiples, cada subproceso no se afecta entre sí y se puede sacar en cualquier momento donde sea necesario sin operaciones de sincronización adicionales. Su función principal es proporcionar a cada subproceso una copia variable independiente, y cada subproceso puede modificar su propia copia variable a voluntad sin afectar las copias variables de otros subprocesos.

ThreadLocal se usa principalmente para aislar información entre múltiples subprocesos, por ejemplo:
1. En un entorno de subprocesos múltiples, cada subproceso tiene su propio campo de datos, como en un grupo de subprocesos, cada subproceso necesita tener su propio campo de datos al procesar tareas. , ThreadLocal es muy adecuado para esta situación.
2. Los subprocesos necesitan usar algunas variables compartidas al procesar tareas, pero estas variables son inmutables.Usar ThreadLocal puede evitar condiciones de carrera en subprocesos múltiples y mejorar el rendimiento del programa.

Con todo, ThreadLocal es un método de uso compartido de datos internos de subprocesos proporcionado en Java. Es un método seguro para subprocesos. En un entorno de subprocesos múltiples, el uso de ThreadLocal puede proteger eficazmente la independencia de los datos compartidos.

POA:

Java AOP (programación orientada a aspectos, programación orientada a aspectos) es un paradigma de programación que enfatiza la extracción horizontal de unidades lógicas (intereses transversales). Las preocupaciones transversales comunes incluyen el registro, las transacciones, la seguridad, el almacenamiento en caché, etc. En la POO tradicional (Programación orientada a objetos, Programación orientada a objetos), estas unidades lógicas a menudo están dispersas en varias clases y métodos, lo que hace que el código sea complicado y difícil de entender y mantener.

Java AOP proporciona una solución que permite a los desarrolladores abstraer preocupaciones transversales en aspectos (aspectos) y aplicar estos aspectos a puntos específicos del código, como llamadas a métodos, creación de objetos, acceso a propiedades, etc. Mediante el uso de Java AOP, los desarrolladores pueden separar las preocupaciones, haciendo que el código sea más claro y modular.

Hay muchas formas de implementar Java AOP, incluido JDK AOP basado en un proxy dinámico, AspectJ basado en la mejora del código de bytes, Spring AOP basado en la anotación, etc. Estos marcos y herramientas proporcionan API convenientes y fáciles de usar, lo que permite a los desarrolladores implementar AOP fácilmente.

el código

En primer lugar, introduzca la dependencia mybatis en el proyecto maven y configure la configuración relevante. ! !

Declare una clase de configuración para una fuente de datos dinámica, el código es el siguiente:


import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * file:DynamicDataSourceConfig
 *
 * @author tarzan 
 */

@Configuration
public class DynamicDataSourceConfig {

    @Value("${spring.datasource.url}")
    private String defaultUrl;
    @Value("${spring.datasource.driverClassName:org.postgresql.Driver}")
    private String driverClassName;
    @Value("${spring.datasource.username}")
    private String defaultUsername;
    @Value("${spring.datasource.password}")
    private String defaultPassword;

    @Primary
    @Bean
    public DynamicRoutingDataSource dynamicDataSource() {
        DataSource defaultDataSource=DataSourceBuilder.create().url(defaultUrl).driverClassName(driverClassName).username(defaultUsername).password(defaultPassword).build();
        return new DynamicRoutingDataSource(defaultDataSource);
    }

}

Lea la configuración predeterminada de la base de datos Spring aquí:

spring: 
 datasource:
    url: jdbc:postgresql://${POSTGRES_HOST:119.167.159.211}:${POSTGRES_PORT:5432}/${POSTGRES_DATABASE:dzbz2_lgyz}
    username: ${POSTGRES_USERNAME:postgres}
    password: ${POSTGRES_PASSWORD:#5Rd!TC2CBA}

Cree una clase de origen de datos de enrutamiento dinámico con el siguiente código:


import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static org.springblade.dynamic.data.source.config.DataSourceContextHolder.DEFAULT_DATA_SOURCE;

/**
 * @author tarzan
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    public final ConcurrentHashMap<String,DataSource> dataSourcesMap = new ConcurrentHashMap<>();

    public DynamicRoutingDataSource(DataSource defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
        Map<Object, Object> targetDataSources=new HashMap<>(1);
        targetDataSources.put(DEFAULT_DATA_SOURCE,defaultDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
        this.dataSourcesMap.put(DEFAULT_DATA_SOURCE, defaultDataSource);
    }


    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }

    public void addDataSource(DataSourceInfo dataSourceInfo,boolean join) {
        DataSource dataSource = createDataSource(dataSourceInfo);
        Map<Object, Object> targetDataSources=new HashMap<>(1);
        if(join){
            targetDataSources.put(DEFAULT_DATA_SOURCE,dataSourcesMap.get(DEFAULT_DATA_SOURCE));
        }
        targetDataSources.put(dataSourceInfo.getName(),dataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
        dataSourcesMap.put(dataSourceInfo.getName(),dataSource);
    }

    public DataSource createDataSource(DataSourceInfo dataSourceInfo) {
        if(dataSourcesMap.containsKey(dataSourceInfo.getName())){
            return dataSourcesMap.get(dataSourceInfo.getName());
        }
        return DataSourceBuilder.create().url(dataSourceInfo.getUrl()).driverClassName(dataSourceInfo.getDriverClassName()).username(dataSourceInfo.getUserName()).password(dataSourceInfo.getPassword()).build();
    }
}

Clase de información de la base de datos DataSourceInfo, el código es el siguiente:

import lombok.AllArgsConstructor;
import lombok.Data;


@Data
@AllArgsConstructor
public class DataSourceInfo {
    private String name;
    private String driverClassName;
    private String url;
    private String userName;
    private String password;

}

Cree una clase de herramienta de fuente de datos DataSourceContextHolder, use el cambio de hilo de fuente de datos, el código es el siguiente: 

public class DataSourceContextHolder {

    public static final String DEFAULT_DATA_SOURCE = "defaultDataSource";
    /**
     * 线程级别的私有变量
     */
    private static final ThreadLocal<String> CURRENT_DATASOURCE_NAME = new ThreadLocal<>();

    public static String getDataSource() {
        return CURRENT_DATASOURCE_NAME.get();
    }

    /**
     * 设置数据源
     */
    public static void setDataSource(String datasourceId) {
        CURRENT_DATASOURCE_NAME.set(datasourceId);
    }

    /**
     * 删除数据源
     */
    public static void removeDataSource() {
        CURRENT_DATASOURCE_NAME.remove();
    }

    /**
     * 切换默认数据源
     */
    public static void switchDefaultDataSource() {
        CURRENT_DATASOURCE_NAME.set(DEFAULT_DATA_SOURCE);
    }
}

La clase de servicio de gestión de orígenes de datos de base de datos se utiliza para leer la información de configuración de orígenes de datos relacionados de la tabla de configuración de orígenes de datos de bases de datos, generar orígenes de datos, realizar cambios de orígenes de datos, replicación maestro-esclavo y otras operaciones.


import org.springblade.core.tool.utils.Func;
import org.springblade.dynamic.data.source.database.entity.Dbs;
import org.springblade.dynamic.data.source.database.service.IDbsService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.Objects;

/**
 * @author tarzan
 * @since JDK1.8
 */
@Service
public class DataSourceService {
    @Resource
    private IDbsService dbsService;
    @Resource
    private DynamicRoutingDataSource dynamicRoutingDataSource;

    public  void switchDataSource(Long dbsId) {
        if (dbsId != null && dbsId != 0) {
            DataSourceContextHolder.switchDefaultDataSource();
            Dbs dbs = dbsService.getById(Func.toLong(dbsId));
            switchDataSource(dbs);
        }
    }

    public void switchDataSource(Dbs dbs) {
            if (Func.isNull(dbs)) {
                throw new RuntimeException("未找到dbsId对应的数据库");
            }
            //创建数据源
            String url = MessageFormat.format("jdbc:postgresql://{0}:{1}/{2}", dbs.getIp(), dbs.getPort(), dbs.getDatabasename());
            DataSourceInfo dataSourceInfo = new DataSourceInfo(String.valueOf(dbs.getId()), "org.postgresql.Driver", url, dbs.getUsername(), dbs.getPassword());
            //加入多数据源列表
            dynamicRoutingDataSource.addDataSource(dataSourceInfo,false);
            DataSourceContextHolder.setDataSource(dataSourceInfo.getName());
    }

    public  void addTargetDataSource(Dbs dbs) {
        if (Func.isNull(dbs)) {
            throw new RuntimeException("未找到dbsId对应的数据库");
        }
        //创建数据源
        String url = MessageFormat.format("jdbc:postgresql://{0}:{1}/{2}", dbs.getIp(), dbs.getPort(), dbs.getDatabasename());
        DataSourceInfo dataSourceInfo = new DataSourceInfo("ms_"+ dbs.getId(), "org.postgresql.Driver", url, dbs.getUsername(), dbs.getPassword());
        dynamicRoutingDataSource.addDataSource(dataSourceInfo,true);
        DataSourceContextHolder.setDataSource(dataSourceInfo.getName());
    }

    public  void switchDataSource(String code) {
        if (Func.isNotBlank(code)) {
            DataSourceContextHolder.switchDefaultDataSource();
            Dbs dbs = getDbs(code);
            if (Objects.nonNull(dbs)) {
                switchDataSource(dbs);
            }else {
                throw new RuntimeException("未找到dbsCode对应的数据库");
            }
        }
    }

    public  void addTargetDataSource(String code) {
        if (Func.isNotBlank(code)) {
            DataSourceContextHolder.switchDefaultDataSource();
            Dbs dbs = getDbs(code);
            if (Objects.nonNull(dbs)) {
                addTargetDataSource(dbs);
            }else {
                throw new RuntimeException("未找到dbsCode对应的数据库");
            }
        }
    }

    private Dbs getDbs(String code) {
        if (Func.isNotBlank(code)) {
            return  dbsService.lambdaQuery().eq(Dbs::getIsDeleted, 0).eq(Dbs::getCode, code).last("limit 1").one();
        }else {
            return null;
        }
    }

    public synchronized void switchDefaultDataSource() {
        DataSourceContextHolder.switchDefaultDataSource();
    }




}

Nota: La clase IDbsService utilizada en esta clase es la clase de interfaz de servicio de la tabla de configuración de la fuente de datos de la base de datos. Debido al límite de longitud del artículo y la implementación relativamente simple, los códigos relevantes se omiten aquí. El código de entidad de base de datos de la clase Dbs se adjunta aquí, lo cual es conveniente para que todos diseñen tablas de base de datos.

Clase de entidad de la tabla de configuración de la fuente de datos DBS, el código es el siguiente:

package org.springblade.dynamic.data.source.database.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.mp.base.BaseEntity;

@Data
@TableName("gis_standard_dbs")
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "Dbs对象", description = "Dbs对象")
public class Dbs extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 连接展示名称
     */
    @ApiModelProperty(value = "连接展示名称")
    private String nickname;
    /**
     * 数据库ip
     */
    @ApiModelProperty(value = "数据库ip")
    private String ip;
    /**
     * 用户名
     */
    @ApiModelProperty(value = "用户名")
    private String username;
    /**
     * 密码
     */
    @ApiModelProperty(value = "密码")
    private String password;
    /**
     * 数据库端口
     */
    @ApiModelProperty(value = "数据库端口")
    private String port;
    /**
     * 数据库名
     */
    @ApiModelProperty(value = "数据库名")
    private String databaseName;

    @ApiModelProperty(value = "唯一标识")
    private String code;

}

Cree una anotación personalizada para especificar el origen de datos conectado en la transacción de operación de la base de datos del servicio o del asignador relacionado. el código se muestra a continuación:


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DbsAnnotation {
    String value() default "";
    boolean join() default false;
}

 Anotación personalizada aop aspecto DbsAspect, el código es el siguiente:


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springblade.core.tool.utils.Func;
import org.springblade.dynamic.data.source.annotations.DbsAnnotation;
import org.springblade.dynamic.data.source.config.DataSourceContextHolder;
import org.springblade.dynamic.data.source.config.DataSourceService;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 业务
 *
 * @author gaoqiang
 * @version 1.0
 * @company 北斗天地股份有限公司
 * @copyright (c) China Bdtd Co.LTD.All rights reserved.
 * @date 2022/9/15 16:11
 * @since JDK1.8
 */
@Order(1)
@Aspect
@Component
@Slf4j
public class DbsAspect {

    @Resource
    private DataSourceService dataSourceService;


    @Pointcut("@annotation(org.springblade.dynamic.data.source.annotations.DbsAnnotation)")
    private void dbsAnnotation() {
    }

    @Before("@annotation(dbs)")
    public void record(DbsAnnotation dbs) {
            if (Func.isNotBlank(dbs.value())) {
                if(dbs.join()){
                    log.info("添加目标数据源 "+dbs.value());
                    dataSourceService.addTargetDataSource(dbs.value());
                }else {
                    log.info("切换数据源 "+dbs.value());
                    dataSourceService.switchDataSource(dbs.value());
                }
            }
    }

    @After("dbsAnnotation()")
    public void after() {
        log.info("切换为默认数据源");
        DataSourceContextHolder.switchDefaultDataSource();
    }

}

Bueno, básicamente has terminado aquí, ¡déjame explicarte cómo usarlo! !

    @DbsAnnotation("master")
    public List<Ktzksj> listAll() {
       return this.list();
    }

El valor maestro en la anotación @DbsAnnotation debe corresponder al valor del campo de código en la tabla de configuración de la fuente de datos. Al ejecutar este método, puede cambiar a la fuente de datos maestra para agregar, eliminar, modificar y verificar la base de datos. Después se ejecuta el método, cambiará automáticamente a la fuente de datos predeterminada.

    @DbsAnnotation(value = "data_center",join = true)
    public void dataSync(List<TunnelBase> tunnelAddList,Long orgId) {
        if(CollectionUtils.isNotEmpty(tunnelAddList)){
            baseMapper.deleteByOrgId(orgId);
            super.saveBatch(tunnelAddList);
        }
    }

 @DbsAnnotation(value = "data_center", join = true) Una combinación más = true que la anterior, lo que significa agregar una fuente de datos de destino del centro de datos en la fuente de datos predeterminada, de modo que cuando los datos se agreguen, eliminen o modifiquen, el se pueden agregar datos Sincronizar con las tablas de la fuente de datos de destino y la fuente de datos del centro de datos al mismo tiempo, ¡siempre que las tablas de las dos fuentes de datos sean exactamente iguales! ! , ¡para que se realice la replicación maestro-esclavo! ! !

Supongo que te gusta

Origin blog.csdn.net/weixin_40986713/article/details/130961589
Recomendado
Clasificación