Spring-Boot 多数据源配置+动态数据源切换+多数据源事物配置实现主从数据库存储分离

网上关于动态数据源配置的博文一搜一大堆,都是拿来主义,往往把需要的人弄得不是太明白,也没有个具体的好用的简单的demo例子供人参考,本篇,我也是拿来主义,大笑,我拿来核心的core,进行demo案列整理,我只挑重要的部分讲,demo会在最后提供GitHub下载



博主 2018年3月16日14:26:47 

注: 这种多数据源的动态切换确实可以解决数据的主从分库操作,但是却有一个致命的BUG,那就是事务不但失效而且无法实现

一致性,因为涉及到跨库,因此我们必须另想办法来实现事务的ACID原则,下一篇,我会讲解如何利用atomikos来实现分布式事务的管理和应用。


Atomikos 是一个为Java平台提供增值服务的并且开源类事务管理器


一、项目目录结构图





二、多数据源SQL结构设计如下(简单主从关系)



sql脚本最后附上





三、多数据源注册(拿来主义)

DynamicDataSourceRegister.java


package com.appleyk.config;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.sql.DataSource;

import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.appleyk.datasource.DynamicDataSource;
import com.appleyk.datasource.DynamicDataSourceContextHolder;
import com.appleyk.pojo.DataSourceInfo;

/**
 * 
 * 功能描述:动态数据源注册 启动动态数据源请在启动类中(如Start)
 * 添加 @Import(DynamicDataSourceRegister.class)
 */
@Configuration
@EnableTransactionManagement
@EnableConfigurationProperties(DataSourceProperties.class)
@MapperScan("com.appleyk")
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {

	private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);

	private ConversionService conversionService = new DefaultConversionService();
	private PropertyValues dataSourcePropertyValues;

	// 如配置文件中未指定数据源类型,使用该默认值
	private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";

	// 数据源
	private DataSource defaultDataSource;
	private Map<String, DataSource> customDataSources = new HashMap<>();

	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
		// 将主数据源添加到更多数据源中
		targetDataSources.put("dataSource", defaultDataSource);
		DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");

		// 添加更多数据源
		targetDataSources.putAll(customDataSources);
		for (String key : customDataSources.keySet()) {
			DynamicDataSourceContextHolder.dataSourceIds.add(key);
		}

		// 创建DynamicDataSource
		GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
		beanDefinition.setBeanClass(DynamicDataSource.class);
		beanDefinition.setSynthetic(true);
		MutablePropertyValues mpv = beanDefinition.getPropertyValues();
		mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
		mpv.addPropertyValue("targetDataSources", targetDataSources);
		registry.registerBeanDefinition("dataSource", beanDefinition);

		System.err.println("动态数据源注册成功,从数据源个数 == " + customDataSources.size());
	}

	/**
	 * 创建DataSource
	 * 
	 * @param type
	 * @param driverClassName
	 * @param url
	 * @param username
	 * @param password
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public DataSource buildDataSource(Map<String, Object> dsMap) {
		try {
			Object type = dsMap.get("type");

			if (type == null)
				type = DATASOURCE_TYPE_DEFAULT;// 默认DataSource

			Class<? extends DataSource> dataSourceType;
			dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);

			String driverClassName = dsMap.get("driver-class-name").toString();
			String url = dsMap.get("url").toString();
			String username = dsMap.get("username").toString();
			String password = dsMap.get("password").toString();

			DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
					.username(username).password(password).type(dataSourceType);

			return factory.build();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		return null;
	}

	/**
	 * 加载多数据源配置
	 */
	@Override
	public void setEnvironment(Environment env) {
		initDefaultDataSource(env);
		initCustomDataSources(env);
	}

	/**
	 * 初始化主数据源
	 * 
	 */
	private void initDefaultDataSource(Environment env) {

		// 读取主数据源
		RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
		Map<String, Object> dsMap = new HashMap<>();
		dsMap.put("type", propertyResolver.getProperty("type"));
		dsMap.put("driver-class-name", propertyResolver.getProperty("driver-class-name"));
		dsMap.put("url", propertyResolver.getProperty("url"));
		dsMap.put("username", propertyResolver.getProperty("username"));
		dsMap.put("password", propertyResolver.getProperty("password"));

		defaultDataSource = buildDataSource(dsMap);

		dataBinder(defaultDataSource, env);

	}

	/**
	 * 为DataSource绑定更多数据
	 * 
	 * @param dataSource
	 * @param env
	 */
	private void dataBinder(DataSource dataSource, Environment env) {
		RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
		dataBinder.setConversionService(conversionService);
		dataBinder.setIgnoreNestedProperties(false);// false
		dataBinder.setIgnoreInvalidFields(false);// false
		dataBinder.setIgnoreUnknownFields(true);// true

		if (dataSourcePropertyValues == null) {
			Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties(".");
			Map<String, Object> values = new HashMap<>(rpr);
			// 排除已经设置的属性
			values.remove("type");
			values.remove("driver-class-name");
			values.remove("url");
			values.remove("username");
			values.remove("password");
			dataSourcePropertyValues = new MutablePropertyValues(values);
		}
		dataBinder.bind(dataSourcePropertyValues);
	}

	/**
	 * 初始化更多数据源
	 * 
	 */
	private void initCustomDataSources(Environment env) {
		// 读取库表中datasource获取更多数据源

		Map<String, Map<String, Object>> customInfo = getCustomDataSourceInfo();
		for (String key : customInfo.keySet()) {
			Map<String, Object> dsMap = customInfo.get(key);
			DataSource ds = buildDataSource(dsMap);
			try {
				// 判断一下 数据源是否连接成功
				ds.getConnection();
			} catch (SQLException e) {
				e.printStackTrace();
			}

			customDataSources.put(key, ds);
			dataBinder(ds, env);
		}
	}

	private Map<String, Map<String, Object>> getCustomDataSourceInfo() {
		Map<String, Map<String, Object>> customMap = new HashMap<>();
		// 从主库的slave表中,读取出从库slave的连接信息
		String sql = "select url,username,password from slave";
		JdbcTemplate jdbcTemplate = new JdbcTemplate(defaultDataSource);
		List<DataSourceInfo> infos = jdbcTemplate.query(sql, new RowMapper<DataSourceInfo>() {
			@Override
			public DataSourceInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
				DataSourceInfo info = new DataSourceInfo();
				info.setType("com.alibaba.druid.pool.DruidDataSource");
				info.setDriverClassName("com.mysql.jdbc.Driver");
				info.setUrl(rs.getString("url"));
				info.setPassWord(rs.getString("password"));
				info.setUserName(rs.getString("username"));
				// 从库名称:slave -- 对应后面的数据源注解里面的name属性
				// 这个地方可以表slave加个字段,字段名ds,值存数据源的名称
				info.setDsName("slave");
				return info;
			}
		});
		for (DataSourceInfo info : infos) {
			Map<String, Object> dsMap = new HashMap<>();
			dsMap.put("type", info.getType());
			dsMap.put("driver-class-name", info.getDriverClassName());
			dsMap.put("url", info.getUrl());
			dsMap.put("username", info.getUserName());
			dsMap.put("password", info.getPassWord());
			customMap.put(info.getDsName(), dsMap);
		}
		return customMap;
	}

	@Bean
	public PlatformTransactionManager masterTransactionManager() {
		System.err.println("masterTransactionManager=========配置主数据库的事务");
		return new DataSourceTransactionManager(defaultDataSource);
	}

	@Bean
	public PlatformTransactionManager slaveTransactionManager() {
		System.err.println("slaveTransactionManager=========配置从数据库的事务");
		return new DataSourceTransactionManager(customDataSources.get("slave"));
	}

}

注意,数据源注册成功后,如果不手动配置各自的事务,会导致后面的数据源表面虽切换成功,但是在默认的同事务下,主从业务执行的时候仍然使用的是默认的主库数据源,也就是会造成“数据源切换失效”(事务一旦开启,Connection就不能再改变)


因此,我们需要在Spring-Boot里,根据数据源的名称向spring容器中注入各自的事务(Bean),当然name我们是知道的






四、AOP实现数据源的动态切换(拿来主义)


DynamicDataSourceAspect.java


package com.appleyk.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.appleyk.annotation.DataSource;
import com.appleyk.datasource.DynamicDataSourceContextHolder;  
  
@Aspect  
@Component  
public class DynamicDataSourceAspect {  
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);  
  
    @Before("@annotation(ds)")  
    public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable {  
        String dsId = ds.name();  
        System.err.println(dsId);
        if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {  
            System.err.println("数据源[{"+ds.name()+"}]不存在,使用默认数据源 >"+point.getSignature());
        } else {  
            System.err.println("Use DataSource : "+ds.name()+">"+point.getSignature() );
            DynamicDataSourceContextHolder.setDataSourceType(ds.name());  
        }       
    }  
  
    @After("@annotation(ds)")  
    public void restoreDataSource(JoinPoint point, DataSource ds) {  
        System.err.println("Revert DataSource : "+ds.name()+" > "+point.getSignature());
        DynamicDataSourceContextHolder.clearDataSourceType();
              
    }  
}  


五、Service层实现不同业务的多数据源切换(使用注解)

(1)保存A对象信息,存进主库master ----- 表a





(2)保存B对象信息,存进从库slave ----- 表b



(3)二者事务虽然不一样,但可以根据各自的业务执行状态进行整个object对象存储的事务控制,比如,先入主库,如果主库insert成功,才向下执行从库的对象存储,如果主库事务回滚,我们可以手动抛出异常,并终止从库的数据操作;




同理,从库的service操作也可以进行相应的事物控制(事物没有进行过多的验证,可自行解决)



六、Controller层提供restful风格的API接口

ObjectController.java


package com.appleyk.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.appleyk.entity.A;
import com.appleyk.result.ResponseMessage;
import com.appleyk.result.ResponseResult;
import com.appleyk.service.ObjectService;

@RestController
@RequestMapping("/rest/v1.0.1/object")
public class ObjectController {

	@Autowired
	private ObjectService objService;

	@PostMapping("/save")
	public ResponseResult SaveObject(@RequestBody A a) throws Exception {

		if (objService.Save(a)) {
			return new ResponseResult(ResponseMessage.OK);
		}
		return new ResponseResult(ResponseMessage.INTERNAL_SERVER_ERROR);
	}
}


注意:master的a表和slave的b表结构一样,只是名称不一样

object对象  分   --- A实体对象对应主库master的a表,B实体(A实体对象构造而来)对象对应从库slave的b表


master -- a表





slave -- b表






七、API接口测试(实现object对象的分库存储)

1.  JSON数据



{
	"name": "appleyk",
	"sex": "F",
	"age":27
}



2.Spring-Boot启动





3.接口测试








a表





b表





控制台数据源切换信息打印








八、项目GitHub地址

有问题欢迎留言探讨



下一篇:Spring-Boot + Atomikos 实现跨库的分布式事务管理

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值