针对使用SpringBoot + MybatisPlus架构,需要用到多数据源,并且在一个方法内可能多个数据源都有事务要求的情况,做实现方法分享,源代码如下:https://github.com/guzhangyu/learn-spring-cloud/tree/master/springboot-multidb
。
1、 多数据源配置
动态指定数据源的类:
@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey();
log.info("当前数据源是:{}", dataSourceName);
return DynamicDataSourceContextHolder.getDataSourceRouterKey();
}
}
其中 DynamicDataSourceContextHolder用于保存当前线程上下文的数据源名:
/**
* 数据源设置工具类
*/
@Slf4j
public class DynamicDataSourceContextHolder {
/**
* 存储已经注册的数据源的key
*/
public static List<String> dataSourceIds = new ArrayList<>();
/**
* 线程级别的私有变量
*/
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
public static String getDataSourceRouterKey () {
return HOLDER.get();
}
public static void setDataSourceRouterKey (String dataSourceRouterKey) {
log.info("切换至{}数据源", dataSourceRouterKey);
HOLDER.set(dataSourceRouterKey);
}
/**
* 设置数据源之前一定要先移除
*/
public static void removeDataSourceRouterKey () {
HOLDER.remove();
}
/**
* 判断指定DataSrouce当前是否存在
*
* @param dataSourceId
* @return
*/
public static boolean containsDataSource(String dataSourceId){
return dataSourceIds.contains(dataSourceId);
}
}
加载多数据源的配置并注册数据源类,在启动类上需要加上 @Import(DynamicDataSourceRegister.class) 注解:
/**
* 动态数据源注册
* 实现 ImportBeanDefinitionRegistrar 实现数据源注册
* 实现 EnvironmentAware 用于读取application.yml配置
*/
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* 配置上下文(也可以理解为配置文件的获取工具)
*/
private Environment evn;
/**
* 别名
*/
private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
/**
* 由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
*/
static {
aliases.addAliases("url", new String[]{"jdbc-url"});
aliases.addAliases("username", new String[]{"user"});
}
/**
* 存储我们注册的数据源
*/
private static final Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();
/**
* 参数绑定工具 springboot2.0新推出
*/
private Binder binder;
/**
* ImportBeanDefinitionRegistrar接口的实现方法,通过该方法可以按照自己的方式注册bean
*
* @param annotationMetadata
* @param beanDefinitionRegistry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
// 获取数据源配置
Map config = (Map)binder.bind("spring.datasource.druid.master", Map.class).get().get("0");
registerDataSource(config);
config = (Map)binder.bind("spring.datasource.druid.slave", Map.class).get().get("0");
registerDataSource(config);
// bean定义类
GenericBeanDefinition define = new GenericBeanDefinition();
// 设置bean的类型,此处DynamicRoutingDataSource是继承AbstractRoutingDataSource的实现类
define.setBeanClass(DynamicRoutingDataSource.class);
// 需要注入的参数
MutablePropertyValues mpv = define.getPropertyValues();
// 添加默认数据源,避免key不存在的情况没有数据源可用
mpv.add("defaultTargetDataSource", customDataSources.get("master"));
// 添加其他数据源
mpv.add("targetDataSources", customDataSources);
// 将该bean注册为datasource,不使用springboot自动生成的datasource
beanDefinitionRegistry.registerBeanDefinition("datasource", define);
log.info("注册数据源成功,一共注册{}个数据源", customDataSources.keySet().size());
}
private DataSourceTransactionManager registerDataSource(Map config) {
// 绑定参数
DataSource consumerDatasource = bind(getDataSourceType((String) config.get("type")), config);
// //设置事务
DataSourceTransactionManager secondDataSourceTransactionManager = new DataSourceTransactionManager();
secondDataSourceTransactionManager.setDataSource(consumerDatasource);
// 获取数据源的key,以便通过该key可以定位到数据源
String key = config.get("key").toString();
customDataSources.put(key, consumerDatasource);
// 数据源上下文,用于管理数据源与记录已经注册的数据源key
DynamicDataSourceContextHolder.dataSourceIds.add(key);
log.info("注册数据源{}成功", key);
return secondDataSourceTransactionManager;
}
public DataSource getDataSource(String key){
return customDataSources.get(key);
}
/**
* 通过字符串获取数据源class对象
*
* @param typeStr
* @return
*/
private Class<? extends DataSource> getDataSourceType(String typeStr) {
Class<? extends DataSource> type;
try {
if (StringUtils.hasLength(typeStr)) {
// 字符串不为空则通过反射获取class对象
type = (Class<? extends DataSource>) Class.forName(typeStr);
} else {
// 默认为hikariCP数据源,与springboot默认数据源保持一致
type = DruidDataSource.class;
}
return type;
} catch (Exception e) {
throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //无法通过反射获取class对象的情况则抛出异常,该情况一般是写错了,所以此次抛出一个runtimeexception
}
}
/**
* 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,目的是尽量保证我们自己添加的数据源构造过程与springboot保持一致
*
* @param result
* @param properties
*/
private void bind(DataSource result, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
// 将参数绑定到对象
binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
}
private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
// 通过类型绑定参数并获得实例对象
return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
}
/**
* @param clazz
* @param sourcePath 参数路径,对应配置文件中的值,如: spring.datasource
* @param <T>
* @return
*/
private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) {
Map properties = binder.bind(sourcePath, Map.class).get();
return bind(clazz, properties);
}
/**
* EnvironmentAware接口的实现方法,通过aware的方式注入,此处是environment对象
*
* @param environment
*/
@Override
public void setEnvironment(Environment environment) {
log.info("开始注册数据源");
this.evn = environment;
// 绑定配置器
binder = Binder.get(evn);
}
}
spring:
datasource:
druid:
defaultDs: master
master:
- key: master
name: master
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/dorm?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
slave:
- key: slave
name: slave
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/jkm?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
username: root
password: root
mybatis-plus:
global-config:
id-dtype: 0
field-strategy: 0
db-config:
logic-delete-value: 0
logic-not-delete-value: 1
mapper-locations: classpath:mybatis/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
cache-enabled: true
通过aop实现在mapper层切换数据源:
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
String value() default "master"; //该值即key值
}
@Aspect
@Component
@Slf4j
public class MapperAspect {
@Pointcut("execution(* com.learn.springboot.mapper.*Mapper.*(..))")
public void pointCut(){
}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
DataSource annotation = getDataSourceAnnotation(joinPoint);
String dsId = annotation == null ? "master" : annotation.value();
log.info("选择数据源:{}", dsId);
DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId);
}
private DataSource getDataSourceAnnotation(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DataSource annotation = method.getAnnotation(DataSource.class);
if(annotation!=null) {
return annotation;
}
Class<?>[] interfaces = joinPoint.getTarget().getClass().getInterfaces();
for(Class<?> anInterface: interfaces) {
annotation = anInterface.getAnnotation(DataSource.class);
if(annotation!=null) {
return annotation;
}
}
return null;
}
@After("pointCut()")
public void after() {
DynamicDataSourceContextHolder.removeDataSourceRouterKey();
}
}
2、多数据源事务支持
目标效果是在一个方法中可以指定哪几个数据源要加上事务支持。
Spring的事务管理器 DataSourceTransactionManager:
绑定到当前线程后,每次拿connection不会调用determineCurrentLookupKey() 方法去获取不同的数据源,从而拿到不同的connection,而是直接去拿这里绑定的connection。
所以使用原生的事务管理器并不能完成我们需要的功能,改成直接在aop中拿connection来开启事务:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TransactionMulti {
String[] value() default {};
int transactionType() default Connection.TRANSACTION_READ_UNCOMMITTED;
}
@Aspect
@Component
@Slf4j
public class MultiTransactionManagerAop {
private DynamicRoutingDataSource dataSourceRouting = new DynamicRoutingDataSource();
@Pointcut("@annotation(com.learn.springboot.annotation.TransactionMulti)")
public void annotationPointcut(){}
@Around("annotationPointcut()")
public void roundExecute(ProceedingJoinPoint joinpoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();
Method method = methodSignature.getMethod();
TransactionMulti annotation = method.getAnnotation(TransactionMulti.class);
String[] values = annotation.value();
int transactionType = annotation.transactionType();
//把涉及到的连接绑定到线程上,开启事务,关闭自动提交
begin(values, transactionType);
//正真执行了 方法
joinpoint.proceed();
//commit
dataSourceRouting.doCommit();
}
@AfterThrowing(pointcut = "annotationPointcut()",throwing = "e")
public void handleThrowing(JoinPoint joinPoint, Exception e) {//controller类抛出的异常在这边捕获
try {
dataSourceRouting.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
private void begin(String[] values,int transactionType) throws SQLException {
for (String value : values) {
DataSource dataSource = DynamicDataSourceRegister.getDataSource(value);
if(dataSource == null){
log.error("没有找到数据源:{}", value);
continue;
}
Connection connection = dataSource.getConnection();
prepareTransactionalConnection(connection,transactionType);
connectBegin(connection);
//绑定到线程上面
dataSourceRouting.bindConnection(value, connection);
}
}
/**
* 开启事物的一些准本工作
*/
private void connectBegin(Connection connection) throws SQLException {
if(connection!=null){
try {
if(connection.getAutoCommit()){
connection.setAutoCommit(false);
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 设置隔离级别
* @param con
* @throws SQLException
*/
protected void prepareTransactionalConnection(Connection con,int transactionType)
throws SQLException {
if (TransactionTypeEnum.isNotDefined(transactionType)){
throw new SqlSessionException("当前事物隔离级别未被定义");
}
con.setTransactionIsolation(transactionType);
}
}
public enum TransactionTypeEnum {
TRANSACTION_NONE(0,"无事务"),
TRANSACTION_READ_UNCOMMITTED(1,"允许读脏,不可重读,幻读"),
TRANSACTION_READ_COMMITTED(2,"仅允许读取已提交的数据,即不能读脏,但是可能发生不可重读和幻读"),
TRANSACTION_REPEATABLE_READ(4,"不可读脏,保证同一事务重复读取相同数据,但是可能发生幻读"),
TRANSACTION_SERIALIZABLE(8,"串行事务,保证不读脏,可重复读,不可幻读");
private int value;
private String details;
TransactionTypeEnum() {
}
TransactionTypeEnum(int value, String details) {
this.value = value;
this.details = details;
}
public static boolean isNotDefined(Integer value) {
TransactionTypeEnum[] transactionTypeEnums = values();
for (TransactionTypeEnum transactionTypeEnum : transactionTypeEnums) {
if (transactionTypeEnum.getValue()==value) {
return false;
}
}
return true;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
}
这里有一个问题需要解决:因为现在事务自己管理,mybatis每次拿完connection就自动调用close和commit方法,这样导致我自己操作的事务失效,所以要先让mybatis不能自作主张关闭connection。
解决方法: spring data获取connection的时候,给它包装类,覆盖原来的close和commit方法。
类DynamicRoutingDataSource 中覆盖getConnection()方法:
//把当前事务下的连接塞入,用于事务处理
final static ThreadLocal<Map<String, ConnectWrap>> connectionThreadLocal = new ThreadLocal<>();
/**
* 如果 在connectionThreadLocal 中有 说明开启了事务,就从这里面拿
*
* @return
* @throws SQLException
*/
@Override
public Connection getConnection() throws SQLException {
Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
//没开事务 直接走
return determineTargetDataSource().getConnection();
} else {
//开了事务,从当前线程中拿,而且拿到的是 包装过的connect 只有我能关闭O__O "…
String currentName = (String) determineCurrentLookupKey();
return stringConnectionMap.get(currentName);
}
}
在这个包装类中
只有手动调用 commit(true) 和 close(true) 才会真正提交和关闭连接。
doCommit() 和 rollback() 方法如下:
/**
* 提交事物
*
* @throws SQLException
*/
public void doCommit() throws SQLException {
//System.out.println("commit:" + connectionThreadLocal.get().toString());
Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
return;
}
for (String dataSourceName : stringConnectionMap.keySet()) {
ConnectWrap connection = stringConnectionMap.get(dataSourceName);
connection.commit(true);
connection.close(true);
}
removeConnectionThreadLocal();
}
/**
* 撤销事物
*
* @throws SQLException
*/
public void rollback() throws SQLException {
// System.out.println("rollback:" + connectionThreadLocal.get().toString());
Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
if (stringConnectionMap == null) {
return;
}
for (String dataSourceName : stringConnectionMap.keySet()) {
ConnectWrap connection = stringConnectionMap.get(dataSourceName);
connection.rollback();
connection.close(true);
}
removeConnectionThreadLocal();
}
public void removeConnectionThreadLocal() {
// System.out.println("remove:" + connectionThreadLocal.get().toString());
connectionThreadLocal.remove();
}
3、测试
@DataSource("slave")
public interface IdentifyScoreMapper extends BaseMapper<IdentifyScore> {
@DataSource("slave")
@Update("update identify_score set score = #{score} where id = 1")
void updateScore(@Param("score") Integer socre);
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.learn.springboot.annotation.DataSource;
import com.learn.springboot.entity.Student;
/**
* @Author zhangyugu
* @Date 2020/9/12 7:19 下午
* @Version 1.0
*/
@DataSource("master")
public interface StudentMapper extends BaseMapper<Student> {
}
@Service
public class TestTransactionService {
@Resource
StudentMapper studentMapper;
@Resource
IdentifyScoreMapper identifyScoreMapper;
// @Transactional(rollbackFor = RuntimeException.class)
@TransactionMulti(value = {"master", "slave"})
public void test() {
identifyScoreMapper.updateScore(77);
Student student = new Student();
student.setId(1L);
student.setName("test");
studentMapper.updateById(student);
if(1==1) {
throw new RuntimeException("ds");
}
identifyScoreMapper.updateScore(88);
student = new Student();
student.setId(2L);
student.setName("test");
studentMapper.updateById(student);
}
}
@Import(DynamicDataSourceRegister.class)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MultidbApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = SpringApplication.run(MultidbApplication.class, args);
TestTransactionService testTransactionService = applicationContext.getBean(TestTransactionService.class);
testTransactionService.test();
}
}
通过抛出异常与不抛出异常两种情况,看数据库数据的变化情况可以进行测试。