读写分离一直都是项目的标配,之前项目的做法非常简单,直接配置两个数据源,一个只读,一个只写,只读的放到xxx.read,只写的放到xxx.write包下。Service层调用的时候根据操作选择对应的数据源。主要配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 略... </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:config/db/mybatis-configuration.xml" /> <property name="mapperLocations"> <array> <value>classpath*:xxx/write/resource/*.sql.xml</value> </array> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="xxx.write" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean> <!-- Transaction manager for a single JDBC DataSource --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
以上实现了为只写的数据源配置事务。Mybatis自动扫描对应包下的xml文件。 这样做优点还是很明显的,简单易懂。以后只要有新功能按照读写分离原则放到指定包下即可。 缺点就是在Service层涉及到读写同时进行的时候,需要调用对应的Mapper,比如:xxxReadMapper,xxxWriteMapper 的方法。 如果以后读写分离改成的数据库层处理,那么这里的代码就需要合并到一起,增加工作量。
那有没有更好的方法呢?是否可以做到自动读写分离呢? 当然是有的,而且还有很多种方式,比如通过数据库代理的方式,而不是通过代码来实现。或者还有其他开源框架。这里介绍下我的实现方式,基于AbstractRoutingDataSource。 该类通过代理的方式实现了数据源的动态分配,在使用时通过自定义的key来选择对应的数据源。它的注释是这么说明的:
1 Abstract javax.sql.DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context.
步骤1:执行db目录下的springboot.sql文件来初始化db,这里需要配置两个db,一个只读(springboot_r)一个写(springboot)。 步骤2:继承自AbstractRoutingDataSource,初始化结束时自动扫描容器内的数据源,实现自动代理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 @Component("dynamicDataSource") @Primary @ConfigurationProperties(prefix = "dynamicDatasource") public static class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware { public static final Map<String, String> DATASOURCE_STRATEGY = new HashMap<>(); private Map<String, String> strategy = new HashMap<>(); private ApplicationContext applicationContext; private String defaultDataSource; @Override protected Object determineCurrentLookupKey() { return DynamicDataSourceHolder.getDataSource(); } @Override protected Object resolveSpecifiedLookupKey(Object lookupKey) { return super.resolveSpecifiedLookupKey(lookupKey); } @Override public void afterPropertiesSet() { Map<String, DataSource> dataSources = applicationContext.getBeansOfType(DataSource.class); if (dataSources.size() == 0) { throw new IllegalStateException("Datasource can not found!!!"); } // exclude current datasource Map<Object, Object> targetDataSource = excludeCurrentDataSource(dataSources); setTargetDataSources(targetDataSource); // 多数据源方法设置 Iterator<String> it = strategy.keySet().iterator(); while (it.hasNext()) { String key = it.next(); String[] values = strategy.get(key).split(","); for (String v : values) { if (StringUtils.isNotBlank(v)) { DATASOURCE_STRATEGY.put(v, key); } } } // 默认数据源设置 setDefaultTargetDataSource(targetDataSource.get(getDefaultDataSource())); super.afterPropertiesSet(); } /*** * exclude current Datasource * * @param dataSources * @return */ private Map<Object, Object> excludeCurrentDataSource(Map<String, DataSource> dataSources) { Map<Object, Object> targetDataSource = new HashMap<>(); Iterator<String> keys = dataSources.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); if (!(dataSources.get(key) instanceof DynamicDataSource)) { targetDataSource.put(key, dataSources.get(key)); } } return targetDataSource; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public Map<String, String> getStrategy() { return strategy; } public void setStrategy(Map<String, String> strategy) { this.strategy = strategy; } public String getDefaultDataSource() { return defaultDataSource; } public void setDefaultDataSource(String defaultDataSource) { this.defaultDataSource = defaultDataSource; } }
步骤3:配置读和写的数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @ConfigurationProperties(prefix = "db.mybatis.jdbc") @Bean(destroyMethod = "close", name = "write") public DataSource dataSourceWrite() { log.info("*************************dataSource***********************"); BasicDataSource dataSource = new BasicDataSource(); dataSource.setRemoveAbandoned(true); dataSource.setTestWhileIdle(true); dataSource.setTimeBetweenEvictionRunsMillis(30000); dataSource.setNumTestsPerEvictionRun(30); dataSource.setMinEvictableIdleTimeMillis(1800000); return dataSource; } @ConfigurationProperties(prefix = "db.mybatis2.jdbc") @Bean(destroyMethod = "close", name = "read") public DataSource dataSourceRead() { log.info("*************************dataSource***********************"); BasicDataSource dataSource = new BasicDataSource(); dataSource.setRemoveAbandoned(true); dataSource.setTestWhileIdle(true); dataSource.setTimeBetweenEvictionRunsMillis(30000); dataSource.setNumTestsPerEvictionRun(30); dataSource.setMinEvictableIdleTimeMillis(1800000); return dataSource; }
步骤4:为动态数据源配置读写分离策略,这里使用的是最简单的前缀规则,如果有需要可以自行改成正则表达式的方式,以下配置定义了get,find,select开头的方法都使用read数据源
1 2 3 dynamicDatasource.strategy.read=get,find,select dynamicDatasource.strategy.write=insert,update,delete,login dynamicDatasource.defaultDataSource=write
步骤5:单元测试,在test包下DynamicDataSourceTest类中有两个方法,一个测试只读一个测试写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testLogin() throws Exception { User user = new User(); user.setUsername("11111111111"); user.setPassword("123456"); User loginUser = userService.login(user); System.out.println("登录结果:" + loginUser); } @Test public void testFindUser() throws Exception { User loginUser = userService.findUserByToken("xxx"); System.out.println("查询用户结果:" + loginUser); }
执行testLogin单元测试可以看出这里的操作用的是写的数据源
1 ooo Using Connection [jdbc:mysql://localhost/springboot?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost, MySQL Connector Java]
执行testFindUser可以看出这里用的是读的数据源
1 ooo Using Connection [jdbc:mysql://localhost/springboot_r?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost, MySQL Connector Java]
这种方式优点:不需要再像之前一样对读写操作分离了,都可以统一到一个Mapper上,代码可以统一到一个包下。程序员甚至都不需要意识到数据库的读写分离。以后替换成db层处理也是非常方便的。
注意点: 因为事务和动态数据源切换都是基于AOP的,所以顺序非常重要。动态切换要在事务之前,如果发现无法动态切换数据源那么可以看下他们之间的顺序。
以上代码已提交至SpringBootLearning的DynamicDataSource工程。
说明: com.cml.springboot.framework.db 动态数据源配置包 com.cml.springboot.framework.mybatis mybatis配置包,配置了mybatis规则和读写数据源
SpringBootLearning是对springboot学习与研究项目,是根据实际项目的形式对进行配置与处理,欢迎star与fork。 [oschina 地址]http://git.oschina.net/cmlbeliever/SpringBootLearning [github 地址]https://github.com/cmlbeliever/SpringBootLearning