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

项目中如果需要由多个数据源,比如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
我的简书地址//www.greatytc.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容