在当今数据驱动的时代,随着业务的快速发展和数据量的持续增长,数据库的性能和稳定性成为了众多开发者关注的焦点。读写分离作为一种提升数据库系统并发处理能力和数据读取性能的有效策略,正被广泛应用于各类应用场景中。特别是在那些读多写少的业务场景下,读写分离的优势更是得以充分彰显。

一、读写分离:原理与应用场景深度剖析

1、什么是读写分离?

读写分离是一种将数据库的读操作和写操作进行分离处理的数据库访问策略。简单来说,就是通过设置一个或多个主数据库来专门负责处理写操作(如插入、更新和删除数据),同时配置一个或多个从数据库来承担所有的读操作(如数据查询)。这样做的好处在于,能够有效分散数据库的负载压力,避免读写操作相互干扰,从而显著提升系统的整体性能和响应速度。

2、读写分离的使用场景

在实际应用中,读写分离主要适用于读多写少的业务场景。例如,电商平台的商品展示页面,用户频繁地浏览商品信息,但对商品信息的修改操作相对较少;新闻资讯类网站,大量用户在浏览新闻内容,而新闻的发布和编辑操作则相对不那么频繁。在这些场景下,采用读写分离策略可以极大地提高系统的并发处理能力,确保用户能够快速、流畅地获取所需数据。
接下来,我们将深入探讨在应用程序中通过不同方式实现数据库读写分离的具体方法和实战技巧。

二、实战攻略:应用程序中实现读写分离的三种主流方式

方式一:AbstractRoutingDataSource + mybatis 拦截器实现读写分离

这种方式通过自定义数据源路由和 mybatis 拦截器,实现根据 SQL 语句类型自动切换主从数据源,从而达到读写分离的效果。

1、引入依赖

在项目的pom.xml文件中引入mybatis-plus-boot-starter依赖:

    <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.verison}</version>
        </dependency>
  1. 配置数据源

创建主数据库和从数据库的数据源配置类,通过@ConfigurationProperties注解读取配置文件中的数据源信息:

    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    @ConditionalOnProperty(name = "spring.datasource.master.jdbc-url")
    public DataSource masterDataSource(){
      return DataSourceBuilder.create().build();
    }

    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    @ConditionalOnProperty(name = "spring.datasource.slave.jdbc-url")
    public DataSource slaveDataSource(){
        return DataSourceBuilder.create().build();
    }
3、定义数据源枚举

创建主从数据源枚举类DataSourceTypeEnum,用于标识主库和从库:

public enum DataSourceTypeEnum {
    MASTER,
    SLAVE
}
  1. 线程级数据源上下文管理

创建DataSourceContextHolder类,用于存储当前线程的数据源类型,实现线程级别的数据源隔离:

public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new TransmittableThreadLocal<>();

    public static void setDataSourceType(DataSourceTypeEnum dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static DataSourceTypeEnum getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}
  1. 动态路由数据源

创建DynamicRoutingDataSource类,继承自AbstractRoutingDataSource,实现动态数据源路由:

@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        if(DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) {
            log.info("DynamicRoutingDataSource 切换数据源到从库");
            return DataSourceTypeEnum.SLAVE;
        }
        log.info("DynamicRoutingDataSource 切换数据源到主库");
        return DataSourceTypeEnum.MASTER;
    }
}
6、配置动态数据源

创建动态数据源配置类,将主数据库和从数据库的数据源添加到动态数据源中,并设置默认数据源:

    @Bean("dataSource")
    @Primary
    public DataSource dynamicDataSource( DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DataSourceTypeEnum.SLAVE, slaveDataSource);

        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
7、创建mybatis拦截器

创建DataSourceSwitchInterceptor类,实现Interceptor接口,通过拦截SQL执行,根据SQL类型切换数据源:

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
})
public class DataSourceSwitchInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];

        if(SqlCommandType.SELECT == ms.getSqlCommandType()){
            DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE);
        }else{
            DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
        }

        try {
            // 执行 SQL
            return invocation.proceed();
        } finally {
            // 恢复数据源  考虑到写入后可能会反查,后续都走主库
             DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
        }
    }
}
8、配置数据源连接信息

在application.yml文件中配置主数据库和从数据库的连接信息:

spring:
  main:
    allow-bean-definition-overriding: true
  datasource:
    dynamic:
      enabled: true
    #master
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: ${DATASOURCE_URL:jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true}
      username: ${DATASOURCE_USERNAME:root}
      password: ${DATASOURCE_PWD:123456}
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        initial-size: 5 #连接池初始化大小
        min-idle: 10 #最小空闲连接数
        max-active: 20 #最大连接数
        maxWait: 60000 # 配置获取连接等待超时的时间
    #slave
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: ${SLAVE_DATASOURCE_URL:jdbc:mysql://localhost:3306/demo_read?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
      username: ${SLAVE_DATASOURCE_USERNAME:root}
      password: ${SLAVE_DATASOURCE_PWD:123456}
      type: com.alibaba.druid.pool.DruidDataSource
      druid:
        initial-size: 5 #连接池初始化大小
        min-idle: 10 #最小空闲连接数
        max-active: 20 #最大连接数
        maxWait: 60000 # 配置获取连接等待超时的时间
    druid:
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username:
        login-password:
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  autoconfigure:
    exclude:
      - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
        
#mybatis-plus相关配置
mybatis-plus:
  mapper-locations: mybatis/*/*.xml
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'
    log-impl: ${SQL_LOG:org.apache.ibatis.logging.stdout.StdOutImpl}
  global-config:
    banner: false

注: 排除druid自动装配,否则会优先加载druid自动装配,导致项目启动报错

9、测试验证

为了演示方便,本示例通过多数据源来模拟读写分离效果。可以通过浏览器访问相应的接口,验证主从切换是否生效。例如,访问查询接口时,查看日志和数据库操作记录,确认是否命中从库数据;访问写入接口时,确认数据是否成功写入主库,且从库数据未发生变化。

主从搭建可以参考https://blog.csdn.net/weixin_43735086/article/details/141127282

主库示例表如下

从库的示例表如下

示例控制器如下


@RestController
@RequestMapping("user")
@RequiredArgsConstructor
@Slf4j
public class UserController {

    private final UserService userService;
    @GetMapping("list")
    public List<User> list(){
        try {
            return userService.list();
        } catch (Exception e) {

        }
        return defaultUsers();

    }


    @GetMapping("save")
    public String save(){
        User user = buildUser();
        return userService.save(user) ? "save success" : "save fail";

    }
验证主从切换效果

浏览器访问http://localhost:8082/user/list

可以看到此时命中是从库数据

浏览器访问http://localhost:8082/user/save

此时主库新增了一条记录

从库数据没有发生变化

综上可以发现读写切换生效

方式二:利用 mybatis-plus 提供的多数据源功能实现读写分离

mybatis-plus提供了强大的多数据源支持,通过简单的配置和注解即可实现读写分离。

1、引入依赖

在项目的pom.xml文件中引入dynamic-datasource-spring-boot-starter依赖:

       <dependency>
                    <groupId>com.baomidou</groupId>
                    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
                    <version>${mybatis-plus.verison}</version>
                </dependency>
2、配置数据源

在application.yml文件中配置主数据库和从数据库的连接信息:

spring:
    datasource:
        dynamic:
            #指定一个主数据源,primary表示是主数据源也可以认为是默认数据源,如果不加注解DS(“数据源名字”)则会去主数据源中查询
            primary: master
            strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
            datasource:
                #master
                master:
                    driver-class-name: com.mysql.cj.jdbc.Driver
                    url: ${DATASOURCE_URL:jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true}
                    username: ${DATASOURCE_USERNAME:root}
                    password: ${DATASOURCE_PWD:123456}
                    type: com.alibaba.druid.pool.DruidDataSource
                    druid:
                        initial-size: 5 #连接池初始化大小
                        min-idle: 10 #最小空闲连接数
                        max-active: 20 #最大连接数
                        maxWait: 60000 # 配置获取连接等待超时的时间
                #slave
                slave:
                    driver-class-name: com.mysql.cj.jdbc.Driver
                    url: ${SLAVE_DATASOURCE_URL:jdbc:mysql://localhost:3306/demo_read?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
                    username: ${SLAVE_DATASOURCE_USERNAME:root}
                    password: ${SLAVE_DATASOURCE_PWD:123456}
                    type: com.alibaba.druid.pool.DruidDataSource
                    druid:
                        initial-size: 5 #连接池初始化大小
                        min-idle: 10 #最小空闲连接数
                        max-active: 20 #最大连接数
                        maxWait: 60000 # 配置获取连接等待超时的时间

        druid:
            # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
            timeBetweenEvictionRunsMillis: 60000
            # 配置一个连接在池中最小生存的时间,单位是毫秒
            minEvictableIdleTimeMillis: 300000
            # 配置一个连接在池中最大生存的时间,单位是毫秒
            maxEvictableIdleTimeMillis: 900000
            # 配置检测连接是否有效
            validationQuery: SELECT 1 FROM DUAL
            testWhileIdle: true
            testOnBorrow: false
            testOnReturn: false
            webStatFilter:
                enabled: true
            statViewServlet:
                enabled: true
                # 设置白名单,不填则允许所有访问
                allow:
                url-pattern: /druid/*
                # 控制台管理用户名和密码
                login-username:
                login-password:
            filter:
                stat:
                    enabled: true
                    # 慢SQL记录
                    log-slow-sql: true
                    slow-sql-millis: 1000
                    merge-sql: true
                wall:
                    config:
                        multi-statement-allow: true
    autoconfigure:
        # 排除druid自动装配,否则会优先加载druid自动装配,导致项目启动报错
      exclude:
        - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure



#mybatis-plus相关配置
mybatis-plus:
    mapper-locations: mybatis/*/*.xml
    configuration:
        map-underscore-to-camel-case: true
        cache-enabled: false
        call-setters-on-nulls: true
        jdbc-type-for-null: 'null'
        log-impl: ${SQL_LOG:org.apache.ibatis.logging.stdout.StdOutImpl}
    global-config:
        banner: false
  1. 使用DS注解切换数据源

在需要切换数据源的方法上使用@DS注解,指定要使用的数据源:

  @GetMapping("list")
    @DS("slave")
    public List<User> list(){
        try {
            return userService.list();
        } catch (Exception e) {

        }
        return defaultUsers();

    }


    @GetMapping("save")
    @DS("master")
    public String save(){
        User user = buildUser();
        return userService.save(user) ? "save success" : "save fail";

    }

通过以上配置就可以实现主从切换效果。更详细的用法可以查看官网
https://www.kancloud.cn/tracy5546/dynamic-datasource/2264611

方法三:利用 shardingsphere 实现的读写分离功能

shardingsphere是一款功能强大的分布式数据库中间件,它提供了简单易用的读写分离功能,能够轻松实现数据库的读写分离。

1、引入依赖

在项目的pom.xml文件中引入sharding-jdbc-spring-boot-starter依赖:

  <dependency>
                    <groupId>org.apache.shardingsphere</groupId>
                    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
                    <version>${sharding-sphere.version}</version>
                </dependency>
2、配置数据源

在application.yml文件中配置主数据库和从数据库的连接信息:

spring:
  shardingsphere:
    enabled: true
    datasource:
      names: master,slave
      #master
      master:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: ${DATASOURCE_URL:jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true}
        username: ${DATASOURCE_USERNAME:root}
        password: ${DATASOURCE_PWD:123456}
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          initial-size: 5 #连接池初始化大小
          min-idle: 10 #最小空闲连接数
          max-active: 20 #最大连接数
          maxWait: 60000 # 配置获取连接等待超时的时间
      #slave
      slave:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: ${SLAVE_DATASOURCE_URL:jdbc:mysql://localhost:3306/demo_read?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
        username: ${SLAVE_DATASOURCE_USERNAME:root}
        password: ${SLAVE_DATASOURCE_PWD:123456}
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
          initial-size: 5 #连接池初始化大小
          min-idle: 10 #最小空闲连接数
          max-active: 20 #最大连接数
          maxWait: 60000 # 配置获取连接等待超时的时间
    masterslave:
      name: ms
      master-data-source-name: master
      slave-data-source-names:
        - slave
      props:
        sql:
         show: true

  datasource:
    druid:
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      webStatFilter:
        enabled: true
      statViewServlet:
        enabled: true
        # 设置白名单,不填则允许所有访问
        allow:
        url-pattern: /druid/*
        # 控制台管理用户名和密码
        login-username:
        login-password:
      filter:
        stat:
          enabled: true
          # 慢SQL记录
          log-slow-sql: true
          slow-sql-millis: 1000
          merge-sql: true
        wall:
          config:
            multi-statement-allow: true
  autoconfigure:
    # 排除druid自动装配,否则会优先加载druid自动装配,导致项目启动报错
    exclude:
      - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
      
         
#mybatis-plus相关配置
mybatis-plus:
  mapper-locations: mybatis/*/*.xml
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: false
    call-setters-on-nulls: true
    jdbc-type-for-null: 'null'
    log-impl: ${SQL_LOG:org.apache.ibatis.logging.stdout.StdOutImpl}
  global-config:
    banner: false

通过以上配置即可达到读写分离的效果,注不同版本的shardingsphere读写分离的配置内容存在差异,本示例用的版本是4.1.1。更多详细配置可以查看官网https://shardingsphere.apache.org/document/current/cn/features/readwrite-splitting/

总结

本文阐述在应用层面上通过3种方式实现读写分离,其中通过shardingsphere来实现读写分离是比较推荐的做法,一是实现简单,二是对代码无侵入性。其次除了上述方式,也可以通过数据库代理中间件来实现读写分离,比如mycat。

最后如果数据库是使用mysql,可以仅依赖mysql驱动包就可以实现读写分离,具体可以查看官方链接
https://dev.mysql.com/doc/connector-j/en/connector-j-source-replica-replication-connection.html

demo链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-db-readwrite-splitting


linyb极客之路
336 声望193 粉丝