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