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) && > 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<Object, Object> targetDataSources = new HashMap<>(); (, ()); (, ()); (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<String> list = (sql, , id); return (list != null && () > 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!