优雅实现Springboot+ Mybatis动态数据源

1.动态数据源介绍

1.1 业务背景

电商订单项目分正向和逆向两个部分:其中正向数据库记录了订单的基本信息,包括订单基本信息、订单商品信息、优惠卷信息、发票信息、账期信息、结算信息、订单备注信息、收货人信息等;逆向数据库主要包含了商品的退货信息和维修信息。数据量超过500万行就要考虑分库分表和读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关的操作。

1.2 解决思路

现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了。
优雅实现Springboot+ Mybatis动态数据源_第1张图片
Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因AbstractRoutingDataSource也是一个DataSource接口。应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。
查看AbstractRoutingDataSource类的源码:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    @Nullable
    private Map<Object, Object> targetDataSources;
    @Nullable
    private Object defaultTargetDataSource;
    private boolean lenientFallback = true;
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
	// 存放的数据对象的Map集合类
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;
    @Nullable
    private DataSource resolvedDefaultDataSource;

    public AbstractRoutingDataSource() {
    }
    // 初始化设置数据源
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }
	// ...
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		// 决策当前选择的数据源的Key
        Object lookupKey = this.determineCurrentLookupKey();
		// 当前选择的数据源
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }
	// 数据源Key的实现方法,由子类去实现
    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

源码中有一个核心的方法 setTargetDataSources(Map targetDataSources) ,它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey() 方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换。

2.实现过程

2.1 环境准备

// 实体类
@Data
public class Product {
	private Integer id;
	private String name;
	private Double price;
	@Override
	public String toString() {
		return "Product{" +
				"id=" + id +
				", name='" + name + '\'' +
				", price=" + price +
				'}';
	}
}

// Mapper类
public interface ProductMapper {
	@Select("select * from product")
	public List<Product> findAllProductM();

	@Select("select * from product")
	public List<Product> findAllProductS();
}

// Service类
@Service
public class ProductService {

	@Autowired(required = false)
	private ProductMapper productMapper;


	public void findAllProductM(){
		List<Product> allProductM = productMapper.findAllProductM();
		System.out.println(allProductM);
	}


	public void findAllProductS(){
		List<Product> allProductS = productMapper.findAllProductS();
		System.out.println(allProductS);
	}
	
}

// Controller类
@RestController
public class ProductController {

	@Autowired
	private ProductService productService;

	@RequestMapping("/findAllProductM")
	public String findAllProductM(){
		productService.findAllProductM();
		return "master";
	}

	@RequestMapping("/findAllProductS")
	public String findAllProductS(){
		productService.findAllProductS();
		return "slave";
	}
}

2.2 具体实现

1.配置多数据源

spring.druid.datasource.master.password=root
spring.druid.datasource.master.username=root
spring.druid.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/product_master?
useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.druid.datasource.slave.password=root
spring.druid.datasource.slave.username=root
spring.druid.datasource.slave.jdbcurl=jdbc:mysql://localhost:3306/product_slave?
useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.druid.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver

2.添加配置类,并初始化数据源

@Configuration
public class MyDataSourceAutoConfiguration {

	Logger logger = LoggerFactory.getLogger(MyDataSourceAutoConfiguration.class);

	/**
	 * 	master dataSource
	 */
	@Bean
	@ConfigurationProperties(prefix = "spring.druid.datasource.master")
	public DataSource masterDataSource(){
		logger.info("create master dataSource...");
		return DataSourceBuilder.create().build();
	}

	/**
	 * 	slave dataSource
	 */
	@Bean
	@ConfigurationProperties(prefix = "spring.druid.datasource.slave")
	public DataSource slaveDataSource(){
		logger.info("create slave dataSource...");
		return DataSourceBuilder.create().build();
	}
}

3.实现AbstractRoutingDataSource

public class RoutingDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return "master"
	}
}

在配置类中添加该数据源的注入:

@Bean
	@Primary
	public DataSource primaryDataSource(
			@Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
			@Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
	){

		RoutingDataSource routingDataSource = new RoutingDataSource();
		Map<Object, Object> map = new HashMap<>();
		map.put("master",masterDataSource);
		map.put("slave",slaveDataSource);

		routingDataSource.setTargetDataSources(map);

		return routingDataSource;

}

4.动态选择数据源对应的Key:
在Servlet的线程模型中,使用ThreadLocal存储key最合适,因此,我们编写一个RoutingDataSourceContext ,来设置并动态存储key:

public class RoutingDataSourceContext {

	static  final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

	// key:指定数据源类型 master slave
	public RoutingDataSourceContext(String key) {
		THREAD_LOCAL.set(key);
	}

	public static String getDataSourceRoutingKey(){
		String key = THREAD_LOCAL.get();
		return key == null ? "master" : key;
	}

	public void close(){
		THREAD_LOCAL.remove();
	}
}

5.更新RoutingDataSource类,直接从RoutingDataSourceContext获取数据源的Key:

public class RoutingDataSource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {

		return RoutingDataSourceContext.getDataSourceRoutingKey();
	}
}

6.在使用多数据源的地方,使用 RoutingDataSourceContext来动态设置Key

@Service
public class ProductService {

	@Autowired(required = false)
	private ProductMapper productMapper;


	public void findAllProductM(){
		String key = "master";
		RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
		List<Product> allProductM = productMapper.findAllProductM();
		System.out.println(allProductM);
	}


	public void findAllProductS(){
		String key = "slave";
		RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
		List<Product> allProductS = productMapper.findAllProductS();
		System.out.println(allProductS);
	}

}

到此为止,我们已经成功实现了数据库的动态路由访问。但是从实现上就显得有点LOW了,所以能不能更优雅一点呢,来,搞一下!!!

2.3 优化一下

我们仔细想想,Spring提供的声明式事务管理,就只需要一个 @Transactional() 注解,放在某个Java方法上,这个方法就自动具有了事务。我们也可以编写一个类似@RoutingWith(“slave”) 注解,放到某个Service的方法上,这个方法内部就自动选择了对应的数据源。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoutingWith {
	String value() default "master";
}

添加切面类,实现数据源Key的设置

@Aspect
@Component
public class RoutingAspect {
	
	@Around("@annotation(with)")
	public Object routingWithDataSource(ProceedingJoinPoint joinPoint,RoutingWith with) throws Throwable {
		// master
		String key = with.value();
		RoutingDataSourceContext routingDataSourceContext = new RoutingDataSourceContext(key);
		return  joinPoint.proceed();
	}
}

改造Service的方法:

@Service
public class ProductService {

	@Autowired(required = false)
	private ProductMapper productMapper;

	@RoutingWith("master")
	public void findAllProductM(){
		List<Product> allProductM = productMapper.findAllProductM();
		System.out.println(allProductM);
	}

	@RoutingWith("slave")
	public void findAllProductS(){
		List<Product> allProductS = productMapper.findAllProductS();
		System.out.println(allProductS);
	}
}

到此为止,我们就实现了用注解动态选择数据源的功能,而且显得优雅了一些了。

你可能感兴趣的:(Mybatis,SpringBoot,mybatis,spring,boot,java)