springboot+druid+mybatis多数据源动态切换

最近空闲,做了一个springboot+mybatis多数据源的动态切换!!!

鉴于网上使用注解区分数据库的方式一个区别,通过mybatis的拦截器拦截SQL语句类型调用不同数据库实现读写分离!!!

同时引入了PageHelper进行分页处理

 

例子介绍

为了简化书写SQL,这里用到了tk.mybatis

maven配置

<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>

配置文件

spring:
  application:
    name: demo
  datasource:
    master:
      # 多数据源,读写分离,写库
      url: jdbc:mysql://IP:PORT/DATABASE?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
      username: test
      password: test123
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
    slave:
      # 多数据源,读写分离, 读库
      url: jdbc:mysql://IP:PORT/DATABASE?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
      username: test 
      password: test123
      driver-class-name: com.mysql.cj.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
logging:
  level:
    com.spring.druid.tk.demo.mapper: info
mybatis:
  mapper-locations: classpath:mapper/*.xml

配置信息ip、端口、数据库、用户名和密码换上自己相应的信息即可

数据库信息实例

CREATE TABLE `blog_auth` (
  `id` varchar(32) NOT NULL COMMENT 'id',
  `user_name` varchar(100) NOT NULL COMMENT '用户名',
  `password` varchar(200) NOT NULL COMMENT '密码',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

这样数据库主库的信息就配置好了,我在这里本机上安装了3306(主库)和3307(从库)两个mysql数据库,在主库新建了表和数据后从库也自动有了数据:

主库用于写和更新删除等操作,从库通过该主库中my.ini配置文件的log-bin开启二进制同步并设定要同步的数据库,这里可以自己去百度配置一下,很简单

 

下面开始代码模块废话不多说!!!

文件目录结构

 

多数据源配置

config目录

config目录,配置了druid数据源信息和SqlSessionFactoryBean以及线程池,这里只对前两者进行描述

@Configuration
public class DataSourceConfig {

    @Value("${mybatis.mapper-locations}")
    private String mybatisPath;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DruidDataSource writeDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DruidDataSource readDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    public DynamicDataSource dynamicDataSource(DruidDataSource writeDataSource, DruidDataSource readDataSource) {
        DynamicDataSource source = new DynamicDataSource();
        source.setWriteDataSource(writeDataSource);
        source.setReadDataSource(readDataSource);
        source.setDefaultTargetDataSource(writeDataSource);
        return source;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DynamicDataSource dynamicDataSource,PageInterceptor pageInterceptor, MybatisPlugin dynamicPlugin){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setLogImpl(StdOutImpl.class);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.setCallSettersOnNulls(true);
        sqlSessionFactoryBean.setConfiguration(configuration);
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.test.test.entity");
        PathMatchingResourcePatternResolver resourceLoader = new PathMatchingResourcePatternResolver();
        Resource[] resources = new Resource[0];
        try {
            resources = resourceLoader.getResources(mybatisPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
        sqlSessionFactoryBean.setMapperLocations(resources);
        Interceptor[] plugins = new Interceptor[]{pageInterceptor, dynamicPlugin};
        sqlSessionFactoryBean.setPlugins(plugins);
        return sqlSessionFactoryBean;
    }

    @Bean
    public PageInterceptor pageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        properties.setProperty("helperDialect", "mysql");
        properties.setProperty("reasonable", "true");
        properties.setProperty("supportMethodsArguments", "true");
        properties.setProperty("params", "true");
        properties.setProperty("autoRuntimeDialect", "true");
        interceptor.setProperties(properties);
        return interceptor;
    }

    @Bean
    public MybatisPlugin dynamicPlugin() {
        MybatisPlugin plugin = new MybatisPlugin();
        return plugin;
    }

    @Bean
    public DynamicDataSourceTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
        DynamicDataSourceTransactionManager manager = new DynamicDataSourceTransactionManager();
        manager.setDataSource(dynamicDataSource);
        return manager;
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

记住PageHelper包要用5.x以后的,因为之前的PageHelper包进行sql拦截时的intercept方法中return null;拦截器未向下传递导致我的自定义拦截器未生效,爬了很久的坑终于发现!!!

同时重写了sqlSessionFactory方法把拦截配置和数据源装载进去;transactionManager方法实现了事务操作

 

重点来了,mysql拦截器

@Intercepts({@Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
), @Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
@Slf4j
@NoArgsConstructor
public class MybatisPlugin implements Interceptor {

    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
    private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
        if (!synchronizationActive) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement)objects[0];
            DynamicDataSourceGlobal dynamicDataSourceGlobal = null;
            if ((dynamicDataSourceGlobal = (DynamicDataSourceGlobal)cacheMap.get(ms.getId())) == null) {
                if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    if (ms.getId().contains("!selectKey")) {
                        dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                    } else {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                        if (sql.matches(".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*")) {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                        } else {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
                        }
                    }
                } else {
                    dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                }
                log.warn("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", new Object[]{ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name()});
                cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
            }

            DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
            log.info("-----------------------------------当前数据库为:{}", DynamicDataSourceHolder.getDataSource());
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return target instanceof Executor ? Plugin.wrap(target, this) : target;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

mybatis定义了Interceptor接口,实现对应的方法完成拦截,类名上的注解标注了要拦截的SQL类型

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这里的intercept方法进来进行了一个事务判断,如果标注了事务操作,那么不进行拦截走默认数据库即主库,因为从库只用于读,加了事务也没用

如果非事务操作时根据sql的类型去动态决定数据走读库或写库

 

现在我们开始配置多数据源的动态切换

datasource目录

通过Spring了解到要实现多数据源的切换,我们需要继承AbstractRoutingDataSource类并重写determineCurrentLookupKey方法,上干货

@NoArgsConstructor
@Data
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 写数据库
     */
    private Object writeDataSource;
    /**
     * 读数据库
     */
    private Object readDataSource;

    @Override
    public void afterPropertiesSet() {
        if (writeDataSource == null) {
            throw new IllegalArgumentException("Property 'writeDataSource' is required");
        } else {
            this.setDefaultTargetDataSource(this.writeDataSource);
            Map<Object, Object> targetDataSources = new HashMap();
            targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), this.writeDataSource);
            if (this.readDataSource != null) {
                targetDataSources.put(DynamicDataSourceGlobal.READ.name(), this.readDataSource);
            }

            this.setTargetDataSources(targetDataSources);
            super.afterPropertiesSet();
        }
    }

    @Override
    protected Object determineCurrentLookupKey() {
        DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
        log.info("-------------------------当前使用的数据库为:{}------------------------", DynamicDataSourceHolder.getDataSource());
        return dynamicDataSourceGlobal != null && dynamicDataSourceGlobal != DynamicDataSourceGlobal.WRITE ? DynamicDataSourceGlobal.READ.name() : DynamicDataSourceGlobal.WRITE.name();
    }
}

afterPropertiesSet方法,初始化bean的时候执行,可以针对某个具体的bean进行配置。

public enum DynamicDataSourceGlobal {

    READ,
    WRITE;

    private DynamicDataSourceGlobal() {
    }

}
public final class DynamicDataSourceHolder {

    private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal();

    private DynamicDataSourceHolder() {
    }

    public static void putDataSource(DynamicDataSourceGlobal dataSource) {
        holder.set(dataSource);
    }

    public static DynamicDataSourceGlobal getDataSource() {
        return (DynamicDataSourceGlobal)holder.get();
    }

    public static void clearDataSource() {
        holder.remove();
    }

}
@NoArgsConstructor
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager implements Serializable {

    private static final long serialVersionUID = -3092901060855087254L;

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        boolean readOnly = definition.isReadOnly();
        if (readOnly) {
            DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
        } else {
            DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
        }

        super.doBegin(transaction, definition);
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        super.doCleanupAfterCompletion(transaction);
        DynamicDataSourceHolder.clearDataSource();
    }
}

 

业务操作

在进行事务操作时,判断使用哪个数据库。现在主从库就配置完成了,接下来就是entity,mapper,service和controller的配置,代码如下

entity

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlogAuth {

    private String id;

    private String userName;

    private String password;

    private Date createTime;

    private Date updateTime;

    private String remark;

}

mapper

@Mapper
@Repository
public interface BlogAuthMapper extends tk.mybatis.mapper.common.Mapper<BlogAuth> {
    BlogAuth qryBlogAuth(@Param("id")String id);
}

service

public interface Test {

    boolean test() throws Exception;
}
@Service
public class TestImpl implements Test {

    @Autowired
    private BlogAuthMapper blogAuthMapper;

    @Transactional(propagation = Propagation.REQUIRED)
    @Override
    public boolean test() throws Exception{
        BlogAuth blogAuth = blogAuthMapper.qryBlogAuth("1");
        BlogAuth blogAuth1 = new BlogAuth();
        blogAuth1.setId(UUIDUtil.getUUID());
        blogAuth1.setPassword("1231234");
        blogAuth1.setUserName("11李四11");
        blogAuth1.setCreateTime(new Date());
        int i = blogAuthMapper.insert(blogAuth1);
  、    throw new RuntimeException("故意抛的异常");
        return true;
    }
}

这里故意抛了异常去检查主从事务是否生效

@RestController
@RequestMapping("/test")
public class web {

    @Autowired
    private Test test;

    @RequestMapping(value = "/test",method = RequestMethod.POST)
    public boolean test(@RequestBody BlogAuth blogAuth) throws Exception{
        test.test();
        return true;
    }
}

至此一个简单的多数据源动态切换的项目就完成了,跑起来试试效果吧小伙伴们!!!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 是一个用于快速构建 Java 应用程序的框架。它可以与多种其他框架和组件进行整合,以实现更丰富的功能。在这里,我们将讨论如何使用 Spring Boot 整合 DruidMyBatis、JTA 分布式事务以及多数据源,同时使用 AOP 注解实现动态切换。 首先,我们可以在 Spring Boot 中集成 Druid 数据源Druid 是一个高性能的 JDBC 连接池,可以提供监控和统计功能。我们可以通过在 pom.xml 文件中添加相关的依赖,并在 application.properties 文件中配置数据源信息,来实现 Druid 的集成。 接下来,我们可以整合 MyBatis 框架,它是一种优秀的持久化解决方案。我们可以使用 MyBatis 来操作数据库,并将其与 Druid 数据源进行整合。为此,我们需要在 pom.xml 文件中添加 MyBatisMyBatis-Spring 的依赖,并配置 MyBatis 的相关配置文件。 此外,我们还可以使用 JTA(Java Transaction API)实现分布式事务。JTA 可以在分布式环境中协调多个参与者的事务操作。我们可以在 pom.xml 文件中添加 JTA 的依赖,并在 Spring Boot 的配置文件中配置 JTA 的相关属性,以实现分布式事务的支持。 在实现多数据源时,我们可以使用 Spring Boot 的 AbstractRoutingDataSource 来实现动态切换数据源。这个类可以根据当前线程或其他条件选择不同数据源来进行数据操作。我们可以通过继承 AbstractRoutingDataSource 并实现 determineCurrentLookupKey() 方法来指定当前数据源的 key。然后,在配置文件中配置多个数据源,并将数据源注入到 AbstractRoutingDataSource 中,从而实现动态切换。 最后,我们可以使用 AOP(Aspect Oriented Programming)注解来实现动态切换。AOP 是一种编程范式,可以通过在代码中插入特定的切面(Aspect)来实现横切关注点的处理。我们可以在代码中使用注解来标记需要切换数据源的方法,然后使用 AOP 技术来拦截这些方法,并根据注解中指定的数据源信息来进行数据源切换。 综上所述,通过整合 DruidMyBatis、JTA 分布式事务以及多数据源,并使用 AOP 注解实现动态切换,我们可以在 Spring Boot 中实现强大而灵活的应用程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值