手动实现一下Mysql读写分离

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

背景

当数据量过大时候,对单表进行更新、查询的操作有时候会导致锁表,让读写速度跟不上,一个页面就要2-3秒,这就需要使用读写分离。 很多应用的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,读操作从数据库来进行。

代码

配置文件

增加双数据源的数据库配置

spring:
  datasource:
      datasource1:
        url: jdbc:mysql://localhost:3306/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
        username: admin
        password: 1024571
        driver-class-name: com.mysql.jdbc.Driver
        filters: stat,wall
        initial-size: 1
        min-idle: 1
        max-active: 20
        max-wait: 60000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 30000
        validation-query: SELECT 'x'
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: false
        max-pool-prepared-statement-per-connection-size: 20
	  datasource2:
        url: jdbc:mysql://localhost:3307/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowMultiQueries=true&useAffectedRows=true
        username: admin
        password: 1024571
        driver-class-name: com.mysql.jdbc.Driver
        filters: stat,wall
        initial-size: 1
        min-idle: 1
        max-active: 20
        max-wait: 60000
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 30000
        validation-query: SELECT 'x'
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        pool-prepared-statements: false
        max-pool-prepared-statement-per-connection-size: 20	
复制代码
自定义注解

自定义数据源key的注解,value为数据源key

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSource {
    String value() default "data1";  
}
复制代码
数据源key设置
@Slf4j
public class DataSourceContextHolder {

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

    // 设置数据源名称
    public static void setDataSource(String dataSource){
        contextHolder.set(dataSource);
    }

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

    // 清除数据源
    public static void clearDataSource(){
        contextHolder.remove();
    }

}
复制代码
动态数据源类
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}
复制代码
数据源配置类

定义双数据源的key和bean对应关系

@Configuration
public class DataSourceConfig {
    /**
     *  数据源1
     */
    @Bean(name = "data1")
    @ConfigurationProperties(prefix = "spring.datasource.data1")
    public DataSource Data1(){
        return DataSourceBuilder.create().build();
    }

    /**
     * 数据源2
     */
    @Bean(name = "data2")
    @ConfigurationProperties(prefix = "spring.datasource.data2")
    public DataSource Data2(){
        return DataSourceBuilder.create().build();
    }

    /**
     * 数据源切换: 通过AOP在不同数据源之间动态切换
     */
    @Primary
    @Bean
    public DataSource dynamicDataSource(){

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        //设置默认数据源
        dynamicDataSource.setDefaultTargetDataSource(Data1());
        //配置多数据源
        Map<Object,Object> dsMap = new HashMap<>();
        dsMap.put("data1",Data1());
        dsMap.put("data2",Data2());

        dynamicDataSource.setTargetDataSources(dsMap);
        return dynamicDataSource;
    }

    /**
     * 配置@Transactional注解事务
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

}
复制代码
自定义切面

切面实现方法通过注解中的value进行切换不同数据源。

@Aspect
@Component
public class DataSourceAspect {
    @Pointcut("@annotation(com.wqy.data.annotation.DataSource)")
    public void pointcutConfig(){

    }
    @Before("pointcutConfig()")
    public void before(JoinPoint joinPoint){
        //获得当前访问的class
        Class<?> className = joinPoint.getTarget().getClass();
        //获得访问的方法名
        String methodName = joinPoint.getSignature().getName();
        //得到方法的参数的类型
        Class[] argClass = ((MethodSignature)joinPoint.getSignature()).getParameterTypes();

        String dataSource = null;
        try {
            // 得到访问的方法对象
            Method method = className.getMethod(methodName, argClass);
            // 判断是否存在@DataSource注解
            if (method.isAnnotationPresent(DataSource.class)) {
                DataSource annotation = method.getAnnotation(DataSource.class);
                // 取出注解中的数据源名
                dataSource = annotation.value();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 设置数据源key
        DataSourceContextHolder.setDataSource(dataSource);
    }

    @After("pointcutConfig()")
    public void after(JoinPoint joinPoint){
        DataSourceContextHolder.clearDataSource();
    }
}
复制代码
使用注解

在方法上面用自定义的数据源注解声明数据源,就可以实现不同方法,不同数据源调用。

    @DataSource("dataSource1")
    public void queryUser() {
        userMapper.select();
    }
复制代码

原理解析

通过AOP对方法进行切面,将注解中的value获取到,并设置为数据源Key,通过数据源配置类,拿到数据源对应的数据库bean,进而实现数据源切换。

AbstractRoutingDataSource

AbstractRoutingDataSource继承AbstractDataSource,如果声明一个类DynamicDataSource继承AbstractRoutingDataSource后,DynamicDataSource本身就相当于一种数据源。所以AbstractRoutingDataSource必然有getConnection()方法获取数据库连接。

大致流程为,通过determineCurrentLookupKey方法获取一个key,通过key从resolvedDataSources中获取数据源DataSource对象。determineCurrentLookupKey()是个抽象方法,需要继承AbstractRoutingDataSource的类实现;而resolvedDataSources是一个Map<Object, DataSource>,里面应该保存当前所有可切换的数据源。


protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;}
复制代码

おすすめ

転載: juejin.im/post/7034124148699987976