Spring AOP从入门到放弃之多数据源读写动态切换

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 项目中如果需要由多个数据源,比如3个,一个主两个从。主库主要是写操作,两个从库做读操作。 那么在spring boot中怎么使用AOP判断程序是读还是写,并且分配到不同的数据源中呢?本文重要是 的代码是使用 spring boot 、druid、mybatis、mybatis plus等技术做支持的。

项目中如果需要由多个数据源,比如3个,一个主两个从。主库主要是写操作,两个从库做读操作。
那么在spring boot中怎么使用AOP判断程序是读还是写,并且分配到不同的数据源中呢?

本文重要是 的代码是使用 spring boot 、druid、mybatis、mybatis plus等技术做支持的。

逻辑步骤

大概的逻辑为,
1、引入durid
2、配置三个数据源,1个写,2个读,两个从库实现简单的负载功能。
3、配置mybatis
4、配置mybatis plus
5、配置aop
6、定义却点 读(select)的方法操作,使用读库的数据源,其他的 update、delete、insert等使用写库的数据源
7、给写库配置spring 的事务,出现异常的时候回滚。

引入jar

这里不累赘写 mysql 、druid等jar包的引入。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

配置Druid

@Configuration
public class DruidConfig {
    /**
     * 注册DruidServlet
     * http://localhost:8080/druid/datasource.html查看监控信息
     * @return
     */
    @Bean
    public ServletRegistrationBean druidServletRegistrationBean() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new StatViewServlet());
        servletRegistrationBean.addUrlMappings("/druid/*");
        return servletRegistrationBean;
    }

    /**
     * 注册DruidFilter拦截
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean duridFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        Map<String, String> initParams = new HashMap<String, String>();
        //设置忽略请求
        initParams.put("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/monitor/druid/*");
        filterRegistrationBean.setInitParameters(initParams);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

配置yml


writedatasource:
  url: jdbc:mysql://xx.x.x.x:3306/slife?useUnicode=true&characterEncoding=utf8&useSSL=false
  driverClass: com.mysql.jdbc.Driver
  username: xx
  password: cdd
  initialSize: 1
  minIdle: 1
  maxActive: 20
  testOnBorrow: true
  timeBetweenEvictionRunsMillis: 60000
  minEvictableIdleTimeMillis: 300000
  validationQuery: SELECT 1 FROM DUAL
  testWhileIdle: true
  testOnReturn: false
  poolPreparedStatements: true
  maxPoolPreparedStatementPerConnectionSize: 20
  filters: stat,wall,logback
  #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  useGlobalDataSourceStat: true


mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.slife.entity


readdatasource01:
  url: jdbc:mysql://xx.x.x.x:3306/slife?useUnicode=true&characterEncoding=utf8&useSSL=false
  driverClass: com.mysql.jdbc.Driver
  username: xx
  password: cdd
  initialSize: 1
  minIdle: 1
  maxActive: 20
  testOnBorrow: true
  timeBetweenEvictionRunsMillis: 60000
  minEvictableIdleTimeMillis: 300000
  validationQuery: SELECT 1 FROM DUAL
  testWhileIdle: true
  testOnReturn: false
  poolPreparedStatements: true
  maxPoolPreparedStatementPerConnectionSize: 20
  filters: stat,wall,logback
  #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  useGlobalDataSourceStat: true


readdatasource02:
  url: jdbc:mysql://xx.x.x.x:3306/slife?useUnicode=true&characterEncoding=utf8&useSSL=false
  driverClass: com.mysql.jdbc.Driver
  username: xx
  password: cdd
  initialSize: 1
  minIdle: 1
  maxActive: 20
  testOnBorrow: true
  timeBetweenEvictionRunsMillis: 60000
  minEvictableIdleTimeMillis: 300000
  validationQuery: SELECT 1 FROM DUAL
  testWhileIdle: true
  testOnReturn: false
  poolPreparedStatements: true
  maxPoolPreparedStatementPerConnectionSize: 20
  filters: stat,wall,logback
  #connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
  useGlobalDataSourceStat: true

加载配置文件数据

/**
 * 只提供了常用的属性,如果有需要,自己添加
 *
 */
@Component
@ConfigurationProperties(prefix = "writedatasource")
public class WriteProperties extends DataProperties{

}


/**
 * 只提供了常用的属性,如果有需要,自己添加
 *
 */
@Component
@ConfigurationProperties(prefix = "readdatasource02")
public class ReadProperties2 extends DataProperties{

}

@ConfigurationProperties(prefix = "druid")
public class DruidProperties {
    private String url;
    private String username;
    private String password;
    private String driverClass;

    private int maxActive;
    private int minIdle;
    private int initialSize;
    private boolean testOnBorrow;
    private String filters;
    。
    。
    。
    。
get  set  省略

3、定义数据源

public enum DataSourceType {
    read("read", "从库"),
    write("write", "主库");

    private String type;

    private String name;

    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

简单的负载均衡配置

public class BlifeAbstractRoutingDataSource extends AbstractRoutingDataSource {
    private  int dataSourceNumber;
    private AtomicInteger count = new AtomicInteger(0);

    public BlifeAbstractRoutingDataSource(int dataSourceNumber) {
        this.dataSourceNumber = dataSourceNumber;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        if (DataSourceType.write.getType().equals(typeKey)){
            return DataSourceType.write.getType();
        }
        // 读 简单负载均衡
        int number = count.getAndAdd(1);
        int lookupKey = number % dataSourceNumber;
        return new Integer(lookupKey);
    }
}

把数据源加ThreadLocal中

public class DataSourceContextHolder {
    private static final ThreadLocal<String> LOCAL = new ThreadLocal<String>();

    private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    public static ThreadLocal<String> getLocal() {
        return LOCAL;
    }

    /**
     * 读可能是多个库
     */
    public static void read() {

        LOCAL.set(DataSourceType.read.getType());
    }

    /**
     * 写只有一个库
     */
    public static void write() {
        logger.debug("writewritewrite");
        LOCAL.set(DataSourceType.write.getType());
    }

    public static String getJdbcType() {
        return LOCAL.get();
    }
}

配置写库的事物

读库 一般没配置事物的需求,当然配置了只读会有更好的效果。

@Configuration
@EnableTransactionManagement
public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {

    private Logger logger= LoggerFactory.getLogger(getClass());

    /**
     * 自定义事务
     * MyBatis自动参与到spring事务管理中,无需额外配置,
     * 只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与
     * DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。
     * @return
     */
    @Resource(name = "writeDataSource1")
    private DataSource dataSource;

    @Bean(name = "transactionManager")
    public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManagers() {
        logger.info("-------------------- transactionManager init ---------------------");
        return new org.springframework.jdbc.datasource.DataSourceTransactionManager(dataSource);
    }
}

约定方法,读写动态切换

@Aspect
@Component
@Order(-100)// 保证该AOP在@Transactional之前执行
public class DataSourceAop {
    private  Logger logger = LoggerFactory.getLogger(getClass());



    @Before("execution(* com.slife.dao..*.select*(..)) || execution(* com.slife.dao..*.get*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.read();
        logger.info("dataSource切换到:Read");
    }

    @Before("execution(* com.slife.dao..*.*insert*(..)) || execution(* com.slife.dao..*.*update*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.write();
        logger.info("dataSource切换到:write");
    }
}

运行项目,执行结果

这里写图片描述

这里写图片描述

点击获取阿里云优惠券

我的官网
我的博客

我的官网http://guan2ye.com
我的CSDN地址http://blog.csdn.net/chenjianandiyi
我的简书地址http://www.jianshu.com/u/9b5d1921ce34
我的githubhttps://github.com/javanan
我的码云地址https://gitee.com/jamen/
阿里云优惠券https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=vf2b5zld&utm_source=vf2b5zld

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
XML Java 数据格式
Spring框架入门:IoC与DI
【5月更文挑战第15天】本文介绍了Spring框架的核心特性——IoC(控制反转)和DI(依赖注入)。IoC通过将对象的创建和依赖关系管理交给容器,实现解耦。DI作为IoC的实现方式,允许外部注入依赖对象。文章讨论了过度依赖容器、配置复杂度等常见问题,并提出通过合理划分配置、使用注解简化管理等解决策略。同时,提醒开发者注意过度依赖注入和循环依赖,建议适度使用构造器注入和避免循环引用。通过代码示例展示了注解实现DI和配置类的使用。掌握IoC和DI能提升应用的灵活性和可维护性,实践中的反思和优化至关重要。
41 4
|
1月前
|
Java 关系型数据库 数据库
Spring Boot多数据源及事务管理:概念与实战
【4月更文挑战第29天】在复杂的企业级应用中,经常需要访问和管理多个数据源。Spring Boot通过灵活的配置和强大的框架支持,可以轻松实现多数据源的整合及事务管理。本篇博客将探讨如何在Spring Boot中配置多数据源,并详细介绍事务管理的策略和实践。
80 3
|
1月前
|
前端开发 Java 数据库连接
Spring系列文章1:Spring入门程序
Spring系列文章1:Spring入门程序
|
1天前
|
druid Java 关系型数据库
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
16 5
|
14天前
|
XML 存储 Java
Spring 6(一)【Spring 入门】
Spring 6(一)【Spring 入门】
|
15天前
|
Java 数据库连接 数据库
Spring日志完结篇,MyBatis操作数据库(入门)
Spring日志完结篇,MyBatis操作数据库(入门)
|
17天前
|
存储 自动驾驶 Java
Spring IoC&DI(1)—入门
Spring IoC&DI(1)—入门
14 1
|
17天前
|
JSON 前端开发 Java
Spring Web MVC入门(3)——响应
Spring Web MVC入门(3)——响应
22 1
|
1天前
|
Java 程序员 开发工具
Spring - SpringBoot入门之环境搭建
Spring - SpringBoot入门之环境搭建
8 0
|
17天前
|
存储 前端开发 Java
Spring Web MVC入门(2)——请求(下)
Spring Web MVC入门(2)——请求
19 0