springBoot+myBatis configura a separação de leitura e gravação com base no mysql

1. O que é a separação de leitura/gravação do banco de dados?

A separação de leitura-gravação do banco de dados é dividir o banco de dados em uma biblioteca mestre-escravo, e a biblioteca mestre é usada para gravar dados e manter os dados adicionando, excluindo e modificando-os. A biblioteca escrava sincroniza os dados da biblioteca principal por meio de um mecanismo de sincronização, ou seja, um backup espelhado completo da biblioteca principal. A separação de leitura e gravação geralmente é para configurar uma biblioteca mestre com várias bibliotecas escravas, que é um projeto de arquitetura de banco de dados comum.

2. Qual problema a separação de leitura e escrita do banco de dados resolve?

A separação de leitura e gravação do banco de dados é principalmente para resolver o gargalo do desempenho da leitura de dados nos negócios. Como a maioria dos sistemas geralmente lê mais e escreve menos, neste momento, a operação de leitura primeiro se tornará o gargalo do serviço de banco de dados e, indiretamente, causará problemas na gravação do banco de dados. Geralmente, o sistema de Internet adota uma arquitetura de design de banco de dados único no início, ou seja, um banco de dados comum para leitura e gravação. No entanto, com a expansão dos negócios e o aumento de usuários do sistema, a leitura e gravação de um único banco de dados será preso, e até mesmo as operações de negócios irão falhar. Neste momento, a ideia de design de separação de leitura e escrita pode atender às necessidades do sistema até certo ponto. O uso adequado da arquitetura do banco de dados com separação de leitura e gravação pode melhorar linearmente o gargalo de desempenho das operações de leitura do banco de dados e, ao mesmo tempo, resolver o impacto dos conflitos de bloqueio de leitura e gravação nas operações de gravação e melhorar o desempenho das operações de gravação.

Observação: a separação de leitura e gravação é apenas uma ideia de design para resolver temporariamente o conflito de desempenho do banco de dados devido ao aumento no volume de negócios e usuários do sistema até certo ponto, mas não pode resolver completamente esse problema. Quando o tamanho do sistema é grande o suficiente, a separação leitura-gravação não será capaz de lidar com os problemas de desempenho do sistema. Neste momento, outras ideias de design são necessárias para resolvê-lo, como sub-biblioteca de banco de dados, partição e sub-segmentação de tabela.

3. Implementação do código de separação de leitura e escrita (springboot+mybatis+mysql)

1. Primeiro, você precisa configurar a biblioteca mestre-escravo do myslql. Para esta configuração, consulte este artigo escrito anteriormente.

2. Implementação de código
1. Crie um novo projeto springboot no Idea

2. Adicione as seguintes informações de configuração de injeção de dependência a pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 使用TkMybatis可以无xml文件实现数据库操作,只需要继承tkMybatis的Mapper接口即可-->
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>1.1.4</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.9</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. Crie um novo arquivo application.yml no diretório src/main/resources e configure as seguintes informações

server:
  port: 8082
  servlet:
    context-path: /dxfl
spring:
  datasource:
    #读库数目
    maxReadCount: 1
    type-aliases-package: com.teamo.dxfl.mapper
    mapper-locations: classpath:/mapper/*.xml
    config-location: classpath:/mybatis-config.xml
    write:
      url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
      username: root
      password:
      driver-class-name: com.mysql.jdbc.Driver
      initialSize: 2           #初始化大小
      maxWait: 6000        #获取连接时最大等待时间,单位毫秒。
      min-idle: 5            # 数据库连接池的最小维持连接数
      maxActive: 20         # 最大的连接数
      initial-size: 5          # 初始化提供的连接数
      max-wait-millis: 200    # 等待连接获取的最大超时时间
    read1:
      url: jdbc:mysql://localhost:3307/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
      username: root
      password:
      driver-class-name: com.mysql.jdbc.Driver
      initialSize: 2          #初始化大小
      maxWait: 6000       #获取连接时最大等待时间,单位毫秒。
      min-idle: 5           # 数据库连接池的最小维持连接数
      maxActive: 20        # 最大的连接数
      initial-size: 5         # 初始化提供的连接数
      max-wait-millis: 200   # 等待连接获取的最大超时时间

4. Escreva a classe de configuração da fonte de dados (no diretório config)
DataSourceConfig.java

@Configuration
public class DataSourceConfig {
    @Value("${spring.datasource.type-aliases-package}")
    private String typeAliasesPackage;

    @Value("${spring.datasource.mapper-locations}")
    private String mapperLocation;

    @Value("${spring.datasource.config-location}")
    private String configLocation;
    /**
     * 写数据源
     * @Primary 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
     * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.write")
    public DataSource writeDataSource() {
        return new DruidDataSource();
    }

    /**
     * 读数据源
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.read1")
    public DataSource readDataSource1() {
        return new DruidDataSource();
    }

    /**
     * 多数据源需要自己设置sqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(routingDataSource());
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 实体类对应的位置
        bean.setTypeAliasesPackage(typeAliasesPackage);
        // mybatis的XML的配置
        bean.setMapperLocations(resolver.getResources(mapperLocation));
        bean.setConfigLocation(resolver.getResource(configLocation));
        return bean.getObject();
    }

    /**
     * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        RoutingDataSourceConfig proxy = new RoutingDataSourceConfig();

        DataSource writeDataSource = writeDataSource();

        //设置数据源Map对象
        Map<Object, Object> dataSource = new HashMap<Object, Object>(2);
        dataSource.put(DataBaseTypeEnum.WRITE.getCode(), writeDataSource);

        //如果配置了多个读数据源,就一次添加到datasource对象中
        dataSource.put(DataBaseTypeEnum.READ.getCode()+"1", readDataSource1());

        //写数据源设置为默认数据源
        proxy.setDefaultTargetDataSource(writeDataSource);
        proxy.setTargetDataSources(dataSource);
        return proxy;
    }

    /**
     * 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理(配置mybatis时候用到)
    */
     @Bean
     public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
     }
}

5. Escreva uma classe de configuração para roteamento da fonte de dados (sob o diretório config), que herda AbstractRoutingDataSource e reescreve o método determineCurrentLookupKey(), que implementa a lógica da seleção da fonte de dados. Ao mesmo tempo, essa classe contém um objeto ThreadLocal, que é usado para armazenar se o tipo de fonte de dados usado pelo thread atual é de leitura ou gravação. Você pode escrever um determinado algoritmo sozinho para realizar o balanceamento de carga da biblioteca de leitura (adicionando à biblioteca de leitura e configurando mais de uma) Aqui, é simples escolher qual fonte de dados do banco de dados de leitura usar obtendo números aleatórios.
DataSourceRouteConfig.java

public class RoutingDataSourceConfig extends AbstractRoutingDataSource {
    //使用ThreadLocal对象保存当前线程是否处于读模式
    private static ThreadLocal<String> DataBaseMap= new ThreadLocal<>();

    @Value("${spring.datasource.maxReadCount}")
    private int maxReadCount;

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = getDataBaseType();
        if (typeKey == DataBaseTypeEnum.WRITE.getCode()) {
            log.info("使用了写库");
            return typeKey;
        }
        //使用随机数决定使用哪个读库
        int index = (int) Math.floor(Math.random() * (maxReadCount - 1 + 1)) + 1;;
        log.info("使用了读库{}", index);
        return DataBaseTypeEnum.READ.getCode() + index;
    }

    public static void setDataBaseType(String dataBaseType) {
        DataBaseMap.set(dataBaseType);
    }

    public static String getDataBaseType() {
        return DataBaseMap.get() == null ? DataBaseTypeEnum.WRITE.getCode() : DataBaseMap.get();
    }

    public static void clearDataBaseType() {
        DataBaseMap.remove();
    }
}

6. Precisamos escrever uma classe Annotation, que é especialmente usada para marcar quais métodos da classe Service usam a fonte de dados de leitura e quais métodos usam a fonte de dados de gravação. (Além deste método, você também pode configurar o nome do método correspondente na classe aop, como curinga save*, update*, delete* métodos para corresponder à fonte de dados de gravação, outros métodos são fontes de dados de leitura, considerados apenas aqui Anotação método de anotação).

ReadDB.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadDB {
    //String value() default "";
}

7. Escreva a classe aop. Antes de solicitar a abertura da transação, use aop (programação orientada a aspectos) do spring para primeiro julgar se a anotação ReadDB está no método Service. Se a anotação ReadDB estiver no método, selecione a fonte de dados de leitura . Esta classe implementa a interface Ordered e reescreve o método getOrder(). Esta interface notifica o Spring para chamar a classe para executar a ordem do método. Quanto menor o valor retornado pelo método getOrder(), maior a prioridade. Cabe ressaltar aqui que o valor retornado pelo método getOrder() deve ser menor que o valor definido para @EnableTransactionManagement(order= ) na classe de inicialização do projeto , de forma a garantir que a operação de seleção de uma fonte de dados seja executada antes iniciando a transação.
DxflApplication.java

@SpringBootApplication
@EnableTransactionManagement(order = 5)
public class DxflApplication {
    public static void main(String[] args) {
        SpringApplication.run(DxflApplication.class, args);
    }
}

ReadDBAspect.java

@Aspect
@Component
public class ReadDBAspect implements Ordered {
    private static final Logger log= LoggerFactory.getLogger(ReadDBAspect.class);
    @Around("@annotation(readDB)")
    public Object setRead(ProceedingJoinPoint joinPoint, ReadDB readDB) throws Throwable {
        try{
            //设置读数据源
            RoutingDataSourceConfig.setDataBaseType(DataBaseTypeEnum.READ.getCode());
            return joinPoint.proceed();
        }finally {
            log.info("清除dataSource类型选中值:"+DataBaseTypeEnum.READ.getData());
            RoutingDataSourceConfig.clearDataBaseType();
        }
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

8. Escreva o método Service, adicione a anotação @ReadDB ao método de leitura de dados e marque o método como o método de leitura do banco de dados.
UserServiceImpl.java

@Service
public class UserviceServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    @ReadDB
    public List<User> getAllUser() {
        return userMapper.selectAll();
    }

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public boolean save(User user){
        if(userMapper.insert(user)>0){
            return true;
        }else{
            return false;
        }
        //throw new RuntimeException("测试事务");
    }
}

9. Escreva a classe Controller para chamar o método Service para implementar o teste dos métodos read e write.
UserController.java

@RestController
@RequestMapping("/user")
public class UserController{
    @Autowired
    private UserService userService;

    @GetMapping("getAllUser")
    public List<User> getAllUser() {
        List resList = userService.getAllUser();
        return resList;
    }

    @PostMapping("save")
    public Result save(@RequestBody User user){
        Result result = new Result();
        if(null != user){
            try{
                userService.save(user);
                result.setCode(ResultEnum.SUCCESS.getCode());
                result.setMsg("保存用户成功!");
            }catch(Exception e) {
                e.printStackTrace();
                result.setCode(ResultEnum.SUCCESS.getCode());
                result.setMsg("保存用户失败,原因:"+ e.getMessage() +"!");
            }
        }else{
            result.setCode(ResultEnum.SUCCESS.getCode());
            result.setMsg("保存用户失败,原因:用户数据为空!");
        }
        return result;
    }
}

10. Teste
Após o serviço ser iniciado com sucesso, teste a leitura da biblioteca: digite o endereço no navegador, http://localhost:8089/dxfl/user/getAllUser, a exibição é a seguinte: Verifique a impressão em segundo plano: mostra
insira a descrição da imagem aquique a biblioteca é usada para
insira a descrição da imagem aquitestar o teste da biblioteca e
escrever a biblioteca O envio do formulário é necessário e uma ferramenta de teste é necessária neste momento, e o ApiPost é usado aqui. Abra a ferramenta de teste, entre na interface para salvar o usuário na coluna de teste, selecione application/json como o método de envio e, em seguida, clique no botão enviar e uma mensagem de retorno bem-sucedida será exibida com uma resposta e um prompt para salvar o usuário significa que o teste de escrita foi bem-sucedido.
insira a descrição da imagem aquiAs informações de impressão do log de back-end são: a biblioteca de gravação é usada e a biblioteca de gravação de teste de nome da tabela é bem-sucedida.
insira a descrição da imagem aquiPara verificar com sucesso se a separação leitura-gravação é realmente bem-sucedida, você pode primeiro desligar a função da biblioteca escrava total (parar escravo) ao testar a biblioteca de gravação e, em seguida, ativar a função da biblioteca mestre-escravo ( start slave) depois de garantir que a biblioteca de gravação especificada seja gravada.

Acho que você gosta

Origin blog.csdn.net/teamo_m/article/details/105794561
Recomendado
Clasificación