阅读 78

数据源读写分离1 - 使用AOP的方式实现 まんが,マンガ

springboot2.x+mybatis+mysql+HikariCP为例,记录下自己的开发记录。

实现思路

读写分离有四种实现方式。 我这里使用的是AOP切面实现的。目前实现的是一个主(Master),两个从(Slave)。废话少说,讲下我的思路。

  • 定义四个数据源,为什么是四个呢? Master*1 ,Slave*2,统一的数据源*1(一下称为DynamicDatasource,这个数据源是其他三个数据源的总和,可以执行它使用哪个数据源(Master,Slave1,Slave2)进行操作).
  • 定义一个DataSourceHolder,存放的是三个数据源(Master,Slave1,Slave2),可以按照条件去获取指定类型的数据源之一。
  • DynamicDataSource替换掉SpringBoot默认提供的数据源。
  • 在方法上使用注解或者使用某一类方法调用前指定使用的数据源。

什么是读写分离

将数据库分为主从库,一个主库用于写数据,多个从库完成读数据的操作。主库之间通过某种机制进行数据的同步,是一种常见的数据库架构。

数据库读写分离架构解决什么问题?

读写分离是用来解决数据库的读写瓶颈的。

在互联网的应用场景中,常常数据量大,并发量高,高可用要求高,一致性要求高,需要使用读写分离架构。 需要注意如下问题:

  • 数据库连接池要机型区分,哪些是读连接池,哪些是写连接池,研发的难度会增加。
  • 为了保证高可用,读连接池需能够实现故障自动转移。
  • 主从的一致性需要考虑。

其他减轻数据库压力的方式

  • 缓存。 这里就不再过多介绍了。关于缓存,也是比较大的一方面,后面我会单独以一篇文章介绍自己的理解吧。

  • 数据库水平切分。
    数据库水平切分,也是一种常见的数据库架构,是一种通过算法,将数据库进行分割的架构。一个水平切分集群中的每个数据库,通常称为一个“分片”。每一个分片中的数据没有重合,所有分片中的数据并集组成全部数据。
    水平切分架构解决什么问题呢?
    大部分的互联网业务,数据量都非常大,单库容量最容易成为瓶颈,当单库的容量成为了瓶颈,我们希望提高数据库的写性能,降低单库容量的话,就可以采用水平切分了。

上代码

  • pom.xml中加上如下内容。
<!--- mybatis -->
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.1</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- HikariCP 数据库连接池-->
<!-- https://mvnrepository.com/artifact/com.zaxxer/HikariCP -->
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.3.0</version>
</dependency>
复制代码
  • 我们首先定义好三个特定的数据源Master*1,Slave*2
public enum DBTypeEnum {
    /**
     * 主数据源
     */
    Master,
    /**
     * 从数据源1
     */
    Slave1,
    /**
     * 从数据源2
     */
    Slave2
}
复制代码
  • 然后,设置DataSourceHolder。这样我们就定义好了数据源的调用的模型了。
@Slf4j
public class DBContextHolder {

    private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();

    private static final AtomicInteger counter = new AtomicInteger(-1);

    public static void set(DBTypeEnum dbType) {
        contextHolder.set(dbType);
    }

    public static DBTypeEnum get() {
        return contextHolder.get();
    }

    public static void master() {
        set(DBTypeEnum.Master);
        log.debug("使用的数据源是: master");
    }

    public static void slave() {
        int index = counter.getAndIncrement() % 2;
        if (counter.get() > 99) {
            counter.set(-1);
        }
        if (index == 0) {
            set(DBTypeEnum.Slave1);
            log.debug("使用的数据源是: slave1");
        } else {
            set(DBTypeEnum.Slave2);
            log.debug("使用的数据源是: slave2");
        }
    }
}
复制代码
  • 设置动态数据源,可以指定上面的任何一种数据源去真正的操作数据库。
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource  extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.get();
    }
}
复制代码
  • 创建四种数据源的实例,ConfigurationProperties注解中的配置项是在application.propreties中配置的。
@Configuration
public class DynamicDataSourceConfig {


    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public HikariDataSource masterDataSource() {
        return new HikariDataSource();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public HikariDataSource slave1DataSource() {
        return new HikariDataSource();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.slave2")
    public HikariDataSource slave2DataSource() {
        return new HikariDataSource();
    }

    @Bean
    public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slave1DataSource") DataSource slave1DataSource,
                                        @Qualifier("slave2DataSource") DataSource slave2DataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(3);
        // 设置所有的数据源
        targetDataSources.put(DBTypeEnum.Master, masterDataSource);
        targetDataSources.put(DBTypeEnum.Slave1, slave1DataSource);
        targetDataSources.put(DBTypeEnum.Slave2, slave2DataSource);

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 将写库设置为默认的数据源
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }   
}
复制代码
  • 指定我们使用的数据源配置,url,username,password等。
spring.datasource.master.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
spring.datasource.master.username=root
spring.datasource.master.password=123456
#spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.slave1.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
spring.datasource.slave1.username=root
spring.datasource.slave1.password=123456
#spring.datasource.slave1.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.slave2.jdbc-url=jdbc:mysql://10.1.14.177:3306/dor_human?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
spring.datasource.slave2.username=root
spring.datasource.slave2.password=123456
#spring.datasource.slave2.driver-class-name=com.mysql.jdbc.Driver
复制代码
  • 这是我们要替换掉springboot默认的数据源
@EnableTransactionManagement
@Configuration
@Slf4j
public class MyBatisConfig {

    @Autowired
    private DataSource dynamicDataSource;

    /**
     * SqlSessionFactory
     *
     * @return SqlSessionFactory
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dynamicDataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.fxb.doraemon.human.entity");
        sqlSessionFactoryBean.setConfigLocation(
                new PathMatchingResourcePatternResolver().getResource("classpath:mapper/mybatis-config.xml"));
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
        log.debug("初始化SqlSessionFactory");
        return sqlSessionFactoryBean.getObject();
    }

    /**
     * 事务管理器
     *
     * @return PlatformTransactionManager
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

复制代码

好了,这样我们的读写分离的准备工作就做好了,接下来要做的就是使用了,这里我们使用AOP的方式,来规定什么时候使用什么数据源。

@Aspect
@Component
@Slf4j
public class DataSourceAop {

    /**
     * 织入读库数据源
     */
    @Pointcut("execution(public * com.fxb.doraemon.human.service..*.get*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.select*(..))")
    public void readPointcut() {
    }

    /***
     * @1:这里有点小问题.
     */
    @Pointcut("@annotation(com.fxb.doraemon.human.annotation.Master) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.save*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.insert*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.update*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.edit*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.delete*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.del*(..)) " +
            "|| execution(public * com.fxb.doraemon.human.service..*.remove*(..)) ")
    public void writePointcut() {
    }

    @Before("readPointcut()")
    public void read() {
        DBContextHolder.slave();
    }

    @Before("writePointcut()")
    public void write() {
        DBContextHolder.master();
    }
}
复制代码
  • 最后,最重要的一步,不然你会发现,aop怎么都切入不了。那是因为没有启动织入代理方式。在你的启动类上加上下面的这个注解。
@EnableAspectJAutoProxy
复制代码

验证一下我们的结果:

保存用户使用的数据源是master.png
查询用户使用的数据源是slave1.png
查询用户使用的数据源是slave2.png

好了,这样我们的数据分离就配置好了。

最后

下一篇《数据库读写分离的坑,你已经在坑了!》文章,我们来解释一下这个@1的这个问题。

如果想了解最新动态,欢迎关注公众号: 方家小白. 欢迎一起交流学习。

期待你的关注

关注下面的标签,发现更多相似文章
评论