Is it difficult to separate reading from writing?SpringBoot is implemented simply with aop

Posted by BraniffNET on Tue, 12 May 2020 18:48:39 +0200

Preface

It's been a month since I joined the new company, I've finished the work at hand, and the last few days I finally have time to research the code for my old project.During the process of researching the code, I found that Spring Aop was used in the project to separate the database from reading and writing, and to learn in my own way (I don't believe in it myself).) character, decided to write an example project to achieve the effect of spring AOP read-write separation.

Environment Deployment

  • Database: MySql

  • Number of libraries: 2, one master and one slave

For mysql's master-slave environment deployment, you can refer to:

https://juejin.im/post/5dd13778e51d453da86c0e6f

Start Project

First, undoubtedly, start building a SpringBoot project and then introduce the following dependencies in the pom file:

<dependencies>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <!-- Required dependencies for dynamic data sources ### start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Required dependencies for dynamic data sources ### end-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.4</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

 

directory structure

After introducing the basic dependencies, sort out the catalog structure, and the project skeleton is roughly as follows:

Building tables

Create a table user that executes sql statements in the primary library while generating corresponding table data from the library

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `user_id` bigint(20) NOT NULL COMMENT 'user id',
  `user_name` varchar(255) DEFAULT '' COMMENT 'User Name',
  `user_phone` varchar(50) DEFAULT '' COMMENT 'User mobile phone',
  `address` varchar(255) DEFAULT '' COMMENT 'address',
  `weight` int(3) NOT NULL DEFAULT '1' COMMENT 'Weight, big takes precedence',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Creation Time',
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update Time',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES ('1196978513958141952', 'Test 1', '18826334748', 'Haizhu District, Guangzhou', '1', '2019-11-20 10:28:51', '2019-11-22 14:28:26');
INSERT INTO `user` VALUES ('1196978513958141953', 'Test 2', '18826274230', 'Tianhe District, Guangzhou', '2', '2019-11-20 10:29:37', '2019-11-22 14:28:14');
INSERT INTO `user` VALUES ('1196978513958141954', 'Test 3', '18826273900', 'Tianhe District, Guangzhou', '1', '2019-11-20 10:30:19', '2019-11-22 14:28:30');

 

Master-Slave Data Source Configuration

application.yml, the main information is the data source configuration of the master-slave database

server:
  port: 8001
spring:
  jackson:
      date-format: yyyy-MM-dd HH:mm:ss
      time-zone: GMT+8
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    master:
      url: jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:
    slave:
      url: jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
      username: root
      password:

 

Since there are two data sources, one master and one slave, we use enumeration classes instead to make it easier for us to use the same data source.

@Getter
public enum DynamicDataSourceEnum {
    MASTER("master"),
    SLAVE("slave");
    private String dataSourceName;
    DynamicDataSourceEnum(String dataSourceName) {
        this.dataSourceName = dataSourceName;
    }
}

 

Data source configuration information class DataSourceConfig, where two data sources are configured, master Db and slaveDb

@Configuration
@MapperScan(basePackages = "com.xjt.proxy.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {

     // Main Library
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource.master")
      public DataSource masterDb() {
  return DruidDataSourceBuilder.create().build();
      }

    /**
     * From Library
     */
    @Bean
    @ConditionalOnProperty(prefix = "spring.datasource", name = "slave", matchIfMissing = true)
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDb() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * Master-Slave Dynamic Configuration
     */
    @Bean
    public DynamicDataSource dynamicDb(@Qualifier("masterDb") DataSource masterDataSource,
        @Autowired(required = false) @Qualifier("slaveDb") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
        if (slaveDataSource != null) {
            targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("dynamicDb") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(
            new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }
    @Bean
    public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
    @Bean(name = "dataSourceTx")
    public DataSourceTransactionManager dataSourceTx(@Qualifier("dynamicDb") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }
}

 

Set Route

The purpose of routing is to make it easy to find the corresponding data source. We can use ThreadLocal to save the information of the data source to each thread so that we can get it when we need it

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_CONTEXT = new ThreadLocal<>();
    public static void set(String datasourceType) {
        DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
    }
    public static String get() {
        return DYNAMIC_DATASOURCE_CONTEXT.get();
    }
    public static void clear() {
        DYNAMIC_DATASOURCE_CONTEXT.remove();
    }
}

 

Get Routes

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

 

The purpose of AbstractRoutingDataSource is to route to the corresponding data source by finding the key. It maintains a set of target data sources internally, maps the routing key to the target data source, and provides a key-based method of finding the data source.For more springboot articles, check the past: SpringBoot Content Aggregation

Notes for data sources

To make it easy to switch data sources, we can write a comment that contains the corresponding enumeration values for the data sources. By default, it is the main library.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSelector {

    DynamicDataSourceEnum value() default DynamicDataSourceEnum.MASTER;
    boolean clear() default true;
}

 

aop switch data source

At this point, aop can finally appear. Here we define an aop class to switch data sources for annotated methods. The code is as follows:

@Slf4j
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {

 @Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")
    public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
        boolean clear = true;
        try {
            Method method = this.getMethod(pjp);
            DataSourceSelector dataSourceImport = method.getAnnotation(DataSourceSelector.class);
            clear = dataSourceImport.clear();
            DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());
            log.info("========Switch data source to:{}", dataSourceImport.value().getDataSourceName());
            return pjp.proceed();
        } finally {
            if (clear) {
                DataSourceContextHolder.clear();
            }

        }
    }
    private Method getMethod(JoinPoint pjp) {
        MethodSignature signature = (MethodSignature)pjp.getSignature();
        return signature.getMethod();
    }

}

 

At this point, we are ready to configure, and we will start testing the results below.For more springboot articles, check the past: SpringBoot Content Aggregation

Write the Service file first, which contains two methods of reading and updating.

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public List<User> listUser() {
        List<User> users = userMapper.selectAll();
        return users;
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.MASTER)
    public int update() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        user.setUserName("Modified Name 2");
        return userMapper.updateByPrimaryKeySelective(user);
    }

    @DataSourceSelector(value = DynamicDataSourceEnum.SLAVE)
    public User find() {
        User user = new User();
        user.setUserId(Long.parseLong("1196978513958141952"));
        return userMapper.selectByPrimaryKey(user);
    }
}

 

According to the annotations on the method, we can see that the reading method is from the library and the updating method is from the main library. The updating object is the data of userId 196978513958141953.

Then we'll write a test to see if it works.

@RunWith(SpringRunner.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    UserService userService;

    @Test
    void listUser() {
        List<User> users = userService.listUser();
        for (User user : users) {
            System.out.println(user.getUserId());
            System.out.println(user.getUserName());
            System.out.println(user.getUserPhone());
        }
    }
    @Test
    void update() {
        userService.update();
        User user = userService.find();
        System.out.println(user.getUserName());
    }
}

 

Test results:

1. Reading methods

2. Update Method

After execution, we can compare the databases and find that both master and slave libraries have modified the data, indicating that our read-write separation was successful.Of course, update methods can point to slave libraries so that only the data from the slave libraries will be modified, not the primary libraries.

Be careful

The example tested above is simple, but it also conforms to the general read-write separation configuration.It is worth mentioning that the role of read-write separation is to alleviate the pressure on the write libraries, that is, the master libraries, but it must be based on the principle of data consistency, that is, to ensure data consistency between the master and slave libraries.If a method involves write logic, all database operations in the method will be in the primary library.

Assuming that the data may not have been synchronized from the library after the write operation has been executed, and then the read operation begins to execute, if the program still reads from the library, data inconsistency will occur, which is not allowed by us.
Finally, send the github address of the project. Interested students can see:

https://github.com/Taoxj/mysql-proxy

Reference:

https://www.cnblogs.com/cjsblog/p/9712457.html

 

Author: Despicable Xue Mou

Link: juejin.im/post/5ddcd93af265da7dce3271de

Topics: Java Spring MySQL SpringBoot Database