项目可能会有多个数据源,例如读写分离至少有两个数据库,或一个主库多个从库,或一个系统里面不同业务拆分有各自的数据库,在使用时需要确定使用正确的数据源。
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() 方法
@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); } }
targetDataSources 和 defaultTargetDataSource 是 AbstractRoutingDataSource 的两个属性,其具体子类在初始化为 Bean 时设置这两个属性值。
此方法将 targetDataSources 转换为 resolvedDataSources,将 defaultTargetDataSource 转换为 resolvedDefaultDataSource,内部实际是从 resolvedDataSources 和 resolvedDefaultDataSource 来获取数据源的 。
如果数据源是可通过后台管理页面动态配置的,获取自定义的动态数据源 Bean,设置 targetDataSources 和 defaultTargetDataSource ,但仅仅将数据源维护在 targetDataSources 是不会生效的,还得手动调用 afterPropertiesSet() 方法再次转换。
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 * 根据 Key 从 Map 中取出数据源或使用默认数据源返回 */ 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
@Nullable protected abstract Object determineCurrentLookupKey();
这是一个抽象方法,AbstractRoutingDataSource 的具体子类必须实现此方法,且不为空,返回的是数据源 Map( Map ) 的 Key,即确定要使用的数据源的 Key。
实现与使用
基于以上分析,应该对动态数据源实现原理有个初步了解。
下面是基于 Spring Boot + Druid + Mybatis + MySQL 配置动态数据源来实现读写分离。
动态数据源实现
数据库
准备至少两个数据库
sakila_master 和 sakila_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 对象中进行后续的 commit 或 rollback 等操作。即开启了事务后是不能切换数据源的,切换数据源会无效,也就是说,切换数据源要在开启事务之前执行。
本示例中动态切换数据源的 AOP 和注解是作用在 Mapper
层的方法上,最简单的修改是将 AOP 的切点或注释作用在 Controller 中的方法上。不过此方式就使得 Controller 层的责职有些混乱。
完美解决思路:最完美的方式在业务层,开启事务之前就完成数据源切换。自定义事务切面,使用 Order
注解,属性值大于数据源切换切面的值,即 事务切面 在 数据源切换切面 后面执行。
注解事务控制AOP
此方式是关闭 Spring Boot 的事务自动配置,创建自定义的事务切面(AOP),通过数据源获取事务状态控制事务。
关闭 Spring Boot 事务自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class}) public class DatasourceApplication { public static void main(String[] args) { SpringApplication.run(DatasourceApplication.class, args); } }
修改数据源切换 AOP 的切点表达式指向 service 层
@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
@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>
调用接口测试
调用保存数据的接口,在两个数据库查看保存的数据查询。
调用获取数据的接口,注意数据来自于哪个数据库。
查询日志输出,读 / 写操作是否有切换数据源。
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
调用保存接口,抛出异常触发事务回滚,查看数据是否有写入库,预期是事务回滚,数据库没有脏数据。
动态数据源扩展
需求:假如读写分离数据库是 多主多从 的架构,并要满足数据源可动态配置即时生效,且不修改源码和配置文件。或 Saas 系统,每个客户有自己独自的数据库,业务需要根据客户对应的数据库来进行动态切换。
思路:
将多个数据源信息存入到 MySQL 中。
基于动态数据源实现,配置默认数据源,在项目启动时从 MySQL 中查询数据源信息。
可通过实现 CommandLineRunner 接口,重写 run(String... args) 方法来处理。
调用数据源配置,创建多个数据源并注册为 Bean,加入到一个集合中。
定义使用数据源策略,轮循或随机访问,或 根据用户 ID 与 数据源的 映射来选择对应数据源。
上面示例源码
GitHub -> spring-boot-datasource
其它参考
注意:本文归作者所有,未经作者允许,不得转载