栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

springboot2.6+Mybatis动态多数据源AOP切换(AbstractRoutingDataSource)

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

springboot2.6+Mybatis动态多数据源AOP切换(AbstractRoutingDataSource)

多数据源系列

1、springboot2.6+Mybatis静态多数据源(集成JTA(Atomikos案例)实现分布式事务控制)
2、springboot2.6+Mybatis动态多数据源AOP切换(AbstractRoutingDataSource)
3、springboot2.6+Mybatis注解多数据源使用dynamic-datasource-spring-boot-starter为依赖

说明

搭建多数据源有多种方式,上一篇博客介绍了一种最基本的方式搭建多数据源,就是把每个数据源配置了一个DataSource的Bean,这种方式显得比较繁琐,mapper也要放在不同的地方,这里介绍一种动态切换数据源的方式

为什么用动态数据源

其实换一个角度想想,我们使用多个SqlSessionFactory来各自连接不同的数据源是很有局限性的。当我们数据源数量比较多的时候类似上文的模板式的代码将充斥整个项目,配置起来比较的繁琐。而且,试想一下,我们并不是每时每刻都对各个数据源都需要进行操作,每个数据源又会保有一个基本的闲置连接数。这样对本就宝贵的系统内存和CPU等资源产生了浪费,所以,第二种方案就应运而生了–动态数据源。我举一个生活中比较形象的例子:工人使用的钻头,其实钻机是只需要一个的,我们只需要根据不同的墙壁材质和孔的形状需要去替换掉钻机上不同的钻头就可以适应各个场景了呀。而上文我们所做的事情是买了两套甚至多套的钻机(真的有点奢侈了!)

缺点是:使用AbstractRoutingDataSource无法在JTA分布式事务中切换数据源,即放弃掉jta多事务,如果强制使用,要继承template重写源码,效果跟第一篇文章差不多,所以放弃jta。

动态数据源方案 文件结构

maven引入:
     
        org.springframework.boot
        spring-boot-starter-parent
        2.6.2
         
        
         
            org.springframework.boot
            spring-boot-starter-aop
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.1.2
        
application.yml 配置文件
   master1:
  url: jdbc:mysql://127.0.0.1:3306/master1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
  username: root
  password: 123456
  driverClassName: com.mysql.cj.jdbc.Driver
master2:
  url: jdbc:mysql://127.0.0.1:3306/master2?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
  username: root
  password: 123456
  driverClassName: com.mysql.cj.jdbc.Driver
oracle:
  url: jdbc:oracle:thin:@10.132.212.63:1688:TESTDB
  username: flx
  password: flx202108
  driverClassName: oracle.jdbc.OracleDriver

logging:
  level:
    com.xkcoding: debug
    com.xkcoding.orm.mybatis.mapper: trace
    
server:
  port: 8080
#  servlet:
#    context-path: /demo

其实换一个角度想想,我们使用多个SqlSessionFactory来各自连接不同的数据源是很有局限性的。当我们数据源数量比较多的时候类似上文的模板式的代码将充斥整个项目,配置起来比较的繁琐。而且,试想一下,我们并不是每时每刻都对各个数据源都需要进行操作,每个数据源又会保有一个基本的闲置连接数。这样对本就宝贵的系统内存和CPU等资源产生了浪费,所以,第二种方案就应运而生了–动态数据源。我举一个生活中比较形象的例子:工人使用的钻头,其实钻机是只需要一个的,我们只需要根据不同的墙壁材质和孔的形状需要去替换掉钻机上不同的钻头就可以适应各个场景了呀。而上文我们所做的事情是买了两套甚至多套的钻机(真的有点奢侈了!)。

1.自定义一个动态数据源上下文类,该类依靠一个ThreadLocal的类变量类标识当前线程是须要访问哪个数据源

为什么要用ThreadLocal?点击下面。
ThreadLocal<String>和static String在多线程的区别

package com.orm.mybatis.dynamic.config;


public class DatabaseContextHolder {

    private static final ThreadLocal contextHolder = new ThreadLocal();

    
    public static void setDBKey(String dataSourceKey) {
        contextHolder.set(dataSourceKey);
    }

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

    
    public static void clearDBKey() {
        contextHolder.remove();
    }
}

2. 建立一个动态数据源

继承自spring的org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource。实现了determineCurrentLookupKey方法,该方法惟一须要作的事情就是从DynamicDataSourceContextHolder获取当前须要访问的数据库名称。

package com.orm.mybatis.dynamic.config;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static DynamicDataSource instance;
    private static byte[] lock=new byte[0];
    private static Map dataSourceMap=new HashMap();
//    private AtomicInteger count = new AtomicInteger(0);  //读写分离


    @Override
    public void setTargetDataSources(Map targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        dataSourceMap.putAll(targetDataSources);
        // 必须添加该句,否则新添加数据源无法识别到
        super.afterPropertiesSet();
    }

    public Map getDataSourceMap() {
        return dataSourceMap;
    }

    
    @Override
    protected Object determineCurrentLookupKey() {
        String dbKey = DatabaseContextHolder.getDBKey();
        System.out.println(Thread.currentThread().getName()+" dbKey:    "+dbKey);
        return dbKey;
    }

    
//    @Override
//    protected Object determineCurrentLookupKey() {
//        String dbKey = DatabaseContextHolder.getDBKey();
//        if (dbKey.equals(DbUtil.DB_MASTER1)) // 如果是写库,直接返回
//            return dbKey;
//        // 读 简单负载均衡
//        int number = Math.abs(count.getAndAdd(1));
//        int lookupKey = number % 2;
//        if (lookupKey==1){
//        return DbUtil.DB_MASTER2;
//        }else{
//        return DbUtil.DB_MASTER3;
//        }
//    }

    @Override
    protected DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    private DynamicDataSource() {}

    public static synchronized DynamicDataSource getInstance(){
        if(instance==null){
            synchronized (lock){
                if(instance==null){
                    instance=new DynamicDataSource();
                }
            }
        }
        return instance;
    }

}

3.配置切面

在实际的项目开发中,不可能老是在访问数据库以前,调用DynamicDataSourceContextHolder.setDataSource,这样很差维护、繁琐、代码可阅读性也很差。因此,能够自定义一个注解,用于标识方法是要走从库仍是主库,而后用一个切面,切面对有相应注解的方法作加强,根据注解的属性,设置须要访问的数据源。
如果了解什么是AOP,点击下方连接

SpringBootAOP切面编程方法拦截和自定义注解拦截实现灵活的AOP切面配置

注解代码:

@documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default DbUtil.DB_MASTER1;
}

DbUtil.class

public class DbUtil {

    
    public static final String DB_MASTER1 = "ds_master1";
    
    public static final String DB_MASTER2 = "ds_master2";
    
    public static final String DB_MASTER3 = "ds_master3";
}

切面代码:

package com.orm.mybatis.dynamic.config;
import com.orm.mybatis.dynamic.annotation.DataSource;
import com.orm.mybatis.dynamic.util.DbUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;

@Order(-10)   //加上这个后,会先切面切换数据源再切换事务,可以在方法里叠加    @Transactional @DataSource(value = DbUtil.DB_MASTER1) 这样就会先切换数据源再添加事务,不会导致无法切换数据源
@Aspect
@Component
public class AOPAspectAnnotation {

//    private AtomicInteger count = new AtomicInteger(0);  //读写分离

    @Pointcut(value = "@annotation(com.orm.mybatis.dynamic.annotation.DataSource)")  //注意这里是全文件匹配函数
    public void getPoint() {
    }

    @Before("getPoint()")
    public void setPointAcc1(){
        System.out.println("BeforeGetPoint");
    }

    @Around("getPoint()")
    public Object getDoAround(ProceedingJoinPoint pjp){
        ThreadLocal startTime = new ThreadLocal<>();
        startTime.set(System.currentTimeMillis());
        System.out.println("我是环绕通知执行");
        Object obj;
        try{
            DataSource dataSource = null;
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            Method method = methodSignature.getMethod();
            dataSource =  method.getAnnotation(DataSource.class);
            if (dataSource==null){
                System.out.println("空值");
                System.out.println(DbUtil.DB_MASTER1);
                DatabaseContextHolder.setDBKey(dataSource.value());
                obj = pjp.proceed();
                System.out.println("执行返回值 : " + obj);
            }else {
                System.out.println(dataSource.value());
//                if (dataSource.value().equals(DbUtil.DB_MASTER1)){ //读写分离 start
                DatabaseContextHolder.setDBKey(dataSource.value());
//                }else {                                            //读写分离 middle
//             // 读 简单负载均衡
//             int number = Math.abs(count.getAndAdd(1));
//             int lookupKey = number % 2;
//             if (lookupKey==1){
//                 DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER2);
//             }else{
//                 DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER2);
//             }
//                }                                                 //读写分离 end
                obj = pjp.proceed();
                System.out.println("执行返回值 : " + obj);
            }
            System.out.println(pjp.getSignature().getName()+"方法执行耗时: " + (System.currentTimeMillis() - startTime.get()));
        } catch (Throwable throwable) {
            System.out.println(throwable+"报错");
            obj=throwable.toString();
        }
        return obj;
    }

    @After("getPoint()")
    public void setPointAcc2(){
        System.out.println("AfterGetPoint");
        DatabaseContextHolder.clearDBKey();
    }

    
    @AfterReturning(returning = "result", pointcut = "getPoint()")
    public void doAfterReturning(Object result){
        System.out.println("大家好,我是@AfterReturning,他们都秀完了,该我上场了"+result);
    }
}

其中@Order是很重要的,必须确保DynamicDataSourceAspect的执行优先于TranctionInterceptor。否则数据源的指定就没法生效(数据源的指定在数据库链接的获取以后!!)

4.配置动态数据源
@Configuration
// 扫描 Mapper 接口并容器管理
@MapperScan(basePackages = DatasourceConfig.PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate")
public class DatasourceConfig {
    // mapper扫描
    static final String PACKAGE = "com.orm.mybatis.dynamic.mapper.*";
    static final String MAPPER_LOCATION = "classpath:mapper
    public HikariDataSource initDataSource(String driverClass, String url, String user, String password){
        //jdbc配置
        HikariDataSource hikariDataSource = new HikariDataSource();
        hikariDataSource.setDriverClassName(driverClass);
        hikariDataSource.setJdbcUrl(url);
        hikariDataSource.setUsername(user);
        hikariDataSource.setPassword(password);
        setPool(hikariDataSource);
        return hikariDataSource;
    }


    
    private void setPool(HikariDataSource hikariDataSource){
        //连接池配置
        hikariDataSource.setMinimumIdle(5);
        hikariDataSource.setMaximumPoolSize(20);
        hikariDataSource.setAutoCommit(true);
        hikariDataSource.setPoolName("SpringBootDemoHikariCP");
        hikariDataSource.setMaxLifetime(1800000);
        hikariDataSource.setIdleTimeout(600000);
        hikariDataSource.setConnectionTimeout(30000);
    }


    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dynamicDataSource);
        sessionFactory.setTypeAliasesPackage("com.orm.mybatis.dynamic.entity");
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sessionFactory.setConfiguration(configuration);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources(DatasourceConfig.MAPPER_LOCATION));
        return sessionFactory.getObject();
    }

    
    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    
    @Bean
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

这一句很重要dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster1);
当调用没有添加@DataSource注解的方法时,默认走主库。到这一步,读写分离的基础都已经有了,接下来只须要按咱们日常调用单数据源那样配置mybatis就能够

UserServiceImpl 业务层
@Service
public class UserServiceImpl {

    @Resource
    private UserMapper1 userMapper1;

    @Resource
    private UserMapper2 userMapper2;

    @Resource
    private AsusPoInfoMapper3 asusPoInfoMapper3;

    public List findAllUser(){
        DatabaseContextHolder.setDBKey(DbUtil.DB_MASTER1);
        List list = userMapper1.selectAllUser();
        DatabaseContextHolder.clearDBKey();
        return list;
    }

    public void testTransitional() {
    ((UserServiceImpl)AopContext.currentProxy()).saveUserMapper2();//调用本类方法如想触发事务需要增强代理。
    ((UserServiceImpl)AopContext.currentProxy()).saveUserMapper1();
    ((UserServiceImpl)AopContext.currentProxy()).saveUserMapper3();
    }

    @Transactional
    @DataSource(value = DbUtil.DB_MASTER1)
    public void saveUserMapper1(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date =  simpleDateFormat.format(new Date());
        String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
        User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
                .lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
        userMapper2.saveUser(user);
    }

    @Transactional
    @DataSource(value = DbUtil.DB_MASTER2)
    public void saveUserMapper2(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date =  simpleDateFormat.format(new Date());
        String UUID = java.util.UUID.randomUUID().toString().substring(0,5);
        User user = User.builder().email("andrew@qq.com"+UUID).name("andrew"+UUID).password("123456"+UUID).phoneNumber("123"+UUID)
                .lastUpdateTime(date).createTime(date).status(0).salt("password"+UUID).build();
        userMapper2.saveUser(user);
    }


    @DataSource(value = DbUtil.DB_MASTER3)
    @Transactional
    public void saveUserMapper3(){
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date =  simpleDateFormat.format(new Date());
        Random random = new Random(500);
        int a = random.nextInt();
        AsusPoInfo asusPoInfo = AsusPoInfo.builder().id(UUID.randomUUID().toString().substring(0,20))
                .woNo("andrew").po("123456").poLine("poline").cPo("cpo123456").shipType("Direct").build();
        asusPoInfoMapper3.insertAsusPoInfo(asusPoInfo);
//        throw new RuntimeException();
    }

}

测试多数据源回滚
package com.orm.mybatis.dynamic;
public class UserTest extends AndrewApplicationTests {

    @Resource
    private UserServiceImpl userService;

    @Test
    public void test1(){
        System.out.println(userService.findAllUser());
    }

    @Test
    public void testTransitionalAspect() throws InterruptedException {
       userService.testTransitional();
    }

切换数据源成功,而且事务能回滚,但如果是多数据源事务,只能回滚报错的数据源的事务。

方案的权衡
    静态多数据源方案优势在于配置简单并且对业务代码的入侵性极小,缺点也显而易见:我们需要在系统中占用一些资源,而这些资源并不是一直需要,一定程度上会造成资源的浪费。如果你需要在一段业务代码中同时使用多个数据源的数据又要去考虑操作的原子性(事务)可以用spring的jta实现事务,那么这种方案无疑会适合你。(aop和dynamic)动态数据源(AbstractRoutingDataSource)方案配置上看起来配置会稍微复杂一些,但是很好的符合了“即拿即用,即用即还”的设计原则,我们把多个数据源看成了一个池子,然后进行消费。它的缺点正如上文所暴露的那样:我们往往需要在事务的需求下做出妥协。而且由于需要切换环境上下文,在高并发量的系统上进行资源竞争时容易发生死锁等活跃性问题。我们常用它来进行数据库的“读写分离”,不需要在一段业务中同时操作多个数据源。这种动态形式并不能用spring的jta实现,而且其他实现方式(seata等)虽然可以实现,但配置复杂且实用度不高。如果需要使用事务,一定记得使用分布式事务进行Spring自带事务管理的替换,否则将无法进行一致性控制。写到这里本文也就结束,好久没有撰写文章很多东西考虑不是很详尽,谢谢批评指正!
项目地址

springboot2.6+mybatis
https://gitee.com/liuweiqiang12/springboot-mybatis-dynamic-datasource

springboot2.6+mybatis-plus
https://gitee.com/liuweiqiang12/springboot-mybatis-plus-dynamic-datasource

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/781487.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号