动态切换数据源实现读写分离方案

最近生产环境mysql新增了从库,用于做读写分离,于是研究了配置动态数据源的方案。本文主要尝试了利用spring自带的动态选择数据源类以及Sharding-Sphere插件两种方案来实现mysql的读写分离。

一、手写动态数据源
我们查看spring的源码可知,AbstractRoutingDataSource正是spring提供的动态配置数据源的方案,可以配置实现多个业务库的切换,可以实现同一业务库的读写分离。
targetDataSource中可以添加多个数据源,而defaultTargetDataSource可以配置默认的数据源:
动态切换数据源实现读写分离方案_第1张图片
动态选择数据源的关键在于类中的一个抽象方法,determineCurrentLookupKey:
动态切换数据源实现读写分离方案_第2张图片
这个方法往往需要在代码中继承的子类中实现,确定了key,targetDataSource就可以找到对应的数据源。
在demo中,我使用和生产环境相同的技术来做测试,即:springboot+jpa+mysql。
首先,定义写库和读库的数据源:


<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>

    
    <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 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数据源,原因如下:
动态切换数据源实现读写分离方案_第3张图片

参考资料:
http://shardingjdbc.io/index_zh.html
https://blog.csdn.net/qq_31125793/article/details/51241943

你可能感兴趣的:(数据库相关)