SpringBoot2实践系列(五十):SpringAOP实现动态数据源切换

star2017 1年前 ⋅ 441 阅读

项目可能会有多个数据源,例如读写分离至少有两个数据库,或一个主库多个从库,或一个系统里面不同业务拆分有各自的数据库,在使用时需要确定使用正确的数据源。

Spring 多数据源实现方式大概有2种,一种是新建多个 MapperScan 扫描不同 Mapper 包,另一种是继承 AbstractRoutingDataSource 实现动态路由。

本篇是基于 Spring AOP 实现数据源动态路由,多个 MapperScan 扫描不同的 Mapper 包的方式可参考 Spring Boot 2实践系列(三十九):Spring Boot 2.x + Mybatis + Druid + Common Mapper 配置多数据源

相关知识可参考 Spring Boot 2实践系列(四十八):Spring AOP详解与应用Spring(二十四) Spring Transaction 事务管理机制

动态数据源

实现原理分析

Spring JDBC 为实现多数据源动态路由提供了 AbstractRoutingDataSource 抽象类,该类继承了 AbstractDataSource ,同时实现了 InitializingBeanAbstractDataSourceDataSource 接口的抽象实现,见下类图。

AbstractDataSource

AbstractRoutingDataSource 是 DataSource 的间接抽象实现,是基于指定的 Key 来选择(路由)目标的数据源,在运行时切换数据源,通常是将 Key 绑定到当前线程上下文中来传递;

AbstractRoutingDataSource 实现了 InitializingBean 接口,重写了 afterPropertiesSet() 方法,其具体子类在 Spring 启动时会被 BeanFactory 初始化为 Bean,并在设置了所有属性后,调用 afterPropertiesSet() 方法对其总体配置和最终初始化执行验证 。

  1. afterPropertiesSet() 方法

    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            // 将 targetDataSources 转为 resolvedDataSources
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            // 将 defaultTargetDataSource 转为 resolvedDefaultDataSource
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }
    

    targetDataSourcesdefaultTargetDataSource 是 AbstractRoutingDataSource 的两个属性,其具体子类在初始化为 Bean 时设置这两个属性值。

    此方法将 targetDataSources 转换为 resolvedDataSources,将 defaultTargetDataSource 转换为 resolvedDefaultDataSource,内部实际是从 resolvedDataSourcesresolvedDefaultDataSource 来获取数据源的 。

    如果数据源是可通过后台管理页面动态配置的,获取自定义的动态数据源 Bean,设置 targetDataSourcesdefaultTargetDataSource ,但仅仅将数据源维护在 targetDataSources 是不会生效的,还得手动调用 afterPropertiesSet() 方法再次转换。

  2. getConnection()determineTargetDataSource() 确定数据源创建链接

    @Override
    public Connection getConnection() throws SQLException {
        // 下面调用的 getConnection() 是具体的数据源实现 DataSource 接口重写的方法
        // 例如, DruidDataSource 或 HikariDataSource 重写了 getConnection() 方法
        // 最终是从数据库连接池中取出连接
        return determineTargetDataSource().getConnection();
    }
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }
    
    /**
     * 检查当前目标数据源,调用 determineCurrentLookupKey() 确定要使用的数据源 Key
     * 根据 KeyMap 中取出数据源或使用默认数据源返回
     */
    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;
    }
    
  3. determineCurrentLookupKey() 数据源 Key

    @Nullable
    protected abstract Object determineCurrentLookupKey();
    

    这是一个抽象方法,AbstractRoutingDataSource 的具体子类必须实现此方法,且不为空,返回的是数据源 Map( Map resolvedDataSources ) 的 Key,即确定要使用的数据源的 Key。

实现与使用

基于以上分析,应该对动态数据源实现原理有个初步了解。

下面是基于 Spring Boot + Druid + Mybatis + MySQL 配置动态数据源来实现读写分离。

动态数据源实现

数据库

准备至少两个数据库

sakila_mastersakila_slave 两个库,数据库及表结构和表数据使用 MySQL 的测试数据库 Github -> datacharmer / test_db -> sakila

创建项目

创建 Spring Boot 项目,排除掉 Spring Boot 默认启用数据源自动配置

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DatasourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DatasourceApplication.class, args);
    }
}

引入依赖

pom.xml 文件导入依赖包

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.springboot.datasource</groupId>
    <artifactId>datasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>datasource</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置数据源属性值

application.properties 添加数据源 和 Mybatis 属性配置

#debug=true

#=========== master datasource ===========
spring.datasource.master.name=master
spring.datasource.master.url=jdbc:mysql://localhost:3306/sakila_master?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8
spring.datasource.master.username=master
spring.datasource.master.password=123456
#=========== slave datasource ===========
spring.datasource.slave.name=slave
spring.datasource.slave.url=jdbc:mysql://localhost:3306/sakila_slave?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8
spring.datasource.slave.username=slave
spring.datasource.slave.password=123456
#=========== mybatis configure ===========
#mybatis.mapper-locations=classpath:mapper/*.xml
#mybatis.type-aliases-package=com.springboot.datasource.entity
#mybatis.configuration.map-underscore-to-camel-case=true
#=========== mapper log ===========
logging.level.com.springboot.datasource.mapper=debug
#mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

定义当前线程变量

定义当前线程绑定数据源 Key

/**
 * 存放线程数据源
 */
public class DataSourceHolder {

    private static final ThreadLocal<DataSourceEnum> threadLocal = new ThreadLocal<>();

    public static void setDataSource(DataSourceEnum key){
        threadLocal.set(key);
    }

    public static DataSourceEnum getDataSource() {
        return threadLocal.get();
    }

    public static void cleanDataSource(){
        threadLocal.remove();
    }
}

定义动态数据源

/**
 * 数据源配置类
 */
@Configuration
@MapperScan(basePackages = "com.springboot.datasource.mapper")
public class DataSourceConfig {

    /**
     * master datasource
     *
     * @return DataSource
     */
    @Bean(name = "dataSourceMaster")
    @ConfigurationProperties("spring.datasource.master")
    public DataSource dataSourceMaster() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * slave datasource
     *
     * @return DataSource
     */
    @Bean(name = "dataSourceSlave")
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource dataSourceSlave() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * DynamicDataSource
     *
     * @param dataSourceMaster
     * @param dataSourceSlave
     * @return DataSource
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster,
                                        @Qualifier("dataSourceSlave") DataSource dataSourceSlave) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceEnum.MASTER, dataSourceMaster);
        targetDataSources.put(DataSourceEnum.SLAVE, dataSourceSlave);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }

    /**
     * Mybatis Configuration
     *
     * @return Configuration
     */
    @Bean(name = "mybatisConfiguration")
    public org.apache.ibatis.session.Configuration mybatisConfiguration() {
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //开启驼峰映射
        configuration.setMapUnderscoreToCamelCase(true);
        return configuration;
    }

    /**
     * SqlSession Factory
     *
     * @param dataSource
     * @return SqlSessionFactory
     * @throws Exception
     */
    @Primary
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
//        sqlSessionFactoryBean.setTransactionFactory(new MultiDataSourceTransactionFactory());
        //指定mapper xml目录
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        sqlSessionFactoryBean.setTypeAliasesPackage("com.springboot.datasource.entity");
        sqlSessionFactoryBean.setConfiguration(mybatisConfiguration());
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * SqlSession Template
     *
     * @param sqlSessionFactory
     * @return SqlSessionTemplate
     */
    @Primary
    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * Transaction Manager
     *
     * @param dynamicDataSource
     * @return DataSourceTransactionManager
     */
    @Bean
    public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

定义查找数据源 Key

继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法,获取数据源 Key

/**
 * 动态数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }
}

定义数据源枚举

也可以使用 String 类型的静态变量。

public enum  DataSourceEnum {
    MASTER,SLAVE
}

定义数据源注解

用户可以使用注解来显式指定要使用的数据源。

/**
 * 数据源选择注解
 */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSourceSelector {
    DataSourceEnum name();
}

创建数据源切换AOP

下面示例使用的是环绕通知,最终必须清空当前线程中的数据源。

/**
 * 数据源动态切换 AOP
 */
@Order(Integer.MAX_VALUE - 2000)
@Aspect
@Component
public class DataSourceAspect {
    private static final Logger logger = LogManager.getLogger(DataSourceAspect.class);

    @Pointcut("execution(* com.springboot.datasource.mapper..*(..))")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object switchDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        String methodName = method.getName();

        DataSourceSelector dsSelector = method.getAnnotation(DataSourceSelector.class);
        Transactional transactional = method.getAnnotation(Transactional.class);

        if (null != dsSelector) {
            //显式指定数据源优先
            DataSourceEnum dataSourceKey = dsSelector.name();
            DataSourceHolder.setDataSource(dataSourceKey);
            logger.info("DataSourceSelector name:{}, DataSource:{}", dsSelector.name(), dataSourceKey);
        } else if (null != transactional && transactional.readOnly()) {
            //读事务路由到从库
            DataSourceHolder.setDataSource(DataSourceEnum.SLAVE);
            logger.info("Transactional readOnly:{}, DataSource:{}", transactional.readOnly(), DataSourceEnum.SLAVE);
        } else if (methodName.startsWith("get") || methodName.startsWith("query") || methodName.startsWith("find")
                || methodName.startsWith("select") || methodName.startsWith("list")) {
            //根据方法前缀判断路由到从库
            DataSourceHolder.setDataSource(DataSourceEnum.SLAVE);
            logger.info("Method Name:{}, DataSource:{}", methodName, DataSourceEnum.SLAVE);
        } else {
            //其它到主库
            DataSourceHolder.setDataSource(DataSourceEnum.MASTER);
            logger.info("Method Name:{}, Datasource:{}", methodName, DataSourceEnum.MASTER);
        }

        try {
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            //这里必须抛出异常才会触发事务回滚
            throw throwable;
        } finally {
            DataSourceHolder.cleanDataSource();
        }
    }
}

动态数据源事务

在实际开发中,开启事务通常是在业务(Service)层,Spring 事务管理(DataSourceTransactionManager)只支持单库事务,开启事务,会将数据源缓存到 DataSourceTransactionObject 对象中进行后续的 commitrollback 等操作。即开启了事务后是不能切换数据源的,切换数据源会无效,也就是说,切换数据源要在开启事务之前执行。

本示例中动态切换数据源的 AOP 和注解是作用在 Mapper 层的方法上,最简单的修改是将 AOP 的切点或注释作用在 Controller 中的方法上。不过此方式就使得 Controller 层的责职有些混乱。

完美解决思路:最完美的方式在业务层,开启事务之前就完成数据源切换。自定义事务切面,使用 Order 注解,属性值大于数据源切换切面的值,即 事务切面数据源切换切面 后面执行。

注解事务控制AOP

此方式是关闭 Spring Boot 的事务自动配置,创建自定义的事务切面(AOP),通过数据源获取事务状态控制事务。

  1. 关闭 Spring Boot 事务自动配置

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
    public class DatasourceApplication {
        public static void main(String[] args) {
            SpringApplication.run(DatasourceApplication.class, args);
        }
    }
    
  2. 修改数据源切换 AOP 的切点表达式指向 service 层

    @Pointcut("execution(* com.springboot.datasource.service..*(..))")
    public void pointcut() {
    }
    
  3. 创建事务控制 AOP

    自定义事务控制AOP 使用了前置通知开启事务,后置通知提交事务,异常通知回滚事务,使用了组合切点,精确定位到 service 层的 Transactional 注解,即关闭了 Spring Boot 事务自动配置后,Transactional 注解在这里仍然有效(此时,这个 Transactional 相当于是自定义的注解了)。

    注意:数据源切换AOP的 @Order 注解值(@Order(Integer.MAX_VALUE - 2000)

    TransactionalAop.class

    @Order(Integer.MAX_VALUE - 1000)//此是重点,Order值必须大于动态数据源切换AOP的Order值
    @Aspect
    @Component
    @Scope("prototype")//线程安全,每次调用都是一个新的实例
    public class TransactionalAop {
    
        private TransactionStatus transactionStatus;
    
        @Autowired
        private TransactionUtils transactionUtils;
    
        /**
         * 切点 Service 层
         */
        @Pointcut("execution(* com.springboot.datasource.service..*(..))")
        public void servicePointcut() {
        }
    
        /**
         * 切点 Transactional 注解
         */
        @Pointcut("@annotation(transactional)")
        public void txAnnotationPointcut(Transactional transactional) {
        }
    
        /**
         * 开启事务
         * @param joinPoint
         * @param transactional
         * @throws Throwable
         */
        @Before(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "joinPoint,transactional")
        public void before(JoinPoint joinPoint, Transactional transactional) throws Throwable {
            // 1 找到对应的方法,通过方法名和参数类型 因为会有重载
            this.transactionStatus = transactionUtils.begin(transactional);
        }
    
        /**
         * 正常返回,提交事务
         * @param transactional
         */
        @AfterReturning(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")
        public void afterReturning(Transactional transactional) {
            transactionUtils.commit(transactionStatus);
        }
    
        /**
         * 异常通知,回滚事务
         * @param transactional
         */
        @AfterThrowing(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")
        public void afterThrowing(Transactional transactional) {
            transactionUtils.rollback(transactionStatus);
        }
    }
    

    从上面代码可以看出,AOP 常用的几种细粒度 通知类型 可以与事务的操作完美契合,可以说 AOP 是为事务而生的,再而扩展适配到其它领域(个人理解,不接受反驳)。

    注意:网上有些文章中的事务通知使用的是 @Around 环绕通知,但没有返回值,此方式是不恰当的,有比较大的局限性,不能在 AOP 层忽略业务层的返回数据;有的有返回值,在返回值前提交事务,是否有其它问题待验证。

    TransactionUtils.class

    @Component
    public class TransactionUtils {
        private static final Logger logger = LogManager.getLogger(TransactionUtils.class);
    
        @Autowired
        private DataSourceTransactionManager dataSourceTransactionManager;
    
        /**
         * begin transactional
         *
         * @param transactional
         * @return TransactionStatus
         */
        public TransactionStatus begin(Transactional transactional) {
            DefaultTransactionAttribute txAttribute = new DefaultTransactionAttribute();
    
            txAttribute.setQualifier(transactional.value());
            txAttribute.setReadOnly(transactional.readOnly());
            txAttribute.setTimeout(transactional.timeout());
            txAttribute.setIsolationLevel(transactional.isolation().value());
            txAttribute.setPropagationBehavior(transactional.propagation().value());
            //注意: 注解Transactional.rollbackFor() 返回的是一个继承 Throwable 的 Class 对象数组
            //而下面方法只能设置单个继承 Throwable 的 Class 对象,且只对 RuntimeException 和 Error 异常有效
    //        txAttribute.rollbackOn()
    
            TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(txAttribute);
            logger.info("begin transactional");
            return transactionStatus;
        }
    
        /**
         * commit transactional
         *
         * @param transactionStatus
         */
        public void commit(TransactionStatus transactionStatus) {
            dataSourceTransactionManager.commit(transactionStatus);
            logger.info("commit transactional");
        }
    
        /**
         * rollback transactional
         *
         * @param transactionStatus
         */
        public void rollback(TransactionStatus transactionStatus) {
            dataSourceTransactionManager.rollback(transactionStatus);
            logger.info("rollback transactional");
        }
    }
    

    注意: TransactionUtils 类可以在业务实现类中注入,在方法的业务逻辑开始前使用此工具类手动开启事务,正常执行完后提交事务,抛出异常后回滚事务,这就是编程式事务,但这是不可取的。

全局事务控制AOP

此方式是采用 Spring AOP 方法名称匹配方法来开启事务,此方式相对就没有注解事务灵活。

重点还是在 @Order 注解的值要大于数据源切换AOP 的 @Order

/**
 * Service 层全局事务
 */
@Order(Integer.MAX_VALUE - 1000)
@Aspect
@Component
public class TransactionAspect {
    private static final Logger logger = LogManager.getLogger(TransactionAspect.class);

    private static final int TX_METHOD_TIMEOUT = 5;
    private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.springboot.datasource.service..*(..))";

    @Autowired
    private PlatformTransactionManager transactionManager;

    @Bean
    public TransactionInterceptor txAdvice() {
        NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
        //只读事务,不做更新操作
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        //以非事务方式执行
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);

        //当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务
        RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
        requiredTx.setRollbackRules(
                Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
        requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        requiredTx.setTimeout(TX_METHOD_TIMEOUT);
        Map<String, TransactionAttribute> txMap = new HashMap<>();
        //事务方法前缀
        txMap.put("add*", requiredTx);
        txMap.put("save*", requiredTx);
        txMap.put("insert*", requiredTx);
        txMap.put("update*", requiredTx);
        txMap.put("delete*", requiredTx);
        txMap.put("remove*", requiredTx);
        //非事务方法前缀
        txMap.put("get*", readOnlyTx);
        txMap.put("find*", readOnlyTx);
        txMap.put("select*", readOnlyTx);
        txMap.put("query*", readOnlyTx);
        source.setNameMap(txMap);
        TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source);
        return txAdvice;
    }

    @Bean
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }
}

动态数据源使用

定义实体类

public class Actor implements Serializable {
    private static final long serialVersionUID = -1623523874495502121L;

    private Long actorId;
    private String firstName;
    private String lastName;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8" )
    private Date lastUpdate;
    //---------set/get 方法--------------
}

定义 Controller

@RestController
@RequestMapping("/actor")
public class ActorController {
    private static final Logger logger = LogManager.getLogger(ActorController.class);

    @Autowired
    private ActorService actorService;

    @GetMapping("/getById")
    public Actor getById(Long id) {
        Actor actor = actorService.getById(id);
        return actor;
    }
    @GetMapping("/getActor")
    public List<Actor> getActor(Actor actor) {
        List<Actor> actorList = actorService.getActor(actor);
        return actorList;
    }

    @PostMapping("/save")
    public Actor save(Actor actor) {
        actor.setLastUpdate(new Date());
        actor = actorService.save(actor);
        return actor;
    }

    @PostMapping("/saveActorList")
    public void saveActorList(Boolean errorFlag) {
        actorService.saveActorList(errorFlag);
    }
}

定义 Service

Service 接口

public interface ActorService {
    Actor getById(Long id);

    Actor save(Actor actor);

    List<Actor> getActor(Actor actor);

    void saveActorList(Boolean errorFlag);
}

Service 接口实现

@Service
public class ActorServiceImpl implements ActorService {

    @Autowired
    private ActorMapper actorMapper;

    @Override
    public Actor getById(Long id) {
        Actor actor = actorMapper.getById(id);
        return actor;
    }

    @Override
    @Transactional
    public Actor save(Actor actor) {
        actorMapper.save(actor);
        return actor;
    }

    @Override
    @DataSourceSelector(name = DataSourceEnum.MASTER)
    public List<Actor> getActor(Actor actor) {
        return actorMapper.getActor(actor);
    }

    @Override
    @Transactional
    @DataSourceSelector(name = DataSourceEnum.SLAVE)
    public void saveActorList(Boolean errorFlag) {
        List<Actor> actorList = new ArrayList<>();
        actorList.add(new Actor("张","飞", new Date()));
        actorList.add(new Actor("关","羽", new Date()));
        actorList.add(new Actor("刘","备", new Date()));
        actorMapper.saveActorList(actorList);

        //抛异常触发事务回滚
        if(null != errorFlag && errorFlag){
            int i = 1/0;
        }

        actorMapper.save(new Actor("曹","操",new Date()));
    }
}

定义 Mapper

Mapper 接口

@Repository
public interface ActorMapper {

//    @Select("select * from actor where actor_id = #{id}")
    Actor getById(@Param("id") Long id);

    void save(Actor actor);

    List<Actor> getActor(Actor actor);

    void saveActorList(List<Actor> actorList);
}

Mapper XML 文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.springboot.datasource.mapper.ActorMapper">

    <select id="getById" parameterType="long" resultType="actor">
        select * from actor where actor_id = #{id}
    </select>

    <insert id="save" parameterType="actor" useGeneratedKeys="true" keyColumn="actor_id" keyProperty="actorId">
        insert into actor (first_name, last_name, last_update) values (#{firstName}, #{lastName}, #{lastUpdate})
    </insert>

    <select id="getActor" parameterType="actor" resultType="actor">
        select * from actor where 1= 1
        <if test="null != firstName and '' != firstName">
            and first_name = #{firstName}
        </if>
        <if test="null != lastName and '' != lastName">
            and last_name = #{lastName}
        </if>
    </select>

    <insert id="saveActorList" parameterType="java.util.List">
        insert into actor (first_name, last_name, last_update) values
        <foreach collection="list" index="index" item="item" separator=",">
            (#{item.firstName}, #{item.lastName}, #{item.lastUpdate})
        </foreach>
    </insert>

</mapper>

调用接口测试

  1. 调用保存数据的接口,在两个数据库查看保存的数据查询。

  2. 调用获取数据的接口,注意数据来自于哪个数据库。

  3. 查询日志输出,读 / 写操作是否有切换数据源。

    2019-12-14 16:36:11.327  INFO 15036 --- [nio-8080-exec-1] c.s.d.c.d.aspect.DataSourceAspect        : DataSourceSelector name:SLAVE, DataSource:SLAVE
    2019-12-14 16:36:23.927  INFO 15036 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1,slave} inited
    
  4. 调用保存接口,抛出异常触发事务回滚,查看数据是否有写入库,预期是事务回滚,数据库没有脏数据。

动态数据源扩展

需求:假如读写分离数据库是 多主多从 的架构,并要满足数据源可动态配置即时生效,且不修改源码和配置文件。或 Saas 系统,每个客户有自己独自的数据库,业务需要根据客户对应的数据库来进行动态切换。

思路:

  1. 将多个数据源信息存入到 MySQL 中。

  2. 基于动态数据源实现,配置默认数据源,在项目启动时从 MySQL 中查询数据源信息。

    可通过实现 CommandLineRunner 接口,重写 run(String... args) 方法来处理。

  3. 调用数据源配置,创建多个数据源并注册为 Bean,加入到一个集合中。

  4. 定义使用数据源策略,轮循或随机访问,或 根据用户 ID 与 数据源的 映射来选择对应数据源。

上面示例源码

GitHub -> spring-boot-datasource

其它参考

  1. SpringBoot多数据源
  2. Spring 动态切换数据源
  3. Spring Boot 配置动态数据源
  4. Spring 动态切换数据源及事务
  5. SpringBoot数据源加载及其多数据源
  6. Spring中实现多数据源事务管理
  7. SpringBoot+MyBatis+MySQL读写分离
  8. Spring的事务管理入门:编程式事务管理(TransactionTemplate)
  9. 手写 Spring 事务,IoC 和 MVC
  10. Spring JtaTransactionManager 实现多数据源使事务
  11. Spring TransactionManager 源码分析
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: