1. Introducción
Lo que tenemos que hacer para la separación de lectura y escritura es elegir qué base de datos ejecutar para un SQL. En cuanto a quién elegirá la base de datos, no hay nada más que dos. O el middleware lo hace por nosotros o el programa lo hace por sí mismo. Por tanto, en términos generales, hay dos formas de realizar la separación entre lectura y escritura. El primero es confiar en el middleware (por ejemplo: MyCat), lo que significa que la aplicación está conectada al middleware, y el middleware nos ayuda a separar SQL; el segundo es separar la aplicación en sí. Aquí elegimos el programa para hacerlo nosotros mismos, principalmente utilizando la fuente de datos de enrutamiento proporcionada por Spring y AOP
Sin embargo, la mayor debilidad (deficiencia) de la separación de lecturas y escrituras a nivel de la aplicación es que los nodos de la base de datos no se pueden agregar dinámicamente, porque la configuración de la fuente de datos está escrita en la configuración. Agregar una nueva base de datos significa agregar una nueva fuente de datos, que debe cambiarse. Configure y reinicie la aplicación. Por supuesto, la ventaja es que es relativamente sencillo.
2. AbstractRoutingDataSource
Enrutar a una fuente de datos específica según una clave de búsqueda específica. Mantiene un conjunto de fuentes de datos de destino internamente, realiza un mapeo entre la clave de enrutamiento y la fuente de datos de destino, y proporciona un método para encontrar la fuente de datos en función de la clave.
3. Practica
Para la configuración, consulte " Configuración de replicación maestro-esclavo de MySQL "
3.1 dependencia de maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cjs.example</groupId>
<artifactId>cjs-datasource-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cjs-datasource-demo</name>
<description></description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
</dependencies>
<configuration>
<configurationFile>${basedir}/src/main/resources/myBatisGeneratorConfig.xml</configurationFile>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>-->
</plugins>
</build>
</project>
3.2. Configuración de la fuente de datos
application.yml
spring:
datasource:
master:
jdbc-url: jdbc:mysql://192.168.102.31:3306/test
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave1:
jdbc-url: jdbc:mysql://192.168.102.56:3306/test
username: pig # 只读账户
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave2:
jdbc-url: jdbc:mysql://192.168.102.36:3306/test
username: pig # 只读账户
password: 123456
driver-class-name: com.mysql.jdbc.Driver
Configuración de múltiples fuentes de datos
Aquí, hemos configurado 4 fuentes de datos, 1 maestra, 2 esclavas y 1 fuente de datos de enrutamiento. Las primeras 3 fuentes de datos se utilizan para generar la cuarta fuente de datos, y solo usaremos la última fuente de datos de enrutamiento en el futuro.
Configuración de MyBatis
package com.cjs.example.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
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;
@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
Dado que ahora hay 4 fuentes de datos en el contenedor Spring, necesitamos especificar manualmente una fuente de datos clara para el administrador de transacciones y MyBatis.
3.3. Establecer clave de enrutamiento / buscar fuente de datos
La fuente de datos de destino son las 3 primeras, que conocemos, pero ¿cómo encontrar la fuente de datos cuando se usa?
Primero, definimos una enumeración para representar estas tres fuentes de datos
package com.cjs.example.enums;
public enum DBTypeEnum {
MASTER, SLAVE1, SLAVE2;
}
A continuación, configure la fuente de datos para cada contexto de hilo a través de ThreadLocal
package com.cjs.example.bean;
import com.cjs.example.enums.DBTypeEnum;
import java.util.concurrent.atomic.AtomicInteger;
public class DBContextHolder {
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1);
public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
System.out.println("切换到master");
}
public static void slave() {
// 轮询
int index = counter.getAndIncrement() % 2;
if (counter.get() > 9999) {
counter.set(-1);
}
if (index == 0) {
set(DBTypeEnum.SLAVE1);
System.out.println("切换到slave1");
}else {
set(DBTypeEnum.SLAVE2);
System.out.println("切换到slave2");
}
}
}
Obtener clave de ruta
package com.cjs.example.bean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
Establecer clave de ruta
De forma predeterminada, todas las consultas van a la base de datos esclava, y la inserción / modificación / eliminación va a la base de datos maestra. Distinguimos tipos de operaciones por nombre de método (CRUD)
package com.cjs.example.aop;
import com.cjs.example.bean.DBContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAop {
@Pointcut("!@annotation(com.cjs.example.annotation.Master) " +
"&& (execution(* com.cjs.example.service..*.select*(..)) " +
"|| execution(* com.cjs.example.service..*.get*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(com.cjs.example.annotation.Master) " +
"|| execution(* com.cjs.example.service..*.insert*(..)) " +
"|| execution(* com.cjs.example.service..*.add*(..)) " +
"|| execution(* com.cjs.example.service..*.update*(..)) " +
"|| execution(* com.cjs.example.service..*.edit*(..)) " +
"|| execution(* com.cjs.example.service..*.delete*(..)) " +
"|| execution(* com.cjs.example.service..*.remove*(..))")
public void writePointcut() {
}
@Before("readPointcut()")
public void read() {
DBContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
/**
* 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库
*/
// @Before("execution(* com.cjs.example.service.impl.*.*(..))")
// public void before(JoinPoint jp) {
// String methodName = jp.getSignature().getName();
//
// if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
// DBContextHolder.slave();
// }else {
// DBContextHolder.master();
// }
// }
}
Hay casos generales y casos especiales. En casos especiales, necesitamos forzar la lectura de la biblioteca principal. Para esta situación, definimos una clave primaria y usamos la anotación para marcar la biblioteca principal.
package com.cjs.example.annotation;
public @interface Master {
}
Por ejemplo, supongamos que tenemos un miembro de la tabla
package com.cjs.example.service.impl;
import com.cjs.example.annotation.Master;
import com.cjs.example.entity.Member;
import com.cjs.example.entity.MemberExample;
import com.cjs.example.mapper.MemberMapper;
import com.cjs.example.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private MemberMapper memberMapper;
@Transactional
@Override
public int insert(Member member) {
return memberMapper.insert(member);
}
@Master
@Override
public int save(Member member) {
return memberMapper.insert(member);
}
@Override
public List<Member> selectAll() {
return memberMapper.selectByExample(new MemberExample());
}
@Master
@Override
public String getToken(String appId) {
// 有些读操作必须读主数据库
// 比如,获取微信access_token,因为高峰时期主从同步可能延迟
// 这种情况下就必须强制从主数据读
return null;
}
}
4. Prueba
package com.cjs.example;
import com.cjs.example.entity.Member;
import com.cjs.example.service.MemberService;
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;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CjsDatasourceDemoApplicationTests {
@Autowired
private MemberService memberService;
@Test
public void testWrite() {
Member member = new Member();
member.setName("zhangsan");
memberService.insert(member);
}
@Test
public void testRead() {
for (int i = 0; i < 4; i++) {
memberService.selectAll();
}
}
@Test
public void testSave() {
Member member = new Member();
member.setName("wangwu");
memberService.save(member);
}
@Test
public void testReadFromMaster() {
memberService.getToken("1234");
}
}
Ver consola
5. Estructura de ingeniería