データベースシリーズ-MySQLマスタースレーブ同期に基づく読み取りと書き込みの分離

前提

マスターおよびマルチスレーブデータベースクラスターを構築する前の記事https://blog.csdn.net/Coder_Boy_/article/details/110950347を参照して   ください

この記事のGithubコードアドレス:https//github.com/cheriduk/spring-boot-integration-template

 

この記事の核心:コードレベルでの読み取りと書き込みの分離

コード環境は、springboot + mybatis + druib接続プールです。読み取りと書き込みを分離する場合は、複数のデータソースを設定する必要があります。書き込み操作を行う場合は、書き込み用のデータソースを選択し、読み取り操作時に読み取り用のデータソースを選択してください。2つの重要なポイントがあります。

  1. データソースを切り替える方法
  2. さまざまな方法に従って適切なデータソースを選択する方法

1)データソースを切り替える方法

通常、springbootはデフォルトの構成を使用します。構成ファイルで接続プロパティを定義するだけで済みますが、今度は自分で構成する必要があります。Springは複数のデータソースをサポートし、複数のデータソースが1つのHashMapTargetDataSourceに配置されます。 dertermineCurrentLookupKeyを使用して、使用するデータソースを決定します。したがって、私たちの目標は非常に明確です。複数のデータソースを作成してTargetDataSourceに配置すると同時に、dertermineCurrentLookupKeyメソッドをオーバーライドして、使用するキーを決定します。

2)データソースの選択方法

トランザクションは通常、サービスレイヤーで注釈が付けられるため、サービスメソッド呼び出しを開始するときに、データソースを決定する必要があります。メソッドの実行を開始する前に操作を実行する一般的なメソッドはありますか?あなたはすでにそれが側面だと思っていると思います。カットする方法は2つあります。

  • アノテーション、読み取り専用アノテーションを定義し、データによってマークされたメソッドは読み取りライブラリを使用します
  • メソッド名、メソッド名に応じた書き込みカットポイント。たとえば、getXXXは読み取りライブラリを使用し、setXXXは書き込みライブラリを使用します。

3)、コードの記述

プロジェクトのディレクトリ構造:

a。構成ファイルを作成し、2つのデータソース情報を構成します

必要な情報のみ、その他のデフォルト設定

application.properties

mysql.datasource.config-location=classpath:/mybatis-config.xml
mysql.datasource.mapper-locations=classpath:/mapper/*.xml
mysql.datasource.num=2

mysql.datasource.read1.driverClass=com.mysql.jdbc.Driver
mysql.datasource.read1.password=123456
mysql.datasource.read1.url=jdbc:mysql://127.0.0.1:3307/test?serverTimezone=Asia/Shanghai
mysql.datasource.read1.username=root

mysql.datasource.read2.driverClass=com.mysql.jdbc.Driver
mysql.datasource.read2.password=123456
mysql.datasource.read2.url=jdbc:mysql://127.0.0.1:3308/test?serverTimezone=Asia/Shanghai
mysql.datasource.read2.username=root


mysql.datasource.write.driverClass=com.mysql.jdbc.Driver
mysql.datasource.write.password=123456
mysql.datasource.write.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=Asia/Shanghai
mysql.datasource.write.username=root

b、DbContextHolderクラスを記述します

このクラスは、データベースカテゴリを設定するために使用されます。各スレッドが読み取りライブラリを使用するか書き込みライブラリを使用するかを格納するためのThreadLocalがあります。コードは次のように表示されます。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Description 这里切换读/写模式
 * 原理是利用ThreadLocal保存当前线程是否处于读模式(通过开始READ_ONLY注解在开始操作前设置模式为读模式,
 * 操作结束后清除该数据,避免内存泄漏,同时也为了后续在该线程进行写操作时任然为读模式
 *
 * @author 杜康
 * @date 2020-08-31
 */
public class DbContextHolder {

    private static Logger log = LoggerFactory.getLogger(DbContextHolder.class);
    public static final String WRITE = "write";
    public static final String READ = "read";

    private static ThreadLocal<String> contextHolder= new ThreadLocal<>();

    public static void setDbType(String dbType) {
        if (dbType == null) {
            log.error("dbType为空");
            throw new NullPointerException();
        }
        log.info("设置dbType为:{}",dbType);
        contextHolder.set(dbType);
    }

    public static String getDbType() {
        return contextHolder.get() == null ? WRITE : contextHolder.get();
    }

    public static void clearDbType() {
        contextHolder.remove();
    }
}

c、determineCurrentLookupKeyメソッドを書き直します

Springはこのメソッドを使用して、データベース操作の開始時に使用するデータベースを決定するgetDbType()ため、上記DbContextHolderクラスのメソッドを呼び出して現在の操作カテゴリ取得すると同時に、読み取りライブラリの負荷分散を実行できます。コードは次のとおりです

package com.gary.dbrw.datachange;

import com.gary.dbrw.util.NumberUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {

    @Value("${mysql.datasource.num}")
    private int num;

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

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DbContextHolder.getDbType();
        if (typeKey.equals(DbContextHolder.WRITE)) {
            log.info("使用了写库w");
            return typeKey;
        }
        //使用随机数决定使用哪个读库(可写负载均衡算法)
        int sum = NumberUtil.getRandom(1, num);
        log.info("使用了读库r{}", sum);
        return DbContextHolder.READ + sum;
    }
}

d、構成クラスを書き込む

読み取りと書き込みが分離されているため、springbootのデフォルト構成は使用できなくなり、手動で構成する必要があります。最初にデータソースを生成し、@ ConfigurPropertiesを使用してデータソースを自動的に生成します。

package com.gary.dbrw.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.gary.dbrw.common.Properties;
import com.gary.dbrw.datachange.DbContextHolder;
import com.gary.dbrw.datachange.MyAbstractRoutingDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

@MapperScan(basePackages = "com.gary.dbrw.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
@Configuration
public class DataSourceConfig {

//    @Value("${mysql.datasource.type-aliases-package}")
//    private String typeAliasesPackage;
//
//    @Value("${mysql.datasource.mapper-locations}")
//    private String mapperLocation;
//
//    @Value("${mysql.datasource.config-location}")
//    private String configLocation;

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

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


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

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

    /**
     * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
        targetDataSources.put(DbContextHolder.READ + "1", read1());
        proxy.setDefaultTargetDataSource(writeDataSource());
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

}

 

読み取りライブラリの数に対して、読み取りデータソースの数を設定する必要があることに注意してください。Bean名は読み取り+シリアル番号です。

最後に、前に作成したMyAbstractRoutingDataSourceクラスを使用して、データソースを設定します

  /**
     * 设置数据源路由,通过该类中的determineCurrentLookupKey决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>(2);
        targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
        targetDataSources.put(DbContextHolder.READ + "1", read1());
        proxy.setDefaultTargetDataSource(writeDataSource());
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

次に、sqlSessionFactoryを設定し、MyBatisのsqlSessionFactoryRefリファレンスを構成する必要があります

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

最後に、トランザクションを構成する必要があります。構成しないと、トランザクションが有効になりません。

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

 

4)、データソースを選択します

複数のデータソースが構成されていますが、コードレベルでデータソースを選択するにはどうすればよいですか?

人気のある注釈を使用してアスペクトを追加します。

最初に読み取り専用アノテーションを定義します。このアノテーションメソッドは読み取りライブラリを使用し、他のメソッドは書き込みライブラリを使用します。プロジェクトが途中で読み取り/書き込み分離に変換される場合は、このメソッドを使用できます。ビジネスコードを変更する必要はありません。 、読み取り専用のサービスメソッドにアノテーションを追加するだけです。


@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

次に、データに使用するデータソースを切り替えるアスペクトを記述し、getOrderを書き換えて、このアスペクトの優先度がトランザクションアスペクトの優先度よりも高くなるようにします(トランザクションを使用できるようにするため)。@EnableTransactionManagement(order = 10)これをスタートアップクラスに追加します。次のようなコードの場合:

@Aspect
@Component
public class ReadOnlyInterceptor implements Ordered {
    private static final Logger log = LoggerFactory.getLogger(ReadOnlyInterceptor.class);

    @Around("@annotation(readOnly)")
    public Object setRead(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
        try {
            DbContextHolder.setDbType(DbContextHolder.READ);
            return joinPoint.proceed();
        } finally {
            //清楚DbType一方面为了避免内存泄漏,更重要的是避免对后续在本线程上执行的操作产生影响
            DbContextHolder.clearDbType();
            log.info("清除threadLocal");
        }
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

テストクラスを構成します。


@Service
public class StudentServiceImpl implements StudentService {
    @Resource
    StudentMapper studentMapper;

    @ReadOnly
    @Override
    public List<Student> selectAllList() {
        return studentMapper.selectAll();
    }


    @Override
    public int addOneStudent(Student student) {
        return studentMapper.insertSelective(student);
    }
}
@RestController
@RequestMapping("user")
public class StudentController {

    @Resource
    StudentService studentService;

    @GetMapping("/testRW")
    public String DbRead(Integer dbType) {
        System.out.println("dbType=:" + dbType);
        List<Student> students = studentService.selectAllList();
        return "ReadDB=>" + students;
    }

    @PostMapping("/testRW")
    public String DbWrite(Student student) {
        int count = studentService.addOneStudent(student);
        return "Wr DB=>" + count;
    }
}

4.テスト検証

結果を試すためのコードを記述します。以下は操作のスクリーンショットです。

 


データベース変更ビュー:


 

 

 

 

おすすめ

転載: blog.csdn.net/Coder_Boy_/article/details/111036260