项目中用不用多数据源是一回事,你自己会不会又是另一回事。SpringBoot2.0.8版本整合MybatisPlus实现多数据源很简单,但是事务总是不生效?MybatisPlus提供了多数据源插件( 链接 ),我可不可以不用?其实多数据源挺好配的,就是事务一直不生效。今天终于解决了。
项目结构:
主要的配置类就是这五个: DsAspect、 DataSourceConfiguration 、MyRoutingDataSource、MybatisConfiguration、TransactionConfig。后面我逐个的解释下每个类的作用。
配置文件:
spring: # 数据源配置 datasource: druid: type: com.alibaba.druid.pool.DruidDataSource defaultDs: master master: name: master url: jdbc:mysql://ip:3306/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver initial-size: 10 min-idle: 10 max-active: 100 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT version() validation-query-timeout: 10000 test-while-idle: true test-on-borrow: false test-on-return: false remove-abandoned: true remove-abandoned-timeout: 86400 filters: stat,wall connection-properties: druid.stat.mergeSql=true; web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true config: enabled: true # slave 数据源 slave: name: slave url: jdbc:mysql://ip:3307/wx_edu?useUnicode=true&characterEncoding=UTF-8&useSSL=false username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver #连接参数 initial-size: 10 min-idle: 10 max-active: 100 max-wait: 60000 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: SELECT version() validation-query-timeout: 10000 test-while-idle: true test-on-borrow: false test-on-return: false remove-abandoned: true remove-abandoned-timeout: 86400 filters: stat,wall connection-properties: druid.stat.mergeSql=true; web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: admin filter: stat: log-slow-sql: true slow-sql-millis: 1000 merge-sql: true wall: config: multi-statement-allow: true config: enabled: truemybatis-plus: global-config: #主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID"; id-type: 0 #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断" field-strategy: 0 #驼峰下划线转换 db-column-underline: true #刷新mapper 调试神器 refresh-mapper: true #数据库大写下划线转换 #capital-mode: true #逻辑删除配置(下面3个配置) logic-delete-value: 0 logic-not-delete-value: 1 # SQL 解析缓存,开启后多租户 @SqlParser 注解生效 # sql-parser-cache: true
DataSourceConfiguration:
主要是配置多个数据源的Bean,上代码:
@Configurationpublic class DataSourceConfiguration { /** * 默认是数据源 */ @Value("${spring.datasource.druid.defaultDs}") private String defaultDs; @Bean(name = "dataSourceMaster") @Primary @ConfigurationProperties(prefix = "spring.datasource.druid.master") public DataSource dataSourceMaster() { DataSource druidDataSource = DruidDataSourceBuilder.create().build(); DbContextHolder.addDataSource(CommonEnum.DsType.DS_MASTER.getValue(), druidDataSource); return druidDataSource; } @Bean(name = "dataSourceSlave") @ConfigurationProperties(prefix = "spring.datasource.druid.slave") public DataSource dataSourceSlave() { DataSource druidDataSource = DruidDataSourceBuilder.create().build(); DbContextHolder.addDataSource(CommonEnum.DsType.DS_SLAVE.getValue(), druidDataSource); return druidDataSource; } @Bean(name = "myRoutingDataSource") public MyRoutingDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster, @Qualifier("dataSourceSlave") DataSource dataSourceSlave) { MyRoutingDataSource dynamicDataSource = new MyRoutingDataSource(); Map targetDataResources = new HashMap<>(); targetDataResources.put(CommonEnum.DsType.DS_MASTER.getValue(), dataSourceMaster); targetDataResources.put(CommonEnum.DsType.DS_SLAVE.getValue(), dataSourceSlave); //设置默认数据源 dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster); dynamicDataSource.setTargetDataSources(targetDataResources); DbContextHolder.setDefaultDs(defaultDs); return dynamicDataSource; }}
这个没啥好解释的,就是把配置文件封装成了dataSource的Bean,其中 MyRoutingDataSource 才是我们要用的数据源,包括事务配置也要用它。
MyRoutingDataSource
public class MyRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DbContextHolder.getCurrentDsStr(); }}
其中 AbstractRoutingDataSource 是Spring的jdbc模块下提供的一个抽象类,该类充当了 DataSource 的路由中介, 能在运行时, 根据某种key值来动态切换到真正的 DataSource 上,重写其中的 determineCurrentLookupKey() 方法,可以实现数据源的切换。意思就是想玩多数据源就使用这个类就对了。我这里还用到了一个 DbContextHolder 工具类(相当于数据源的持有者),代码如下,基本上是在网上拷贝的,其中做了一点点修改:
public class DbContextHolder { /** * 项目中配置数据源 */ private static Map dataSources = new ConcurrentHashMap<>(); /** * 默认数据源 */ private static String defaultDs = ""; /** * 为什么要用链表存储(准确的是栈) *
* 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 * */ private static final ThreadLocal
* 为了支持嵌套切换,如ABC三个service都是不同的数据源 * 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。 * 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。 *
> contextHolder = new ThreadLocal() { @Override protected Object initialValue() { return new ArrayDeque(); } }; /** * 设置当前线程使用的数据源 * * @param dsName */ public static void setCurrentDsStr(String dsName) { if (StringUtils.isBlank(dsName)) { log.error("==========>dbType is null,throw NullPointerException"); throw new NullPointerException(); } if (!dataSources.containsKey(dsName)) { log.error("==========>datasource not exists,dsName={}", dsName); throw new RuntimeException("==========>datasource not exists,dsName={" + dsName +"}"); } contextHolder.get().push(dsName); } /** * 获取当前使用的数据源 * * @return */ public static String getCurrentDsStr() { return contextHolder.get().peek(); } /** * 清空当前线程数据源 *
* 如果当前线程是连续切换数据源 * 只会移除掉当前线程的数据源名称 *
*/ public static void clearCurrentDsStr() { Deque deque = contextHolder.get(); deque.poll(); if (deque.isEmpty()){ contextHolder.remove(); } } /** * 添加数据源 * * @param dsName * @param dataSource */ public static void addDataSource(String dsName, DataSource dataSource) { if (dataSources.containsKey(dsName)) { log.error("==========>dataSource={} already exist", dsName); //throw new RuntimeException("dataSource={" + dsName + "} already exist"); return; } dataSources.put(dsName, dataSource); } /** * 获取指定数据源 * * @return */ public static DataSource getDefaultDataSource() { if (StringUtils.isBlank(defaultDs)) { log.error("==========>default datasource must be configured"); throw new RuntimeException("default datasource must be configured."); } if (!dataSources.containsKey(defaultDs)) { log.error("==========>The default datasource must be included in the datasources"); throw new RuntimeException("==========>The default datasource must be included in the datasources"); } return dataSources.get(defaultDs); } /** 设置默认数据源 * @param defaultDsStr */ public static void setDefaultDs(String defaultDsStr) { defaultDs = defaultDsStr; } /**获取所有 数据源 * @return */ public static Map getDataSources() { return dataSources; } /** * @return */ public static String getDefaultDs() { return defaultDs; }
MybatisConfiguration:
这是MybatisPlus配置类,如果你用的是Mybatis要简单一点。因为Mybatis只需要配置 SqlSessionFactory ,而 MybatisPlus是配置 MybatisSqlSessionFactoryBean
@Slf4j@Configuration@AutoConfigureAfter({DataSourceConfiguration.class})@MapperScan(basePackages = {"com.sqt.edu.*.mapper*","com.sqt.edu.*.api.mapper*"})public class MybatisConfiguration { @Bean public SqlSessionFactory sqlSessionFactory(@Qualifier(value = "myRoutingDataSource") MyRoutingDataSource myRoutingDataSource) throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(myRoutingDataSource); return sqlSessionFactoryBean.getObject(); } @Bean(name = "mybatisSqlSessionFactoryBean") @Primary public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier(value = "myRoutingDataSource") DataSource dataSource) throws Exception { log.info("==========>开始注入 MybatisSqlSessionFactoryBean"); MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); Set result = new LinkedHashSet<>(16); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); try { result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*.xml"))); result.addAll(Arrays.asList(resolver.getResources("classpath*:config/mapper/*/*.xml"))); result.addAll(Arrays.asList(resolver.getResources("classpath*:mapper/*/*.xml"))); } catch (IOException e) { log.error("获取【classpath:mapper/*/*.xml,classpath:config/mapper/*/*.xml】资源错误!异常信息:{}", e); } bean.setMapperLocations(result.toArray(new org.springframework.core.io.Resource[0])); bean.setDataSource(dataSource); bean.setVfs(SpringBootVFS.class); com.baomidou.mybatisplus.core.MybatisConfiguration configuration = new com.baomidou.mybatisplus.core.MybatisConfiguration(); configuration.setLogImpl(StdOutImpl.class); configuration.setMapUnderscoreToCamelCase(true); //添加 乐观锁插件 configuration.addInterceptor(optimisticLockerInterceptor()); bean.setConfiguration(configuration); GlobalConfig globalConfig = GlobalConfigUtils.defaults(); //设置 字段自动填充处理 globalConfig.setMetaObjectHandler(new MyMetaObjectHandler()); bean.setGlobalConfig(globalConfig); log.info("==========>注入 MybatisSqlSessionFactoryBean 完成!"); return bean; }}
这里配置的 SqlSessionFactory 和 MybatisSqlSessionFactoryBean 都需要 MyRoutingDataSource 这个数据源。
DsAspect:
数据源切换切面配置类
@Order(0)@Aspect@Component@Slf4jpublic class DsAspect { /** * 配置AOP切面的切入点 * 切换放在service接口的方法上 */ @Pointcut("execution(* com.sqt..service..*Service.*(..))") public void dataSourcePointCut() { } /** * 根据切点信息获取调用函数是否用TargetDataSource切面注解描述, * 如果设置了数据源,则进行数据源切换 */ @Before("dataSourcePointCut()") public void before(JoinPoint joinPoint) { if (StringUtils.isNotBlank(DbContextHolder.getCurrentDsStr())) { log.info("==========>current thread {} use dataSource[{}]", Thread.currentThread().getName(), DbContextHolder.getCurrentDsStr()); return; } String method = joinPoint.getSignature().getName(); Method m = ((MethodSignature) joinPoint.getSignature()).getMethod(); try { if (null != m && m.isAnnotationPresent(DS.class)) { // 根据注解 切换数据源 DS td = m.getAnnotation(DS.class); String dbStr = td.value(); DbContextHolder.setCurrentDsStr(dbStr); log.info("==========>current thread {} add dataSource[{}] to ThreadLocal, request method name is : {}", Thread.currentThread().getName(), dbStr, method); } else { DbContextHolder.setCurrentDsStr(DbContextHolder.getDefaultDs()); log.info("==========>use default datasource[{}] , request method name is : {}", DbContextHolder.getDefaultDs(), method); } } catch (Exception e) { log.error("==========>current thread {} add data to ThreadLocal error,{}", Thread.currentThread().getName(), e); throw e; } } /** * 执行完切面后,将线程共享中的数据源名称清空, * 数据源恢复为原来的默认数据源 */ @After("dataSourcePointCut()") public void after(JoinPoint joinPoint) { log.info("==========>clean datasource[{}]", DbContextHolder.getCurrentDsStr()); DbContextHolder.clearCurrentDsStr(); }}
这个类就是一个简单的切面配置,作用就是在Service方法之前切换数据源,自定义一个 DS() 注解,作用到Service方法上并且标明是master还是slave即可。
事务配置:
重点来了!重点来了!经过上面那些配置,多数据源已经配置好了。但是此时事务是不生效的,无论你是把 @Transactional 作用到Service类上还是方法上,都不生效!此时你还需要配置一个事务管理器,并且把 MyRoutingDataSource 我们自定义的数据源给事务管理器。看TransactionConfig:
@Aspect@Configuration@Slf4jpublic class TransactionConfig { @Autowired ConfigurableApplicationContext applicationContext; private static final int TX_METHOD_TIMEOUT = 300; private static final String AOP_POINTCUT_EXPRESSION = "execution(*com.sqt..service..*Service.*(..))"; @Bean(name = "txAdvice") public TransactionInterceptor txAdvice() { NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource(); // 只读事务,不做更新操作 RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute(); readOnlyTx.setReadOnly(true); readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务 RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute(); requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class))); requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); requiredTx.setTimeout(TX_METHOD_TIMEOUT); Map txMap = new HashMap<>(); txMap.put("add*", requiredTx); txMap.put("save*", requiredTx); txMap.put("insert*", requiredTx); txMap.put("create*", requiredTx); txMap.put("update*", requiredTx); txMap.put("batch*", requiredTx); txMap.put("modify*", requiredTx); txMap.put("delete*", requiredTx); txMap.put("remove*", requiredTx); txMap.put("exec*", requiredTx); txMap.put("set*", requiredTx); txMap.put("do*", requiredTx); txMap.put("get*", readOnlyTx); txMap.put("query*", readOnlyTx); txMap.put("find*", readOnlyTx); txMap.put("*", requiredTx); source.setNameMap(txMap); TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager(), source); return txAdvice; } @Bean public Advisor txAdviceAdvisor(@Qualifier("txAdvice") TransactionInterceptor txAdvice) { AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); pointcut.setExpression(AOP_POINTCUT_EXPRESSION); return new DefaultPointcutAdvisor(pointcut, txAdvice); } /**自定义 事务管理器 管理我们自定义的 MyRoutingDataSource 数据源 * @return */ @Bean(name = "transactionManager") public DataSourceTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(applicationContext.getBean(MyRoutingDataSource.class)); return transactionManager; }
配置DataSourceTransactionManager是重点! ! ! 配置DataSourceTransactionManager是重点! ! !
由于我是自定义的切面配置事务,所以这个代码略长。重点是配置事务管理器,并且把我们动态路由数据源(MyRoutingDataSource)交给事务管理器,这样我们的事务才会回滚!