предисловие
- Одно дело, используете ли вы в своем проекте несколько источников данных, а другое — будете ли вы самим собой.
- В SpringBoot 2.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: true
mybatis-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-компонентов из нескольких источников данных приведенный выше код:
@Configuration
public 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<Object, Object> 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;
}
}
Это непросто объяснить, это значит инкапсулировать файл конфигурации в bean-компонент dataSource, которыйMyRoutingDataSourceЭто источник данных, который мы используем, включая конфигурацию транзакций.
MyRoutingDataSource
public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getCurrentDsStr();
}
}
вAbstractRoutingDataSourceЭто абстрактный класс, предоставляемый модулем jdbc Spring, который действует какDataSourceПосредник маршрутизации может динамически переключаться на реальный в соответствии с определенным значением ключа во время выполнения.DataSourceвыше, переписатьdetermineCurrentLookupKey()способ переключения источника данных. Это означает, что если вы хотите играть с несколькими источниками данных, вы должны использовать этот класс. Я также использовалDbContextHolderКласс инструмента (эквивалент держателя источника данных), код выглядит следующим образом, в основном скопирован из Интернета, с небольшой модификацией:
public class DbContextHolder {
/**
* 项目中配置数据源
*/
private static Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
/**
* 默认数据源
*/
private static String defaultDs = "";
/**
* 为什么要用链表存储(准确的是栈)
* <pre>
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。
* </pre>
*/
private static final ThreadLocal<Deque<String>> 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();
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源
* 只会移除掉当前线程的数据源名称
* </p>
*/
public static void clearCurrentDsStr() {
Deque<String> 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<String, DataSource> getDataSources() {
return dataSources;
}
/**
* @return
*/
public static String getDefaultDs() {
return defaultDs;
}
Конфигурация Mybatis:
Это класс конфигурации 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<Resource> 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
@Slf4j
public 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();
}
}
Этот класс представляет собой простую конфигурацию аспекта, функция состоит в том, чтобы переключить источник данных перед методом службы и настроитьDS()Аннотация, действующая на метод Service и указывающая, является ли он ведущим или подчиненным.
Конфигурация транзакции:
Вот в чем дело! Суть в том, что после описанных выше настроек несколько источников данных были настроены. Но транзакция недействительна в это время, независимо от того, ставили ли вы@TransactionalЭто не влияет ни на класс службы, ни на метод!MyRoutingDataSourceНаш пользовательский источник данных для менеджера транзакций. См. TransactionConfig:
@Aspect
@Configuration
@Slf4j
public 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<String, TransactionAttribute> 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 — это главное!
Этот код немного длиннее, так как я настраиваю транзакцию конфигурации аспекта. Суть в том, чтобы настроить менеджер транзакций и передать наш источник данных динамической маршрутизации (MyRoutingDataSource) менеджеру транзакций, чтобы наша транзакция была отброшена!
Суммировать:
- Смысл настройки нескольких источников данных заключается в настройке наследования источников данных.
AbstractRoutingDataSourceи зарегистрировать в нем несколько источников данных. - Причина, по которой транзакция не вступает в силу, заключается в том, что менеджер транзакций Spring по умолчанию не берет на себя наш пользовательский источник данных.Решение состоит в том, чтобы настроить менеджер транзакций, чтобы вставить в него наш пользовательский источник данных.