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

转载自: https://blog.csdn.net/Appleyk/article/details/79442006

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



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

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

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


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


一、项目目录结构图





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



sql脚本最后附上





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

DynamicDataSourceRegister.java



    
    
  1. package com.appleyk.config;
  2. import java.sql.ResultSet;
  3. import java.sql.SQLException;
  4. import java.util.HashMap;
  5. import java.util.List;
  6. import java.util.Map;
  7. import javax.sql.DataSource;
  8. import org.mybatis.spring.annotation.MapperScan;
  9. import org.slf4j.Logger;
  10. import org.slf4j.LoggerFactory;
  11. import org.springframework.beans.MutablePropertyValues;
  12. import org.springframework.beans.PropertyValues;
  13. import org.springframework.beans.factory.support.BeanDefinitionRegistry;
  14. import org.springframework.beans.factory.support.GenericBeanDefinition;
  15. import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
  16. import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
  17. import org.springframework.boot.bind.RelaxedDataBinder;
  18. import org.springframework.boot.bind.RelaxedPropertyResolver;
  19. import org.springframework.boot.context.properties.EnableConfigurationProperties;
  20. import org.springframework.context.EnvironmentAware;
  21. import org.springframework.context.annotation.Bean;
  22. import org.springframework.context.annotation.Configuration;
  23. import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
  24. import org.springframework.core.convert.ConversionService;
  25. import org.springframework.core.convert.support.DefaultConversionService;
  26. import org.springframework.core.env.Environment;
  27. import org.springframework.core.type.AnnotationMetadata;
  28. import org.springframework.jdbc.core.JdbcTemplate;
  29. import org.springframework.jdbc.core.RowMapper;
  30. import org.springframework.jdbc.datasource.DataSourceTransactionManager;
  31. import org.springframework.transaction.PlatformTransactionManager;
  32. import org.springframework.transaction.annotation.EnableTransactionManagement;
  33. import com.appleyk.datasource.DynamicDataSource;
  34. import com.appleyk.datasource.DynamicDataSourceContextHolder;
  35. import com.appleyk.pojo.DataSourceInfo;
  36. /**
  37. *
  38. * 功能描述:动态数据源注册 启动动态数据源请在启动类中(如Start)
  39. * 添加 @Import(DynamicDataSourceRegister.class)
  40. */
  41. @Configuration
  42. @EnableTransactionManagement
  43. @EnableConfigurationProperties(DataSourceProperties.class)
  44. @MapperScan( "com.appleyk")
  45. public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
  46. private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceRegister.class);
  47. private ConversionService conversionService = new DefaultConversionService();
  48. private PropertyValues dataSourcePropertyValues;
  49. // 如配置文件中未指定数据源类型,使用该默认值
  50. private static final Object DATASOURCE_TYPE_DEFAULT = "org.apache.tomcat.jdbc.pool.DataSource";
  51. // 数据源
  52. private DataSource defaultDataSource;
  53. private Map<String, DataSource> customDataSources = new HashMap<>();
  54. @Override
  55. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  56. Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
  57. // 将主数据源添加到更多数据源中
  58. targetDataSources.put( "dataSource", defaultDataSource);
  59. DynamicDataSourceContextHolder.dataSourceIds.add( "dataSource");
  60. // 添加更多数据源
  61. targetDataSources.putAll(customDataSources);
  62. for (String key : customDataSources.keySet()) {
  63. DynamicDataSourceContextHolder.dataSourceIds.add(key);
  64. }
  65. // 创建DynamicDataSource
  66. GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
  67. beanDefinition.setBeanClass(DynamicDataSource.class);
  68. beanDefinition.setSynthetic( true);
  69. MutablePropertyValues mpv = beanDefinition.getPropertyValues();
  70. mpv.addPropertyValue( "defaultTargetDataSource", defaultDataSource);
  71. mpv.addPropertyValue( "targetDataSources", targetDataSources);
  72. registry.registerBeanDefinition( "dataSource", beanDefinition);
  73. System.err.println( "动态数据源注册成功,从数据源个数 == " + customDataSources.size());
  74. }
  75. /**
  76. * 创建DataSource
  77. *
  78. * @param type
  79. * @param driverClassName
  80. * @param url
  81. * @param username
  82. * @param password
  83. * @return
  84. */
  85. @SuppressWarnings( "unchecked")
  86. public DataSource buildDataSource(Map<String, Object> dsMap) {
  87. try {
  88. Object type = dsMap.get( "type");
  89. if (type == null)
  90. type = DATASOURCE_TYPE_DEFAULT; // 默认DataSource
  91. Class<? extends DataSource> dataSourceType;
  92. dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
  93. String driverClassName = dsMap.get( "driver-class-name").toString();
  94. String url = dsMap.get( "url").toString();
  95. String username = dsMap.get( "username").toString();
  96. String password = dsMap.get( "password").toString();
  97. DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
  98. .username(username).password(password).type(dataSourceType);
  99. return factory.build();
  100. } catch (ClassNotFoundException e) {
  101. e.printStackTrace();
  102. }
  103. return null;
  104. }
  105. /**
  106. * 加载多数据源配置
  107. */
  108. @Override
  109. public void setEnvironment(Environment env) {
  110. initDefaultDataSource(env);
  111. initCustomDataSources(env);
  112. }
  113. /**
  114. * 初始化主数据源
  115. *
  116. */
  117. private void initDefaultDataSource(Environment env) {
  118. // 读取主数据源
  119. RelaxedPropertyResolver propertyResolver = new RelaxedPropertyResolver(env, "spring.datasource.");
  120. Map<String, Object> dsMap = new HashMap<>();
  121. dsMap.put( "type", propertyResolver.getProperty( "type"));
  122. dsMap.put( "driver-class-name", propertyResolver.getProperty( "driver-class-name"));
  123. dsMap.put( "url", propertyResolver.getProperty( "url"));
  124. dsMap.put( "username", propertyResolver.getProperty( "username"));
  125. dsMap.put( "password", propertyResolver.getProperty( "password"));
  126. defaultDataSource = buildDataSource(dsMap);
  127. dataBinder(defaultDataSource, env);
  128. }
  129. /**
  130. * 为DataSource绑定更多数据
  131. *
  132. * @param dataSource
  133. * @param env
  134. */
  135. private void dataBinder(DataSource dataSource, Environment env) {
  136. RelaxedDataBinder dataBinder = new RelaxedDataBinder(dataSource);
  137. dataBinder.setConversionService(conversionService);
  138. dataBinder.setIgnoreNestedProperties( false); // false
  139. dataBinder.setIgnoreInvalidFields( false); // false
  140. dataBinder.setIgnoreUnknownFields( true); // true
  141. if (dataSourcePropertyValues == null) {
  142. Map<String, Object> rpr = new RelaxedPropertyResolver(env, "spring.datasource").getSubProperties( ".");
  143. Map<String, Object> values = new HashMap<>(rpr);
  144. // 排除已经设置的属性
  145. values.remove( "type");
  146. values.remove( "driver-class-name");
  147. values.remove( "url");
  148. values.remove( "username");
  149. values.remove( "password");
  150. dataSourcePropertyValues = new MutablePropertyValues(values);
  151. }
  152. dataBinder.bind(dataSourcePropertyValues);
  153. }
  154. /**
  155. * 初始化更多数据源
  156. *
  157. */
  158. private void initCustomDataSources(Environment env) {
  159. // 读取库表中datasource获取更多数据源
  160. Map<String, Map<String, Object>> customInfo = getCustomDataSourceInfo();
  161. for (String key : customInfo.keySet()) {
  162. Map<String, Object> dsMap = customInfo.get(key);
  163. DataSource ds = buildDataSource(dsMap);
  164. try {
  165. // 判断一下 数据源是否连接成功
  166. ds.getConnection();
  167. } catch (SQLException e) {
  168. e.printStackTrace();
  169. }
  170. customDataSources.put(key, ds);
  171. dataBinder(ds, env);
  172. }
  173. }
  174. private Map<String, Map<String, Object>> getCustomDataSourceInfo() {
  175. Map<String, Map<String, Object>> customMap = new HashMap<>();
  176. // 从主库的slave表中,读取出从库slave的连接信息
  177. String sql = "select url,username,password from slave";
  178. JdbcTemplate jdbcTemplate = new JdbcTemplate(defaultDataSource);
  179. List<DataSourceInfo> infos = jdbcTemplate.query(sql, new RowMapper<DataSourceInfo>() {
  180. @Override
  181. public DataSourceInfo mapRow(ResultSet rs, int rowNum) throws SQLException {
  182. DataSourceInfo info = new DataSourceInfo();
  183. info.setType( "com.alibaba.druid.pool.DruidDataSource");
  184. info.setDriverClassName( "com.mysql.jdbc.Driver");
  185. info.setUrl(rs.getString( "url"));
  186. info.setPassWord(rs.getString( "password"));
  187. info.setUserName(rs.getString( "username"));
  188. // 从库名称:slave -- 对应后面的数据源注解里面的name属性
  189. // 这个地方可以表slave加个字段,字段名ds,值存数据源的名称
  190. info.setDsName( "slave");
  191. return info;
  192. }
  193. });
  194. for (DataSourceInfo info : infos) {
  195. Map<String, Object> dsMap = new HashMap<>();
  196. dsMap.put( "type", info.getType());
  197. dsMap.put( "driver-class-name", info.getDriverClassName());
  198. dsMap.put( "url", info.getUrl());
  199. dsMap.put( "username", info.getUserName());
  200. dsMap.put( "password", info.getPassWord());
  201. customMap.put(info.getDsName(), dsMap);
  202. }
  203. return customMap;
  204. }
  205. @Bean
  206. public PlatformTransactionManager masterTransactionManager() {
  207. System.err.println( "masterTransactionManager=========配置主数据库的事务");
  208. return new DataSourceTransactionManager(defaultDataSource);
  209. }
  210. @Bean
  211. public PlatformTransactionManager slaveTransactionManager() {
  212. System.err.println( "slaveTransactionManager=========配置从数据库的事务");
  213. return new DataSourceTransactionManager(customDataSources.get( "slave"));
  214. }
  215. }

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


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






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


DynamicDataSourceAspect.java



    
    
  1. package com.appleyk.aop;
  2. import org.aspectj.lang.JoinPoint;
  3. import org.aspectj.lang.annotation.After;
  4. import org.aspectj.lang.annotation.Aspect;
  5. import org.aspectj.lang.annotation.Before;
  6. import org.slf4j.Logger;
  7. import org.slf4j.LoggerFactory;
  8. import org.springframework.stereotype.Component;
  9. import com.appleyk.annotation.DataSource;
  10. import com.appleyk.datasource.DynamicDataSourceContextHolder;
  11. @Aspect
  12. @Component
  13. public class DynamicDataSourceAspect {
  14. private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
  15. @Before( "@annotation(ds)")
  16. public void changeDataSource(JoinPoint point, DataSource ds) throws Throwable {
  17. String dsId = ds.name();
  18. System.err.println(dsId);
  19. if (!DynamicDataSourceContextHolder.containsDataSource(dsId)) {
  20. System.err.println( "数据源[{"+ds.name()+ "}]不存在,使用默认数据源 >"+point.getSignature());
  21. } else {
  22. System.err.println( "Use DataSource : "+ds.name()+ ">"+point.getSignature() );
  23. DynamicDataSourceContextHolder.setDataSourceType(ds.name());
  24. }
  25. }
  26. @After( "@annotation(ds)")
  27. public void restoreDataSource(JoinPoint point, DataSource ds) {
  28. System.err.println( "Revert DataSource : "+ds.name()+ " > "+point.getSignature());
  29. DynamicDataSourceContextHolder.clearDataSourceType();
  30. }
  31. }


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

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





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



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




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



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

ObjectController.java



    
    
  1. package com.appleyk.controller;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.web.bind.annotation.PostMapping;
  4. import org.springframework.web.bind.annotation.RequestBody;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. import com.appleyk.entity.A;
  8. import com.appleyk.result.ResponseMessage;
  9. import com.appleyk.result.ResponseResult;
  10. import com.appleyk.service.ObjectService;
  11. @RestController
  12. @RequestMapping( "/rest/v1.0.1/object")
  13. public class ObjectController {
  14. @Autowired
  15. private ObjectService objService;
  16. @PostMapping( "/save")
  17. public ResponseResult SaveObject(@RequestBody A a) throws Exception {
  18. if (objService.Save(a)) {
  19. return new ResponseResult(ResponseMessage.OK);
  20. }
  21. return new ResponseResult(ResponseMessage.INTERNAL_SERVER_ERROR);
  22. }
  23. }


注意: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 实现跨库的分布式事务管理

				<script>
					(function(){
						function setArticleH(btnReadmore,posi){
							var winH = $(window).height();
							var articleBox = $("div.article_content");
							var artH = articleBox.height();
							if(artH > winH*posi){
								articleBox.css({
									'height':winH*posi+'px',
									'overflow':'hidden'
								})
								btnReadmore.click(function(){
									if(typeof window.localStorage === "object" && typeof window.csdn.anonymousUserLimit === "object"){
										if(!window.csdn.anonymousUserLimit.judgment()){
											window.csdn.anonymousUserLimit.Jumplogin();
											return false;
										}else if(!currentUserName){
											window.csdn.anonymousUserLimit.updata();
										}
									}
									
									articleBox.removeAttr("style");
									$(this).parent().remove();
								})
							}else{
								btnReadmore.parent().remove();
							}
						}
						var btnReadmore = $("#btn-readmore");
						if(btnReadmore.length>0){
							if(currentUserName){
								setArticleH(btnReadmore,3);
							}else{
								setArticleH(btnReadmore,1.2);
							}
						}
					})()
				</script>
				</article>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值