最近生产环境mysql新增了从库,用于做读写分离,于是研究了配置动态数据源的方案。本文主要尝试了利用spring自带的动态选择数据源类以及Sharding-Sphere插件两种方案来实现mysql的读写分离。
一、手写动态数据源
我们查看spring的源码可知,AbstractRoutingDataSource正是spring提供的动态配置数据源的方案,可以配置实现多个业务库的切换,可以实现同一业务库的读写分离。
targetDataSource中可以添加多个数据源,而defaultTargetDataSource可以配置默认的数据源:
动态选择数据源的关键在于类中的一个抽象方法,determineCurrentLookupKey:
这个方法往往需要在代码中继承的子类中实现,确定了key,targetDataSource就可以找到对应的数据源。
在demo中,我使用和生产环境相同的技术来做测试,即:springboot+jpa+mysql。
首先,定义写库和读库的数据源:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.dongnao.dynamic"></context:component-scan>
<!--这里多数据源,springBoot启动时需要指定一个默认的数据源,所以需要加primary="true",否则会出现数据源bean匹配失败错误-->
<bean id="writeDataSource" name="writeDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" primary="true">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://"/>
<property name="username" value=""/>
<property name="password" value=""/>
</bean>
<bean id="readDataSource" name="readDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" >
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://"/>
<property name="username" value=""/>
<property name="password" value=""/>
</bean>
<!--动态选择数据源-->
<bean id="dataSource" class="com.dongnao.dynamic.config.DynamicDataSource" >
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="write" value-ref="writeDataSource"/>
<entry key="read" value-ref="readDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" destroy-method="destroy" >
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"/>
<property name="showSql" value="true"/>
</bean>
</property>
<property name="packagesToScan" value="com.dongnao.dynamic.entity"/>
<property name="jpaPropertyMap">
<map>
<entry key="javax.persistence.schema-generation.database.action" value="none"/>
</map>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
其次,定义targetDataSource需要的key和相关读写方法(出于线程安全的考虑使用threadlocal):
public enum DataSourceType {
read, write;
}
public class DataSourceContextHolder {
private static final Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);
private static ThreadLocal<DataSourceType> local = new ThreadLocal<>();
public static void setDbType(DataSourceType type) {
if(type == null) {
logger.debug("====== DataSource Type is null, set the default value! =======");
local.set(DataSourceType.write);
}
local.set(type);
}
public static DataSourceType getDbType() {
if (local.get() != null) {
logger.debug("=====Threadlocal数据源的类型是:======"+local.get().name());
}
else {
logger.debug("=====Threadlocal数据源的类型是:空======");
}
return local.get() == null ? DataSourceType.write : local.get();
}
public static void clearDbType() {
local.remove();
}
}
再次,实现自定义类,继承动态数据源的抽象类:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
DataSourceType dataSourceType = DataSourceContextHolder.getDbType();
if (dataSourceType == null || dataSourceType == DataSourceType.write) {
return DataSourceType.write.name();
}
return DataSourceType.read.name();
}
public static void read() {
DataSourceContextHolder.setDbType(DataSourceType.read);
}
public static void write() {
DataSourceContextHolder.setDbType(DataSourceType.write);
}
}
然后,为了在业务层service中方便使用,需要定义两个注解,区分读写service的切面:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Master {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Slave {
}
@Aspect
@Component
public class DataSourceAOP {
Logger log = LoggerFactory.getLogger(DataSourceAOP.class);
@Before("(@annotation(com.dongnao.dynamic.config.Slave) " +
"|| execution(public * com.dongnao.dynamic.service..*.find*(..)) " +
"|| execution(public * com.dongnao.dynamic.service..*.get*(..))) " +
"&& !@annotation(com.dongnao.dynamic.config.Master)")
public void setReadDataSourceType() {
DynamicDataSource.read();
log.info("dataSource 切换到:slave");
}
@Before("(@annotation(com.dongnao.dynamic.config.Master) " +
"|| execution(public * com.dongnao.dynamic.service..*.insert*(..)) " +
"|| execution(public * com.dongnao.dynamic.service..*.update*(..)) " +
"|| execution(public * com.dongnao.dynamic.service..*.save*(..)) " +
"|| execution(public * com.dongnao.dynamic.service..*.add*(..))) " +
"|| execution(public * com.dongnao.dynamic.service..*.delete*(..))) " +
"&& !@annotation(com.car.union.config.Slave)")
public void setWriteDataSourceType() {
DynamicDataSource.write();
log.info("dataSource 切换到: master");
}
}
最后,我们就可以做测试啦,需要强调的是,如果在service层加了事务,访问切面的方法和访问determineDataSource方法的顺序是不一致的,使用了事务注解,是要先确定数据源的,也就是先走determineDataSource方法,也就是会使用defaultDataSource设置的默认数据源,所以一般只在增改删业务中加@Transactional注解:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Slave
public User findUserById(Long id) {
User user = userRepository.findUserById(id);
return user;
}
@Master
@Transactional
public User saveUser(User user) {
User u = userRepository.save(user);
return u;
}
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:application-config.xml")
public class DynamicApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
User user = userService.findUserById(1L);
// User auser = new User();
// auser.setUserName("yangyang");
// auser.setUserAge("80");
// auser.setUserAddress("beijing");
//
// User user = userService.saveUser(auser);
System.out.println("UserId:"+user.getId()+" UserName:"+user.getUserName()+" UserAge:"+user.getUserAge()+" UserAddress:"+user.getUserAddress());
}
}
二、使用sharding-sphere插件
Sharding-Sphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款产品组成。3款产品提供标准化的数据分片、读写分离、柔性事务和数据治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。
我们这里主要使用sharding-jdbc来实现读写分离,当然框架还支持分库分表,我们也会根据业务需要增加分表功能的使用。
sharding-jdbc的使用十分便捷,基本上没有代码侵入,直接在springboot中进行配置即可。
application.properties:
sharding.jdbc.datasource.names=ds_master,ds_slave0,ds_slave1
sharding.jdbc.datasource.ds_master.type=org.apache.commons.dbcp2.BasicDataSource
sharding.jdbc.datasource.ds_master.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_master.url=jdbc:mysql://
sharding.jdbc.datasource.ds_master.username=
sharding.jdbc.datasource.ds_master.password=
sharding.jdbc.datasource.ds_slave0.type=org.apache.commons.dbcp2.BasicDataSource
sharding.jdbc.datasource.ds-slave0.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_slave0.url=jdbc:mysql://
sharding.jdbc.datasource.ds_slave0.username=
sharding.jdbc.datasource.ds_slave0.password=
sharding.jdbc.datasource.ds_slave1.type=org.apache.commons.dbcp2.BasicDataSource
sharding.jdbc.datasource.ds_slave1.driver-class-name=com.mysql.jdbc.Driver
sharding.jdbc.datasource.ds_slave1.url=jdbc:mysql://
sharding.jdbc.datasource.ds_slave1.username=
sharding.jdbc.datasource.ds_slave1.password=
sharding.jdbc.config.masterslave.name=ds_ms
sharding.jdbc.config.masterslave.master-data-source-name=ds_master
sharding.jdbc.config.masterslave.slave-data-source-names=ds_slave0,ds_slave1
sharding.jdbc.config.masterslave.load-balance-algorithm-type=round_robin
spring.jpa.properties.hibernate.hbm2ddl.auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.properties.hibernate.id.new_generator_mappings=false
spring.jpa.properties.hibernate.format_sql=true
这里强调一下,我在业务中,使用的是HikariDataSource数据源,原因如下:
参考资料:
http://shardingjdbc.io/index_zh.html
https://blog.csdn.net/qq_31125793/article/details/51241943