SpringBoot2.X+MybatisPlus+多数据源+事务

前言
  1. 项目中用不用多数据源是一回事,你自己会不会又是另一回事。
  2. SpringBoot2.0.8版本整合MybatisPlus实现多数据源很简单,但是事务总是不生效?
  3. MybatisPlus提供了多数据源插件(链接),我可不可以不用?
  4. 其实多数据源挺好配的,就是事务一直不生效。今天终于解决了。
项目结构:

xxx

主要的配置类就是这五个: DsAspect、 DataSourceConfiguration 、MyRoutingDataSource、MybatisConfiguration、TransactionConfig。后面我逐个的解释下每个类的作用。

配置文件:
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
107
108
109
110
111
112
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,上代码:

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
@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;
}

}

这个没啥好解释的,就是把配置文件封装成了dataSource的Bean,其中MyRoutingDataSource才是我们要用的数据源,包括事务配置也要用它。

MyRoutingDataSource

1
2
3
4
5
6
7
public class MyRoutingDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getCurrentDsStr();
}
}

其中AbstractRoutingDataSource是Spring的jdbc模块下提供的一个抽象类,该类充当了DataSource的路由中介, 能在运行时, 根据某种key值来动态切换到真正的DataSource上,重写其中的determineCurrentLookupKey()方法,可以实现数据源的切换。意思就是想玩多数据源就使用这个类就对了。我这里还用到了一个DbContextHolder工具类(相当于数据源的持有者),代码如下,基本上是在网上拷贝的,其中做了一点点修改:

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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;
}
MybatisConfiguration:

这是MybatisPlus配置类,如果你用的是Mybatis要简单一点。因为Mybatis只需要配置SqlSessionFactory,而 MybatisPlus是配置MybatisSqlSessionFactoryBean

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
@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;
}

}

这里配置的SqlSessionFactoryMybatisSqlSessionFactoryBean都需要MyRoutingDataSource这个数据源。

DsAspect:

数据源切换切面配置类

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
@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:

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
@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是重点! ! ! 配置DataSourceTransactionManager是重点! ! !

由于我是自定义的切面配置事务,所以这个代码略长。重点是配置事务管理器,并且把我们动态路由数据源(MyRoutingDataSource)交给事务管理器,这样我们的事务才会回滚!

总结:
  1. 配置多数据源的重点是自定义一个数据源继承AbstractRoutingDataSource,并将多个数据源注册进去。
  2. 事务不生效原因是Spring的默认事务管理器没有接管我们自定义的数据源.解决方法是配置一个事务管理器将我们自定义的数据源塞给它
顶我一下下!
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2021 ListenerSun
  • 访问人数: | 浏览次数:

请我吃个棒棒糖可否~

支付宝
微信