SpringBoot+MybatisPlus+HikariCP实现多数据源动态切换

在实际工作中,我们的系统可能是多个数据源,通过集成MyBatis-Plus和HikariCP实现多数据源动态切换是一个常见的需求。以下是一个详细的实现步骤,你可以根据自己的需要进行调整。

1. 添加依赖

首先,在pom.xml文件中添加所需的依赖:

<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.5</version>
		<relativePath />
</parent>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<!-- MyBatis-Plus -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.5.5</version>
		</dependency>
		<!-- 数据库驱动 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

HikariCP数据源不需要额外导包,springboot自动引入

2. 配置数据源属性

application.yml文件中配置多个数据源属性:

3. 配置数据源

创建一个配置类来配置多个数据源:

package com.work.config;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
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.jdbc.datasource.DataSourceTransactionManager;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.work.enums.DatabaseTypeEnum;
import com.zaxxer.hikari.HikariDataSource;

/**
 * springboot整合mybatis-plus 实现多数据源动态切换
 * 配置数据源和事务管理器 
 * @author summer
 */
@Configuration
@MapperScan(basePackages = "com.work.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {
	/**
	 * 主库数据源
	 * @return
	 */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }
    
    /**
     * 从库数据源
     * @return
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }
    
    /**
     * 动态切换数据源
     * @param masterDataSource
     * @param slaveDataSource
     * @return
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                               @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DatabaseTypeEnum.MASTER.getDatabaseType(), masterDataSource);
        targetDataSources.put(DatabaseTypeEnum.SLAVE.getDatabaseType(), slaveDataSource);

        DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
        //设置默认的数据源,当没有指定数据源时,将使用主库为默认数据源
        dataSource.setDefaultTargetDataSource(masterDataSource);
        //将配置好的多数据源设置到DynamicRoutingDataSource中
        dataSource.setTargetDataSources(targetDataSources);
        return dataSource;
    }
    
    /**
     * 配置sqlSession
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
    	 // 导入mybatissqlsession配置
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        // 指明数据源
        sessionFactory.setDataSource(dataSource);
        //多数据源必须在定义数据源时添加以下2句,否则分页插件无效
        Interceptor[] interceptors = new Interceptor[] { mybatisPlusInterceptor() };
        sessionFactory.setPlugins(interceptors);
        // 返回SqlSessionFactory实例,用于创建SqlSession,使用SqlSession调用sql时不需要指定数据源,因为就在此处指定了
        return sessionFactory.getObject();
    }
    
    /**
     * 配置事务管理器
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    /**
	 * mybatis-Plus 分页配置
	 * 注意:如果是单数据源,此时分页插件能正常使用,而如果是多数据源,那么还需在配置SqlSessionFactory的地方再添加配置,否则分页时查询总数total还是0
	 * @return
	 */
	@Bean
	public MybatisPlusInterceptor mybatisPlusInterceptor() {
		MybatisPlusInterceptor mybatisPlusInterceptor=new MybatisPlusInterceptor();
		PaginationInnerInterceptor paginationInnerInterceptor=new PaginationInnerInterceptor();
		paginationInnerInterceptor.setDbType(DbType.MYSQL);
		paginationInnerInterceptor.setOverflow(true);
		mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
		return mybatisPlusInterceptor;
	}
}

4.定义数据库类型枚举

package com.work.enums;
/**
 * 数据库类型枚举
 * @author summer
 *
 */
public enum DatabaseTypeEnum {
	MASTER("master", "主库"),
    SLAVE("slave", "从库"),
    ;
    private final String databaseType;
    private final String msg;
    DatabaseTypeEnum(String databaseType, String msg) {
        this.databaseType = databaseType;
        this.msg = msg;
    }
	public String getDatabaseType() {
		return databaseType;
	}
	public String getMsg() {
		return msg;
	}
}

5.动态切换数据源

使用AOP或者注解的方式实现数据源的动态切换

package com.work.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import com.work.config.DynamicRoutingDataSource;
import com.work.enums.DatabaseTypeEnum;

/**
 * AOP 实现动态数据源切换
 * @author summer
 *
 */
@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(com.work.target.ReadOnly)")
    public void useSlaveDataSource() {
    	DynamicRoutingDataSource.setDataSource(DatabaseTypeEnum.SLAVE.getDatabaseType());
    }

    @Before("@annotation(com.work.target.WriteOnly)")
    public void useMasterDataSource() {
    	DynamicRoutingDataSource.setDataSource(DatabaseTypeEnum.MASTER.getDatabaseType());
    }

    //需要每次清空ThreadLocal的内容,防止内存泄漏
    @After("@annotation(com.work.target.ReadOnly) || @annotation(com.work.target.WriteOnly)")
    public void clearDataSource() {
    	DynamicRoutingDataSource.clearDataSource();
    }
}

创建自定义注解:

package com.work.target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 读数据标识注解
 * @author summer
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
	
}

package com.work.target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 写数据标识注解
 * @author summer
 *
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteOnly {
	
}

实现DynamicRoutingDataSource类来管理数据源切换:

package com.work.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
 * 动态主从数据源
 * 使用 ThreadLocal来持有当前线程的数据源类型
 * @author summer
 *
 */
public class DynamicRoutingDataSource extends AbstractRoutingDataSource{
	private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

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

    @Override
    protected Object determineCurrentLookupKey() {
        return CONTEXT_HOLDER.get();
    }
}

6.编写测试类

实体类:

package com.work.model.db;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import lombok.Data;
@Data
@TableName(value = "check_code_info")
public class CheckCodeInfo {
	/**
	 * 主键ID  自增长
	 */
	@TableId(type = IdType.AUTO)
	private Integer id;
	private String token;
    private String checkCode;
    /**
     * 使用状态1:未使用2:已使用
     */
    private Integer status;
    /**
     * 创建时间
     */
//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    
    /**
     * 更新时间
     */
//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    /**
     * 过期时间
     */
//    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date expiTime;
}

mapper类:

package com.work.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.work.model.db.CheckCodeInfo;

public interface CheckCodeInfoMapper extends BaseMapper<CheckCodeInfo>{
    
}

Service类:

package com.work.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.work.model.db.CheckCodeInfo;

public interface CheckCodeInfoService {
	/**
	 * 读数据
	 * @param 
	 * @return
	 */
	IPage<CheckCodeInfo> getCheckCodeInfoList(int currentPage, int pageSize);
	
	/**
	 * 添加数据
	 */
	boolean addCheckCodeInfo();
}

Service实现类:

package com.work.service.impl;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.work.mapper.CheckCodeInfoMapper;
import com.work.model.db.CheckCodeInfo;
import com.work.service.CheckCodeInfoService;
import com.work.target.ReadOnly;
import com.work.target.WriteOnly;

import cn.hutool.core.util.RandomUtil;

@Service
public class CheckCodeInfoServiceImpl implements CheckCodeInfoService {
	@Autowired
	private CheckCodeInfoMapper checkCodeInfoMapper;
	/**
	 * 测试分页查询
	 * 读数据注解
	 */
	@ReadOnly
	@Override
	public IPage<CheckCodeInfo> getCheckCodeInfoList(int currentPage, int pageSize) {
//		LambdaQueryWrapper<CheckCodeInfo> lambdaQueryWrapper=new LambdaQueryWrapper<CheckCodeInfo>();
		//分页参数  mybatis-plus自带的分页,传入当前页码和每页条数
		Page<CheckCodeInfo> page=new Page<CheckCodeInfo>(currentPage,pageSize);
		//调用Mapper的分页查询方法,传入Page对象和查询条件(这里为null表示无条件查询)
		IPage<CheckCodeInfo> pageResult=checkCodeInfoMapper.selectPage(page, null);
		// 从 从库查询数据
		return pageResult;
	}
	
	/**
	 * 测试写数据
	 * 写数据注解
	 */
	@WriteOnly
	@Override
	public boolean addCheckCodeInfo() {
		CheckCodeInfo checkCodeInfo = new CheckCodeInfo();
		checkCodeInfo.setToken(RandomUtil.randomString(30));
		checkCodeInfo.setCheckCode(RandomUtil.randomNumbers(3));
		checkCodeInfo.setCreateTime(new Date());
		checkCodeInfo.setUpdateTime(new Date());
		int num = checkCodeInfoMapper.insert(checkCodeInfo);
		// 写入主库
		return num > 0 ? true : false;
	}
}

测试Controller类:

package com.work.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.work.common.result.CommonResult;
import com.work.model.db.CheckCodeInfo;
import com.work.service.CheckCodeInfoService;

/**
 * 动态切换多数据源,测试Controller类
 * @author summer
 *
 */
@RestController
@RequestMapping("/checkCode")
public class DataSourceTestController {
	@Autowired
    private CheckCodeInfoService checkCodeInfoService;
    
    /**
     * 测试数据源动态切换,从 从库读取数据
     * @return
     */
    @GetMapping("/getCheckCodeInfoList")
    public CommonResult<IPage<CheckCodeInfo>> getCheckCodeInfoList(@RequestParam(defaultValue = "1") int currentPage,
            @RequestParam(defaultValue = "10") int pageSize) {
    	return CommonResult.success(checkCodeInfoService.getCheckCodeInfoList(currentPage,pageSize));
    }
    
    /**
     * 测试数据源动态切换,从 从库读取数据
     * @return
     */
    @PostMapping("/addCheckCodeInfo")
    public CommonResult<Boolean> addCheckCodeInfo() {
    	return CommonResult.success(checkCodeInfoService.addCheckCodeInfo());
    }
}

postMan测试:

主库:

测试从库查询:

从库:

7.控制台结果,也能看到确实使用了HikariCP的数据库连接池,这样就大公告成了

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值