Springboot+Mybatis-plus多数据源以及实现事务一致性
在实际项目开发中,会同时连接2个或者多个数据库进行开发,因此我们需要配置多数据源,在使用多数据源的时候,在业务中可能会对2个不同的数据库进行插入、修改等操作,如何保证多数据源的事务一致性问题?主要解决如下问题:
- 如何配置多数据源
- 如何保证事务一致性
1.多数据源配置
如果只是配置多数据可以使用mybatis-plus的注解@DS,@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
官方文档: https://baomidou.com/pages/a61e1b/#文档-documentation
2.事务一致性
现在有2个数据库,需要同时对2个数据库中的表都进行插入操作,此时如果使用注解@Transactional就不行了。
通过配置不同的Mapper接口扫描路径使用不同的SqlSessionTemplate来实现。不同的SqlSessionTemplate就是不同的SqlSessionFactory,也就是不同的DataSource。
2.1添加POM文件
<!-- MyBatis Plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!-- 多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
2.2 配置2个不同的数据源
spring:
datasource:
dynamic:
primary: master
datasource:
master:
jdbc-url: jdbc:mysql://xxxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
username: root
password: root
slave:
jdbc-url: jdbc:mysql://xxx?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&verifyServerCertificate=false&useSSL=false
username: root
password: 123
2.3 创建2个mapper包
2个mapper包分别对应存放2个数据源对应的mapper文件,这个里面没有什么特殊的,和之前怎么做现在还是怎么做
- 创建MasterDataSourceConfig配置文件
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.qz.soft.sampling.config.MybatisPlusConfig;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @author sean
* @date 2021/12/23
*/
@Configuration
@MapperScan(basePackages = "com.sean.soft.sampling.mapper.master",sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
@Resource
private MybatisPlusConfig mybatisPlusConfig;
@Primary
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public DataSource masterDataSource()
{
return DataSourceBuilder.create().build();
}
@Primary
@Bean("masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
//如果要使用mybatis-plus的功能的话需要使用MybatisSqlSessionFactoryBean,不要使用SqlSessionFactoryBean,否则使用mybatis-plus里面的方法会报错找不到该方法
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
bean.setConfiguration(configuration);
//添加分页功能
Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()};
bean.setPlugins(plugins);
//设置全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setIdentifierGenerator(new CustomIdGenerator());
globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID));
globalConfig.setBanner(false);
bean.setGlobalConfig(globalConfig);
bean.setTypeAliasesPackage("com.qz.soft.sampling.entity");
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/*.xml"));
return bean.getObject();
}
@Primary
@Bean("masterSqlSessionTemplate")
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory")SqlSessionFactory sqlSessionFactory)
{
return new SqlSessionTemplate(sqlSessionFactory);
}
@Primary
@Bean("masterTransactionManager")
public PlatformTransactionManager masterTransactionManager(@Qualifier("masterDataSource")DataSource dataSource)
{
return new DataSourceTransactionManager(dataSource);
}
}
- 创建SlaveDataSourceConfig配置文件
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.qz.soft.sampling.config.MybatisPlusConfig;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @author sean
* @date 2021/12/23
*/
@Configuration
@MapperScan(basePackages = "com.sean.soft.sampling.mapper.slave",sqlSessionFactoryRef = "slaveSqlSessionFactory")
public class SlaveDataSourceConfig {
@Resource
private MybatisPlusConfig mybatisPlusConfig;
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.slave")
public DataSource masterDataSource()
{
return DataSourceBuilder.create().build();
}
@Bean("slaveSqlSessionFactory")
public SqlSessionFactory slaveSqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
bean.setConfiguration(configuration);
//添加分页功能
Interceptor[] plugins = {mybatisPlusConfig.mybatisPlusInterceptor()};
bean.setPlugins(plugins);
//全局配置
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setIdentifierGenerator(new CustomIdGenerator());
globalConfig.setDbConfig(new GlobalConfig.DbConfig().setIdType(IdType.ASSIGN_ID));
globalConfig.setBanner(false);
bean.setGlobalConfig(globalConfig);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/slave/*.xml"));
return bean.getObject();
}
@Bean("slaveSqlSessionTemplate")
public SqlSessionTemplate slaveSqlSessionTemplate(@Qualifier("slaveSqlSessionFactory")SqlSessionFactory sqlSessionFactory)
{
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean("slaveTransactionManager")
public PlatformTransactionManager slaveTransactionManager(@Qualifier("slaveDataSource")DataSource dataSource)
{
return new DataSourceTransactionManager(dataSource);
}
}
2.4 创建自定义注解@CustomTransaction
/**
* @author sean
* @date 2021/12/23
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER,ElementType.METHOD})
public @interface CustomTransaction {
String[] value() default {};
}
2.5 创建AOP切面,解析自定义注解
import cn.hutool.core.util.ArrayUtil;
import com.qz.soft.sampling.util.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.util.Stack;
/**
* @author sean
* @date 2021/12/23
*/
@Slf4j
@Aspect
@Configuration
public class TransactionAop {
@Pointcut("@annotation(com.qz.soft.sampling.annotation.CustomTransaction)")
public void CustomTransaction() {
}
@Around(value = "CustomTransaction() && @annotation(annotation)")
public Object syncLims(ProceedingJoinPoint joinPoint, CustomTransaction annotation) throws Throwable {
Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack = new Stack<>();
Stack<TransactionStatus> transactionStatusStack = new Stack<>();
try {
if (!openTransaction(dataSourceTransactionManagerStack, transactionStatusStack, annotation)) {
return null;
}
Object ret = joinPoint.proceed();
commit(dataSourceTransactionManagerStack,transactionStatusStack);
return ret;
}catch (Throwable e)
{
rollback(dataSourceTransactionManagerStack,transactionStatusStack);
log.error(String.format("MultTransactionAspect, method:%s-%s occors error:",joinPoint.getTarget().getClass().getSimpleName(),
joinPoint.getSignature().getName()),e);
throw e;
}
}
/**
* 开启事务处理方法
*
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
* @param multiTransaction
* @return
*/
public Boolean openTransaction(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack, CustomTransaction multiTransaction) {
String[] transactionManagerNames = multiTransaction.value();
if (ArrayUtil.isEmpty(transactionManagerNames)) {
return false;
}
for (String beanName : transactionManagerNames) {
DataSourceTransactionManager dataSourceTransactionManager = (DataSourceTransactionManager) BeanUtil.getBean(beanName);
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
transactionStatusStack.push(transactionStatus);
dataSourceTransactionManagerStack.push(dataSourceTransactionManager);
}
return true;
}
/**
* 提交处理方法
*
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
*/
private void commit(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().commit(transactionStatusStack.pop());
}
}
/**
* 回滚处理方法
* @param dataSourceTransactionManagerStack
* @param transactionStatusStack
*/
private void rollback(Stack<DataSourceTransactionManager> dataSourceTransactionManagerStack,
Stack<TransactionStatus> transactionStatusStack) {
while (!dataSourceTransactionManagerStack.isEmpty()) {
dataSourceTransactionManagerStack.pop().rollback(transactionStatusStack.pop());
}
}
}
需要用到的工具类:BeanUtil
@Component
public class BeanUtil implements ApplicationContextAware {
protected static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String name) {
return context.getBean(name);
}
public static <T> T getBean(Class<T> c){
return context.getBean(c);
}
}
MybatisPlusConfig配置类
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setDbType(DbType.MYSQL);
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
自定义主键生成策略
@Slf4j
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
return UIDGenerator.getUID();
}
}
@Slf4j
public class UIDGenerator {
/** 开始时间截 (2017-11-06) */
private final long twepoch = 1509976472321L;
private final long workerIdBits = 3L;
//最大为7
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long timestampLeftShift = workerIdBits;
private long workerId;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
private static class UIDGeneratorHolder {
private static final UIDGenerator instance = new UIDGenerator();
}
private static UIDGenerator get(){
return UIDGeneratorHolder.instance;
}
public static long getUID() {
return getUID(null);
}
public static long getUID(Long workerId) {
UIDGenerator generator = get();
if(workerId == null){
workerId = 0l;
}else if (workerId.longValue() > generator.maxWorkerId || workerId.longValue() < 0) {
throw new IllegalArgumentException(String.format("workId不能大于%d或小于0", generator.maxWorkerId));
}
generator.workerId = workerId;
return generator.nextId();
}
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
private synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
// throw new RuntimeException(
// String.format("时间被回退,生成的无效时间戳%d", lastTimestamp - timestamp));
log.error("时间被回退,生成的无效时间戳{}", lastTimestamp - timestamp);
}
//如果是同一时间生成的,则重新获取
if (lastTimestamp == timestamp) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
//上次生成ID的时间截
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift) | workerId;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
}
这样我们就完成了整个代码的编写,下面就进行测试,测试的时候只需要在方法上使用自定义注解@CustomTransaction(value = {"masterTransactionManager","slaveTransactionManager"})