项目可能会有多个数据源,例如读写分离至少有两个数据库,或一个主库多个从库,或一个系统里面不同业务拆分有各自的数据库,在使用时需要确定使用正确的数据源。
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  ,同时实现了 InitializingBean ,AbstractDataSource  是 DataSource  接口的抽象实现,见下类图。
AbstractRoutingDataSource 是 DataSource  的间接抽象实现,是基于指定的 Key 来选择(路由)目标的数据源,在运行时切换数据源,通常是将 Key 绑定到当前线程上下文中来传递;
AbstractRoutingDataSource 实现了 InitializingBean  接口,重写了 afterPropertiesSet()  方法,其具体子类在 Spring 启动时会被 BeanFactory  初始化为 Bean,并在设置了所有属性后,调用 afterPropertiesSet()  方法对其总体配置和最终初始化执行验证 。 
afterPropertiesSet()  方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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);                  this .resolvedDataSources.put(lookupKey, dataSource);     });     if  (this .defaultTargetDataSource != null ) {                  this .resolvedDefaultDataSource = resolveSpecifiedDataSource(this .defaultTargetDataSource);     } } 
targetDataSources  和 defaultTargetDataSource  是 AbstractRoutingDataSource 的两个属性,其具体子类在初始化为 Bean 时设置这两个属性值。
此方法将 targetDataSources  转换为 resolvedDataSources ,将 defaultTargetDataSource  转换为 resolvedDefaultDataSource ,内部实际是从 resolvedDataSources  和 resolvedDefaultDataSource  来获取数据源的 。
如果数据源是可通过后台管理页面动态配置的,获取自定义的动态数据源 Bean,设置 targetDataSources  和 defaultTargetDataSource  ,但仅仅将数据源维护在 targetDataSources  是不会生效的,还得手动调用 afterPropertiesSet()  方法再次转换。
getConnection()  和 determineTargetDataSource()  确定数据源创建链接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public  Connection getConnection ()  throws  SQLException {                   return  determineTargetDataSource().getConnection(); } @Override public  Connection getConnection (String username, String password)  throws  SQLException {    return  determineTargetDataSource().getConnection(username, password); } 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; } 
determineCurrentLookupKey()  数据源 Key
1 2 @Nullable protected  abstract  Object determineCurrentLookupKey () ;
这是一个抽象方法,AbstractRoutingDataSource 的具体子类必须实现此方法,且不为空,返回的是数据源 Map( Map<Object, DataSource> resolvedDataSources  ) 的 Key,即确定要使用的数据源的 Key。
 
实现与使用 基于以上分析,应该对动态数据源实现原理有个初步了解。
下面是基于 Spring Boot + Druid + Mybatis + MySQL 配置动态数据源来实现读写分离。
动态数据源实现 数据库 准备至少两个数据库
sakila_master  和 sakila_slave  两个库,数据库及表结构和表数据使用 MySQL 的测试数据库 Github -> datacharmer / test_db -> sakila 。
创建项目 创建 Spring Boot 项目,排除掉 Spring Boot 默认启用数据源自动配置
1 2 3 4 5 6 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public  class  DatasourceApplication  {    public  static  void  main (String[] args)  {         SpringApplication.run(DatasourceApplication.class, args);     } } 
引入依赖 pom.xml 文件导入依赖包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 <?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 />       </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 属性配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 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 logging.level.com.springboot.datasource.mapper =debug 
定义当前线程变量 定义当前线程绑定数据源 Key
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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();     } } 
定义动态数据源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 @Configuration @MapperScan(basePackages = "com.springboot.datasource.mapper") public  class  DataSourceConfig  {         @Bean(name = "dataSourceMaster")      @ConfigurationProperties("spring.datasource.master")      public  DataSource dataSourceMaster ()  {         return  DruidDataSourceBuilder.create().build();     }          @Bean(name = "dataSourceSlave")      @ConfigurationProperties("spring.datasource.slave")      public  DataSource dataSourceSlave ()  {         return  DruidDataSourceBuilder.create().build();     }          @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;     }          @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;     }          @Primary      @Bean(name = "sqlSessionFactory")      public  SqlSessionFactory sqlSessionFactory (@Qualifier("dynamicDataSource")  DataSource dataSource)  throws  Exception {         SqlSessionFactoryBean  sqlSessionFactoryBean  =  new  SqlSessionFactoryBean ();         sqlSessionFactoryBean.setDataSource(dataSource);                  ResourcePatternResolver  resolver  =  new  PathMatchingResourcePatternResolver ();         sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml" ));         sqlSessionFactoryBean.setTypeAliasesPackage("com.springboot.datasource.entity" );         sqlSessionFactoryBean.setConfiguration(mybatisConfiguration());         return  sqlSessionFactoryBean.getObject();     }          @Primary      @Bean(name = "sqlSessionTemplate")      public  SqlSessionTemplate sqlSessionTemplate (@Qualifier("sqlSessionFactory")  SqlSessionFactory sqlSessionFactory)  {         return  new  SqlSessionTemplate (sqlSessionFactory);     }          @Bean      public  DataSourceTransactionManager transactionManager (@Qualifier("dynamicDataSource")  DynamicDataSource dynamicDataSource)  {         return  new  DataSourceTransactionManager (dynamicDataSource);     } } 
定义查找数据源 Key 继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法,获取数据源 Key
1 2 3 4 5 6 7 8 9 10 public  class  DynamicDataSource  extends  AbstractRoutingDataSource  {    @Override      protected  Object determineCurrentLookupKey ()  {         return  DataSourceHolder.getDataSource();     } } 
定义数据源枚举 也可以使用 String 类型的静态变量。
1 2 3 public  enum   DataSourceEnum  {    MASTER,SLAVE } 
定义数据源注解 用户可以使用注解来显式指定要使用的数据源。
1 2 3 4 5 6 7 8 @Retention(value = RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public  @interface  DataSourceSelector {    DataSourceEnum name () ; } 
创建数据源切换AOP 下面示例使用的是环绕通知,最终必须清空当前线程中的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @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  对象中进行后续的 commit  或 rollback  等操作。即开启了事务后是不能切换数据源的,切换数据源会无效 ,也就是说,切换数据源要在开启事务之前执行。
本示例中动态切换数据源的 AOP 和注解是作用在 Mapper 层的方法上,最简单的修改是将 AOP 的切点或注释作用在 Controller 中的方法上。不过此方式就使得 Controller 层的责职有些混乱。
完美解决思路 :最完美的方式在业务层,开启事务之前就完成数据源切换。自定义事务切面,使用 Order 注解,属性值大于数据源切换切面的值,即 事务切面  在 数据源切换切面  后面执行。
注解事务控制AOP 此方式是关闭 Spring Boot 的事务自动配置,创建自定义的事务切面(AOP),通过数据源获取事务状态控制事务。
关闭 Spring Boot 事务自动配置
1 2 3 4 5 6 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class}) public  class  DatasourceApplication  {    public  static  void  main (String[] args)  {         SpringApplication.run(DatasourceApplication.class, args);     } } 
修改数据源切换 AOP 的切点表达式指向 service 层
1 2 3 @Pointcut("execution(* com.springboot.datasource.service..*(..))") public  void  pointcut ()  {} 
创建事务控制 AOP
自定义事务控制AOP 使用了前置通知开启事务,后置通知提交事务,异常通知回滚事务,使用了组合切点,精确定位到 service 层的 Transactional 注解,即关闭了 Spring Boot 事务自动配置后,Transactional 注解在这里仍然有效(此时,这个 Transactional  相当于是自定义的注解了)。
注意 :数据源切换AOP的 @Order 注解值(@Order(Integer.MAX_VALUE - 2000) )
TransactionalAop.class 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Order(Integer.MAX_VALUE - 1000) @Aspect @Component @Scope("prototype") public  class  TransactionalAop  {    private  TransactionStatus transactionStatus;     @Autowired      private  TransactionUtils transactionUtils;          @Pointcut("execution(* com.springboot.datasource.service..*(..))")      public  void  servicePointcut ()  {     }          @Pointcut("@annotation(transactional)")      public  void  txAnnotationPointcut (Transactional transactional)  {     }          @Before(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "joinPoint,transactional")      public  void  before (JoinPoint joinPoint, Transactional transactional)  throws  Throwable {                  this .transactionStatus = transactionUtils.begin(transactional);     }          @AfterReturning(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")      public  void  afterReturning (Transactional transactional)  {         transactionUtils.commit(transactionStatus);     }          @AfterThrowing(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")      public  void  afterThrowing (Transactional transactional)  {         transactionUtils.rollback(transactionStatus);     } } 
从上面代码可以看出,AOP 常用的几种细粒度 通知类型  可以与事务的操作完美契合,可以说 AOP 是为事务而生的,再而扩展适配到其它领域(个人理解,不接受反驳)。
注意 :网上有些文章中的事务通知使用的是 @Around  环绕通知,但没有返回值,此方式是不恰当的,有比较大的局限性,不能在 AOP 层忽略业务层的返回数据;有的有返回值,在返回值前提交事务,是否有其它问题待验证。
TransactionUtils.class 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Component public  class  TransactionUtils  {    private  static  final  Logger  logger  =  LogManager.getLogger(TransactionUtils.class);     @Autowired      private  DataSourceTransactionManager dataSourceTransactionManager;          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());                           TransactionStatus  transactionStatus  =  dataSourceTransactionManager.getTransaction(txAttribute);         logger.info("begin transactional" );         return  transactionStatus;     }          public  void  commit (TransactionStatus transactionStatus)  {         dataSourceTransactionManager.commit(transactionStatus);         logger.info("commit transactional" );     }          public  void  rollback (TransactionStatus transactionStatus)  {         dataSourceTransactionManager.rollback(transactionStatus);         logger.info("rollback transactional" );     } } 
注意:  TransactionUtils  类可以在业务实现类中注入,在方法的业务逻辑开始前使用此工具类手动开启事务,正常执行完后提交事务,抛出异常后回滚事务,这就是编程式事务 ,但这是不可取的。
 
全局事务控制AOP 此方式是采用 Spring AOP 方法名称匹配方法来开启事务,此方式相对就没有注解事务灵活。
重点还是在 @Order  注解的值要大于数据源切换AOP 的 @Order 值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @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());     } } 
动态数据源使用 定义实体类 1 2 3 4 5 6 7 8 9 10 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; 	 } 
定义 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @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 接口
1 2 3 4 5 6 7 8 9 public  interface  ActorService  {    Actor getById (Long id) ;     Actor save (Actor actor) ;     List<Actor> getActor (Actor actor) ;     void  saveActorList (Boolean errorFlag) ; } 
Service 接口实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @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 接口
1 2 3 4 5 6 7 8 9 10 11 12 @Repository public  interface  ActorMapper  {    Actor getById (@Param("id")  Long id) ;     void  save (Actor actor) ;     List<Actor> getActor (Actor actor) ;     void  saveActorList (List<Actor> actorList) ; } 
Mapper XML 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <?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 2019 -12 -14  16 :36 :11.327   INFO 15036  --- [nio-8080 -exec-1 ] c.s.d.c.d.aspect.DataSourceAspect        : DataSourceSelector name:SLAVE, DataSource:SLAVE2019 -12 -14  16 :36 :23.927   INFO 15036  --- [nio-8080 -exec-1 ] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1 ,slave} inited
调用保存接口,抛出异常触发事务回滚,查看数据是否有写入库,预期是事务回滚,数据库没有脏数据。
 
动态数据源扩展 需求:假如读写分离数据库是 多主多从  的架构,并要满足数据源可动态配置即时生效,且不修改源码和配置文件。或 Saas 系统,每个客户有自己独自的数据库,业务需要根据客户对应的数据库来进行动态切换。
思路:
将多个数据源信息存入到 MySQL 中。
基于动态数据源实现,配置默认数据源,在项目启动时从 MySQL 中查询数据源信息。
可通过实现 CommandLineRunner  接口,重写 run(String… args)  方法来处理。
调用数据源配置,创建多个数据源并注册为 Bean,加入到一个集合中。
定义使用数据源策略,轮循或随机访问,或 根据用户 ID 与 数据源的 映射来选择对应数据源。
 
上面示例源码 GitHub -> spring-boot-datasource 
其它参考 
SpringBoot多数据源 Spring 动态切换数据源 Spring Boot 配置动态数据源 Spring 动态切换数据源及事务 SpringBoot数据源加载及其多数据源 Spring中实现多数据源事务管理 SpringBoot+MyBatis+MySQL读写分离 Spring的事务管理入门:编程式事务管理(TransactionTemplate) 手写 Spring 事务,IoC  和 MVC Spring JtaTransactionManager 实现多数据源使事务 Spring TransactionManager 源码分析