springboot separate read and write the configuration database

Why do read and write separate database

Most Internet business, less often reading and writing, this time, the database will first read a database called the bottleneck, then, if we want to be able to read linear performance improvement of the database, eliminating the read-write lock conflicts so as to enhance database write performance, then you can use "packet architecture" (separate read and write architecture).

In a word, separate read and write is used to solve the bottleneck of database read performance.

solution

  • Acting tools, such as: MySQL Proxy, Mycat
  • Coding within the project (described in this article)

step

Add rely on the support

<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>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

Add configuration

spring:
  datasource:
    master:
      pool-name: master
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/payment
      username: user_RW
      password: 123456
      maximum-pool-size: 10
      minimum-idle: 5
    slave:
    - pool-name: slave1
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/payment
      username: user_R
      password: 123456
      maximum-pool-size: 10
      minimum-idle: 5
    - pool-name: slave2
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://127.0.0.1:3306/payment
      username: user_R
      password: 123456
      maximum-pool-size: 10
      minimum-idle: 5

Configure the data source, transaction management

import com.ssic.payment.core.datasource.DSNames;
import com.ssic.payment.core.datasource.DataSourceAspect;
import com.ssic.payment.core.datasource.DynamicDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

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

@Configuration
@EnableTransactionManagement(order = 100)
@ConditionalOnClass(JdbcOperations.class)
@ConditionalOnProperty(prefix = "spring.datasource", name = "master.jdbc-url")
public class DataSourceConfig {

    @Bean
    public DataSourceAspect DataSourceAspect(){
        return new DataSourceAspect();
    }

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix="spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slave1DataSource")
    @ConfigurationProperties(prefix="spring.datasource.slave[0]")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slave2DataSource")
    @ConfigurationProperties(prefix="spring.datasource.slave[1]")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "dataSource")
    @Qualifier(value = "dataSource")
    @DependsOn({"masterDataSource", "slave1DataSource", "slave2DataSource"})
    public DataSource dynamicDataSource() {

        DataSource masterDataSource = masterDataSource();
        DataSource slave1DataSource = slave1DataSource();
        DataSource slave2DataSource = slave2DataSource();

        Map<Object, Object> targetDataSources = new HashMap<>(3);
        targetDataSources.put(DSNames.MASTER, masterDataSource);
        targetDataSources.put(DSNames.SLAVE1, slave1DataSource);
        targetDataSources.put(DSNames.SLAVE2, slave2DataSource);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);

        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

}

Coding section interception interfaces

DataSourceAspect.java

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;

@Slf4j
@Aspect
@Order(1)
public class DataSourceAspect {

    @Pointcut("@annotation(com.ssic.payment.core.datasource.Slave) && execution(* com.ssic.payment.*.service.impl.*.*(..))")
    public void readPointcut() { }

    @Pointcut("@annotation(com.ssic.payment.core.datasource.Master) && execution(* com.ssic.payment.*.service.impl.*.*(..))")
    public void writePointcut() { }

    @Before("readPointcut()")
    public void readBefore(JoinPoint joinPoint) {
        DSContextHolder.slave();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} USE DATASOURCE SLAVE", className, methodName);
    }

    // 关闭这个方法,运行开启事务的测试类,会报错,
    // 因为测试用例的事务早于了切面执行,导致主从切换错误
    // 解决办法:切面退出前复位数据源到master
    @After("readPointcut()")
    public void readAfter(JoinPoint joinPoint) {
        DSContextHolder.master();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} RESET DATASOURCE MASTER", className, methodName);
    }

    @Before("writePointcut()")
    public void writeBefore(JoinPoint joinPoint) {
        DSContextHolder.master();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} USE DATASOURCE MASTER", className, methodName);
    }

}

Master.jav

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Master {
}

Slave.java

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Slave {
}

DSNames.java

public enum DSNames {
    // 这种方法并不好,见后面的改进说明
    MASTER, SLAVE1, SLAVE2;
}

DSContextHolder.java

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class DSContextHolder {

    private static final ThreadLocal<DSNames> contextHolder = new ThreadLocal<>();

    private static final AtomicInteger counter = new AtomicInteger(-1);

    public static void set(DSNames dsType) {
        contextHolder.set(dsType);
    }

    public static DSNames get() {
        return contextHolder.get();
    }

    public static void master() {
        set(DSNames.MASTER);
    }

    public static void slave() {
        int index = counter.getAndIncrement() % 2;
        if (counter.get() > 9999) {
            counter.set(-1);
        }
        if (index == 0) {
            set(DSNames.SLAVE1);
        }else {
            set(DSNames.SLAVE2);
        }
    }

}

DynamicDataSource.java

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DSContextHolder.get();
    }

}

unit test

import com.ssic.payment.accounting.Application;
import com.ssic.payment.accounting.domain.Demo;
import com.ssic.payment.accounting.domain.enums.DemoStatus;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;

@Slf4j
@Transactional //测试完成回滚数据
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class DemoServiceTest {

    @Autowired
    public DemoService service;

    @Test
    public void CRUDTest() {

        //CREATE
        Demo o = new Demo();
        o.setTitle("test");
        o.setAmount(1);
        o.setCreateTime(new Date());
        o.setStatus(DemoStatus.UNPAY);
        o.setCreateTime(new Date());
        service.save(o);
        Assert.assertNotNull(o.getId());

        //READ
        o = service.findById(o.getId());
        Assert.assertNotNull(o.getId());

        //UPDATE
        o.setTitle("CRUDTest1");
        service.save(o);
        o = service.findById(o.getId());
        Assert.assertTrue(o.getTitle().equals("CRUDTest1"));

        //DELETE
        service.delete(o.getId());
        o = service.findById(o.getId());
        Assert.assertNull(o);

    }

}

note

  • Default master library (in order to test the support at the time of the transaction)
  • Aop section after switching the data source needs to exit, the data source to the master reset
  • Use Order executed before the specified cut priorities

Improve

  • From the number of the bank is hard-coded into the system, and then to switch DSNames, this is not easy dynamic configuration can be dynamically configured to transform the

DataSourceConfig.java

import com.ssic.payment.core.datasource.DSNames;
import com.ssic.payment.core.datasource.DataSourceAspect;
import com.ssic.payment.core.datasource.DynamicDataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@EnableTransactionManagement(order = 100)
@ConditionalOnClass(JdbcOperations.class)
@EnableConfigurationProperties({MutiDataSourceProperties.class})
@ConditionalOnProperty(prefix = "spring.datasource", name = "master.jdbc-url")
public class DataSourceConfig {

    @Resource
    MutiDataSourceProperties mutiDataSourceProperties;

    @Bean
    public DataSourceAspect DataSourceAspect(){
        List<String> slaves = new ArrayList<>();
        for(Map<String, Object> prop : mutiDataSourceProperties.getSlave()){
            slaves.add(prop.get("pool-name").toString());
        }
        return new DataSourceAspect(slaves);
    }

    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix="spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "dataSource")
    @Qualifier(value = "dataSource")
    @DependsOn({"masterDataSource"})
    public DataSource dynamicDataSource() {

        HikariDataSource masterDataSource = (HikariDataSource) masterDataSource();

        Map<Object, Object> targetDataSources = new HashMap<>(3);
        targetDataSources.put(DSNames.MASTER.name(), masterDataSource);

        // 添加读库
        for(Map<String, Object> prop : mutiDataSourceProperties.getSlave()){
            HikariDataSource ds = buildDataSource(prop, true);
            targetDataSources.put(ds.getPoolName(), ds);
        }

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);

        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

    private HikariDataSource buildDataSource(Map<String, Object> map, Boolean readOnly){

        String poolNameKey = "pool-name";
        String driverClassNameKey = "driver-class-name";
        String jdbcUrlKey = "jdbc-url";
        String usernameKey = "username";
        String passwordKey = "password";
        String maximumPoolSizeKey = "maximum-pool-size";
        String minimumIdleKey = "minimum-idle";

        HikariDataSource dataSource = new HikariDataSource();
        if(map.containsKey(poolNameKey)){
            dataSource.setPoolName(map.get(poolNameKey).toString());
        }
        if(map.containsKey(driverClassNameKey)){
            dataSource.setDriverClassName(map.get(driverClassNameKey).toString());
        }
        if(map.containsKey(jdbcUrlKey)){
            dataSource.setJdbcUrl(map.get(jdbcUrlKey).toString());
        }
        if(map.containsKey(usernameKey)){
            dataSource.setUsername(map.get(usernameKey).toString());
        }
        if(map.containsKey(passwordKey)){
            dataSource.setPassword(map.get(passwordKey).toString());
        }
        if(map.containsKey(maximumPoolSizeKey)){
            dataSource.setMaximumPoolSize(Integer.parseInt(map.get(maximumPoolSizeKey).toString()));
        }
        if(map.containsKey(minimumIdleKey)){
            dataSource.setMinimumIdle(Integer.parseInt(map.get(minimumIdleKey).toString()));
        }
        dataSource.setReadOnly(readOnly);

        return dataSource;
    }

}

@ConfigurationProperties(prefix = "spring.datasource")
class MutiDataSourceProperties {

    private Map<String, Object> master;
    private List<Map<String, Object>> slave;

    public Map<String, Object> getMaster() {
        return master;
    }

    public void setMaster(Map<String, Object> master) {
        this.master = master;
    }

    public List<Map<String, Object>> getSlave() {
        return slave;
    }

    public void setSlave(List<Map<String, Object>> slave) {
        this.slave = slave;
    }

}

DataSourceAspect.java

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import java.util.List;

@Slf4j
@Aspect
@Order(1)
public class DataSourceAspect {

    /**
     * 切面启动时设置数据源从库列表名称
     * @param slaves
     */
    public DataSourceAspect(List<String> slaves){
        DSContextHolder.setSlaves(slaves);
    }

    @Pointcut("@annotation(com.ssic.payment.core.datasource.Slave) && execution(* com.ssic.payment.*.service.impl.*.*(..))")
    public void readPointcut() { }

    @Pointcut("@annotation(com.ssic.payment.core.datasource.Master) && execution(* com.ssic.payment.*.service.impl.*.*(..))")
    public void writePointcut() { }

    @Before("readPointcut()")
    public void readBefore(JoinPoint joinPoint) {
        DSContextHolder.slave();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} USE DATASOURCE SLAVE", className, methodName);
    }

    @After("readPointcut()")
    public void readAfter(JoinPoint joinPoint) {
        DSContextHolder.master();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} RESET DATASOURCE MASTER", className, methodName);
    }

    @Before("writePointcut()")
    public void writeBefore(JoinPoint joinPoint) {
        DSContextHolder.master();
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        log.debug("{}.{} USE DATASOURCE MASTER", className, methodName);
    }

}

DSContextHolder.java

import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
public class DSContextHolder {

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

    private static final AtomicInteger counter = new AtomicInteger(0);

    private static List<String> slaves;

    public static void setSlaves(List<String> slaves) {
        DSContextHolder.slaves = slaves;
    }

    public static void set(String dsName) {
        contextHolder.set(dsName);
    }

    public static String get() {
        return contextHolder.get();
    }

    public static void master() {
        set(DSNames.MASTER.name());
    }

    public static void slave() {
        if(slaves.size()>0){
            int index = counter.getAndIncrement() % slaves.size();
            if (counter.get() > 9999) {
                counter.set(-1);
            }
            set(slaves.get(index));
        } else {
            master();
        }
    }

}

DSNames.java

public enum DSNames {
    MASTER, SLAVE;
}

Want to know more details, please refer to this project:

https://github.com/bestaone/HiAuth

Guess you like

Origin blog.csdn.net/weixin_34195364/article/details/90928918