SoFunction
Updated on 2025-03-08

Detailed explanation of database read and write separation in Spring

1. Background

Most systems read more and write less. In order to reduce the pressure on the database, multiple slave libraries can be created for the master library. The slave library can automatically synchronize data from the master library. The program sends the written operations to the master library and sends the read operations to the slave library for execution.

Today's main goal: realize read-write separation through spring.

Reading and writing separation requires the following 2 functions:

1. The read method is controlled by the caller to control whether to read the slave library or the master library.

2. There are transactional methods, and all internal read and write operations are connected to the main library.

2. Think about 3 questions

The read method is controlled by the caller to control whether it is read from the slave library or the main library. How to implement it?

You can add a parameter to all read methods to control whether to read the slave or master library.

How to route data sources?

The spring-jdbc package provides an abstract class: AbstractRoutingDataSource, which implements an interface. We use this class as the data source class. The focus is that this class can be used to route data sources. Multiple real data sources can be configured inside it. The developers will decide which data source to use in the end.

There is a map in AbstractRoutingDataSource, which is used to store multiple target data sources.

private Map<Object, DataSource> resolvedDataSources;

For example, the master-slave library can be stored in this way

("master",Main library data source);
("salave",From the library data source);

There is also an abstract method determineCurrentLookupKey in AbstractRoutingDataSource. Use the return value of this method as key to find the corresponding data source in the resolvedDataSources above, as the data source for the current operation db

protected abstract Object determineCurrentLookupKey();

Where to control the separation of reading and writing?

Read and write separation is a general function, which can be implemented through spring aop, add an interceptor, before intercepting the target method, get which library you need to go to, store this flag in ThreadLocal, use this flag as the return value of the () method, and clear this flag from ThreadLocal after the target method is executed in the interceptor.

3. Code implementation

DsType

Represents the data source type, with 2 values ​​to distinguish whether it is a master or slave library.

package com.;
public enum DsType {
    MASTER, SLAVE;
}

DsTypeHolder

There is a ThreadLocal internally to record whether the current master library or slave library is used. Put this flag in dsTypeThreadLocal

package com.;
public class DsTypeHolder {
    private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>();
    public static void master() {
        ();
    }
    public static void slave() {
        ();
    }
    public static DsType getDsType() {
        return ();
    }
    public static void clearDsType() {
        ();
    }
}

IService Interface

This interface serves as a flag. When a certain class needs to enable read-write separation, it needs to implement this interface. The classes that implement this interface will be intercepted by the read-write separation interceptor.

package com.;
//The service that needs to implement read and write separation needs to implement this interfacepublic interface IService {
}

ReadWriteDataSource

Read and write to separate the data source, inherit ReadWriteDataSource, pay attention to its internal determinationCurrentLookupKey method, and obtain the flags that currently need to go to the master library or the slave library from the above ThreadLocal.

package com.;
import ;
import ;
public class ReadWriteDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return ();
    }
}

ReadWriteInterceptor

Read and write separation interceptors need to be executed in front of the transaction interceptor. Through the @1 code, we set the order of this interceptor to Integer.MAX_VALUE - 2. Later, we set the order of the transaction interceptors to Integer.MAX_VALUE - 1. The execution order of the transaction interceptors is reached from a small amount, so ReadWriteInterceptor will execute before the transaction interceptor.

Since there are mutual calls in business methods, such as service2.m2 is called in service1.m1, and service2.m3 is called in service2.m2, we only need to obtain which data source to use before the m1 method is executed. Therefore, the following code will record whether to go to the master library or the slave library when entering this interceptor for the first time.

The following method will obtain the last parameter of the current target method. The last parameter can be of type DsType. Developers can use this parameter to control whether to go to the main library or the slave library.

package com.;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
@Aspect
@Order(Integer.MAX_VALUE - 2) //@1
@Component
public class ReadWriteInterceptor {
    @Pointcut("target(IService)")
    public void pointcut() {
    }
    //Get the last parameter of the current target method    private Object getLastArgs(final ProceedingJoinPoint pjp) {
        Object[] args = ();
        if ((args) &amp;&amp;  &gt; 0) {
            return args[ - 1];
        } else {
            return null;
        }
    }
    @Around("pointcut()")
    public Object around(final ProceedingJoinPoint pjp) throws Throwable {
        //Judge whether it is the first time I come in, used to handle transaction nesting        boolean isFirst = false;
        try {
            if (() == null) {
                isFirst = true;
            }
            if (isFirst) {
                Object lastArgs = getLastArgs(pjp);
                if ((lastArgs)) {
                    ();
                } else {
                    ();
                }
            }
            return ();
        } finally {
            //When exiting, clean up            if (isFirst) {
                ();
            }
        }
    }
}

ReadWriteConfiguration

spring configuration class, function

1. @3: Used to register some classes in the com. package into the spring container, such as the interceptor above ReadWriteInterceptor

2. @1: Turn on the function of spring aop

3. @2: Enable spring to automatically manage transactions. The order of @EnableTransactionManagement is used to specify the order of transaction interceptors. Here we set order to Integer.MAX_VALUE - 1, and the order of ReadWriteInterceptor above is Integer.MAX_VALUE - 2, so ReadWriteInterceptor will be executed before the transaction interceptor.

package com.;
import ;
import ;
import ;
import ;
@Configuration
@EnableAspectJAutoProxy //@1
@EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2
@ComponentScan(basePackageClasses = ) //@3
public class ReadWriteConfiguration {
}

@EnableReadWrite

This annotation uses two to enable the function of read-write separation. @1 imports ReadWriteConfiguration into the spring container through @Import, so that the function of read-write separation will be automatically enabled. You need to use read and write separation in your business, just add the @EnableReadWrite annotation to the spring configuration class.

package com.;
import ;
import .*;
@Target()
@Retention()
@Documented
@Import() //@1
public @interface EnableReadWrite {
}

4. Case

The key code for separation of reading and writing has been written. Let’s take a case to verify the effect.

Execute sql scripts

The following are 2 databases: javacode2018_master (main library), javacode2018_slave (slave library)

A t_user table was created in both libraries, and a piece of data was inserted respectively. Later, this data was used to verify whether the master library or slave library was passed.

DROP DATABASE IF EXISTS javacode2018_master;
CREATE DATABASE IF NOT EXISTS javacode2018_master;
USE javacode2018_master;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT 'Name'
);
INSERT INTO t_user (name) VALUE ('master library');
DROP DATABASE IF EXISTS javacode2018_slave;
CREATE DATABASE IF NOT EXISTS javacode2018_slave;
USE javacode2018_slave;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT 'Name'
);
INSERT INTO t_user (name) VALUE ('slave library');

spring configuration class

@1: Enable read and write separation

masterDs() method: define the main library data source

slaveDs() method: define the source of the data from the library

dataSource(): Define read and write separation routing data source

There are 2 methods to define JdbcTemplate and transaction manager. Both methods define the injected bean name as dataSource through @Qualifier("dataSource"): that is, the read and write separation routing data source returned by dataSource() above is injected.

package com..demo1;

import com.;
import com.;
import com.;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;
import ;
import ;

@EnableReadWrite //@1
@Configuration
@ComponentScan
public class MainConfig {
    //Main Data Source    @Bean
    public DataSource masterDs() {
         dataSource = new ();
        ("");
        ("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8");
        ("root");
        ("root123");
        (5);
        return dataSource;
    }

    //From the library data source    @Bean
    public DataSource slaveDs() {
         dataSource = new ();
        ("");
        ("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8");
        ("root");
        ("root123");
        (5);
        return dataSource;
    }

    //Read and write separation routing data source    @Bean
    public ReadWriteDataSource dataSource() {
        ReadWriteDataSource dataSource = new ReadWriteDataSource();
        //Set the main library as the default library. When the corresponding data source is not found in the datasource map during routing, this default data source will be used        (());
        //Set multiple target libraries        Map&lt;Object, Object&gt; targetDataSources = new HashMap&lt;&gt;();
        (, ());
        (, ());
        (targetDataSources);
        return dataSource;
    }

    //JdbcTemplate, dataSource injects read and write separated data sources as defined above    @Bean
    public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

    //Define the transaction manager, dataSource injects read and write separated data sources as defined above.    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

UserService

This class is equivalent to the service we usually write. For the sake of the method, I directly use JdbcTemplate to operate the database. The real project operation db will be placed in dao.

getUserNameById method: query name through id.

insert method: Insert data. All internal operations will go to the main library. In order to verify whether the query will go to the main library, after inserting the data, we will call the (id, ) method to perform the query operation. The second parameter deliberately uses SLAVE. If the query has a result, it means that the main library is gone, otherwise the slave library is going to the slave library. Why do we need to call getUserNameById through ?

Finally, it is a proxy object. Only by accessing its internal methods through the proxy object will it be intercepted by a separate read and write interceptor.

package com..demo1;
import com.;
import com.;
import ;
import ;
import ;
import ;
import ;
import ;
@Component
public class UserService implements IService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private UserService userService;
    @Transactional(propagation = , readOnly = true)
    public String getUserNameById(long id, DsType dsType) {
        String sql = "select name from t_user where id=?";
        List&lt;String&gt; list = (sql, , id);
        return (list != null &amp;&amp; () &gt; 0) ? (0) : null;
    }
    //This insert method will go to the main library, and all internal operations will go to the main library    @Transactional
    public void insert(long id, String name) {
        (("Insert data{id:%s, name:%s}", id, name));
        ("insert into t_user (id,name) values (?,?)", id, name);
        String userName = (id, );
        ("Query results:" + userName);
    }
}

Test cases

package com..demo1;
import com.;
import ;
import ;
import ;
public class Demo1Test {
    UserService userService;
    @Before
    public void before() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        ();
        ();
         = ();
    }
    @Test
    public void test1() {
        ((1, ));
        ((1, ));
    }
    @Test
    public void test2() {
        long id = ();
        (id);
        (id, "Zhang San");
    }
}

The test1 method executes 2 queries, querying the master and slave libraries respectively, and outputs:

Master Library
slave library

Is it very good? The developer controls whether it is the main library or the slave library.

The execution result of test2 is as follows. It can be seen that the data just inserted was found, indicating that all operations in insert go to the main library.

1604905117467
Insert data {id:1604905117467, name:Zhang San}
Query result: Zhang San

This is the end of this article about Spring's detailed explanation of database reading and writing separation. For more related Spring reading and writing separation content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!