写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下!
GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master
项目中用不用多数据源是一回事,你自己会不会又是另一回事。* 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: 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
这个没啥好解释的,就是把配置文件封装成了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> 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
@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();
}
}
这个类就是一个简单的切面配置,作用就是在Service方法之前切换数据源,自定义一个 DS() 注解,作用到Service方法上并且标明是master还是slave即可。
事务配置:
重点来了!重点来了!经过上面那些配置,多数据源已经配置好了。但是此时事务是不生效的,无论你是把 @Transactional 作用到Service类上还是方法上,都不生效!此时你还需要配置一个事务管理器,并且把 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 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)交给事务管理器,这样我们的事务才会回滚!
来源:https://www.tuicool.com/articles/2ieiEnu