方式1:Apache ShardingSphere 实现读写分离(推荐)
简介
Apache ShardingSphere(前身为 Sharding-JDBC)。ShardingSphere 提供了多种部署模式,其中 ShardingSphere-JDBC 是以 JDBC 驱动的方式嵌入到应用程序中,对应用透明,就像使用普通的 JDBC 连接一样。它通过解析 SQL 语句,自动将读操作路由到从库,写操作路由到主库。
核心思想
配置多个数据源
在 Spring Boot 配置中定义主库和从库的数据源。
规则配置
配置读写分离规则,指定哪个数据源是主库,哪些是从库。
SQL解析和路由
ShardingSphere 在底层拦截 JDBC 操作,解析 SQL 语句(SELECT 语句通常路由到读库,INSERT, UPDATE, DELETE 语句路由到写库),然后将请求转发到正确的数据源。
事务处理
ShardingSphere 会自动处理读写分离下的事务,确保事务的原子性。
代码实现
引入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
   | <dependencies>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-data-jpa</artifactId>     </dependency>     <dependency>         <groupId>mysql</groupId>         <artifactId>mysql-connector-java</artifactId>         <scope>runtime</scope>     </dependency>
      <dependency>         <groupId>org.apache.shardingsphere</groupId>         <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>         <version>5.3.2</version> </dependency>     <dependency>         <groupId>com.zaxxer</groupId>         <artifactId>HikariCP</artifactId>     </dependency> </dependencies>
   | 
 
application.yml配置
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
   | 
  spring:   main:     allow-bean-definition-overriding: true    datasource:     type: com.zaxxer.hikari.HikariDataSource 
 
  shardingsphere:   datasource:     names: master,slave1 
      master:        jdbcUrl: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC       username: root       password: your_master_password       driverClassName: com.mysql.cj.jdbc.Driver
      slave1:        jdbcUrl: jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=UTC       username: root       password: your_slave_password       driverClassName: com.mysql.cj.jdbc.Driver
    rules:     - !READ_WRITE_SPLITTING        dataSources:                  read-write-splitting-datasource:           writeDataSourceName: master            readDataSourceNames:              - slave1           loadBalancer:              type: ROUND_ROBIN 
 
  spring.jpa:   hibernate:     ddl-auto: update   show-sql: true   properties:     hibernate:       dialect: org.hibernate.dialect.MySQL8Dialect   open-in-view: true 
 
  | 
 
配置说明:
spring.datasource.type: 指定使用 HikariCP 连接池。 
shardingsphere.datasource.names: 列出所有物理数据源的名称(master, slave1)。 
shardingsphere.datasource.master/slave1: 分别配置主库和从库的连接信息。 
shardingsphere.rules: 这是 ShardingSphere 的核心配置。
!READ_WRITE_SPLITTING: 这是 YAML 标签,表示这是一个读写分离规则。 
dataSources.read-write-splitting-datasource: 定义一个逻辑数据源,你的 Spring Boot 应用将通过这个逻辑数据源名称来操作数据库。 
writeDataSourceName: master: 指定哪个物理数据源作为写库。 
readDataSourceNames: - slave1: 指定哪些物理数据源作为读库,可以配置多个。 
loadBalancer.type: 定义读请求的负载均衡策略。ROUND_ROBIN 是轮询,RANDOM 是随机,WEIGHT 允许你为每个从库配置权重。 
 
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 28 29 30 31 32 33 34
   | package com.example.demo.service;
  import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
  import java.util.List;
  @Service public class UserService {
      @Autowired     private UserRepository userRepository;
           @Transactional     public User saveUser(User user) {         return userRepository.save(user);     }
           @Transactional(readOnly = true)     public List<User> getAllUsers() {         return userRepository.findAll();     }
           @Transactional(readOnly = true)     public User getUserById(Long id) {         return userRepository.findById(id).orElse(null);     } }
   | 
 
ShardingSphere 的优势
- 对业务代码零侵入:这是 ShardingSphere 最大的优势。你的 Service、DAO 层代码无需任何改动,就像在使用一个普通的数据源一样。
 
- SQL 自动解析和路由:ShardingSphere 会自动解析 SQL 语句,判断是读操作还是写操作,并将其路由到相应的数据源。
 
- 负载均衡:支持多种读库负载均衡策略(轮询、随机、权重)。
 
- 事务支持:它能自动感知 Spring 的事务,确保在事务内部,所有操作(无论读写)都路由到主库,以保证事务的原子性和隔离性。这解决了许多手动实现方案中事务与数据源切换的复杂性。
 
- 高可用:ShardingSphere 还支持故障转移,当主库或从库发生故障时,可以自动进行切换。
 
- 功能强大:除了读写分离,ShardingSphere 还提供了数据分片、分布式事务、分布式主键生成等更多高级功能。
 
AOP + ThreadLocal实现
代码实现
引入必要的依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | <dependencies>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-data-jpa</artifactId>     </dependency>     <dependency>         <groupId>mysql</groupId>         <artifactId>mysql-connector-java</artifactId>         <scope>runtime</scope>     </dependency>     <dependency>         <groupId>org.springframework.boot</groupId>         <artifactId>spring-boot-starter-aop</artifactId>     </dependency>     <dependency>         <groupId>com.zaxxer</groupId>         <artifactId>HikariCP</artifactId>     </dependency> </dependencies>
   | 
 
定义数据源类型枚举
这个枚举用于标识当前操作是读还是写。
1 2 3 4 5 6 7
   |  package com.example.demo.datasource;
  public enum DBType {     MASTER,      SLAVE    }
 
  | 
 
数据源上下文管理
使用 ThreadLocal 存储当前线程的数据源类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   |  package com.example.demo.datasource;
  public class DBContextHolder {     private static final ThreadLocal<DBType> contextHolder = new ThreadLocal<>();
      public static void setDBType(DBType dbType) {         contextHolder.set(dbType);     }
      public static DBType getDBType() {         return contextHolder.get();     }
      public static void clearDBType() {         contextHolder.remove();     } }
 
  | 
 
动态数据源路由
继承 AbstractRoutingDataSource,根据 DBContextHolder 中的类型选择对应的数据源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   |  package com.example.demo.datasource;
  import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  import java.util.concurrent.ThreadLocalRandom;
  public class DynamicDataSource extends AbstractRoutingDataSource {
      @Override     protected Object determineCurrentLookupKey() {         DBType dbType = DBContextHolder.getDBType();         if (dbType == null || dbType == DBType.MASTER) {                          return DBType.MASTER;         } else {                                       return DBType.SLAVE;         }     } }
 
  | 
 
数据源配置类
配置主库和从库的连接信息,并将它们注入到 DynamicDataSource 中。
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
   |  package com.example.demo.config;
  import com.example.demo.datasource.DBType; import com.example.demo.datasource.DynamicDataSource; import com.zaxxer.hikari.HikariDataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary;
  import javax.sql.DataSource; import java.util.HashMap; import java.util.Map;
  @Configuration public class DataSourceConfig {
      @Bean(name = "masterDataSource")     @ConfigurationProperties(prefix = "spring.datasource.master")     public DataSource masterDataSource() {         return DataSourceBuilder.create().type(HikariDataSource.class).build();     }
      @Bean(name = "slaveDataSource")     @ConfigurationProperties(prefix = "spring.datasource.slave")     public DataSource slaveDataSource() {         return DataSourceBuilder.create().type(HikariDataSource.class).build();     }
      @Bean     @Primary      public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,                                         @Qualifier("slaveDataSource") DataSource slaveDataSource) {         DynamicDataSource dynamicDataSource = new DynamicDataSource();
                   Map<Object, Object> targetDataSources = new HashMap<>();         targetDataSources.put(DBType.MASTER, masterDataSource);         targetDataSources.put(DBType.SLAVE, slaveDataSource);         dynamicDataSource.setTargetDataSources(targetDataSources);
                   dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
          return dynamicDataSource;     } }
 
  | 
 
自定义注解
用于标记哪些方法是读操作。
1 2 3 4 5 6 7 8 9 10
   |  package com.example.demo.annotation;
  import java.lang.annotation.*;
  @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ReadOperation { }
 
  | 
 
AOP 切面实现读写分离
通过 AOP 拦截带有 @ReadOperation 注解的方法,在方法执行前设置数据源为从库,方法执行后清除数据源。
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
   |  package com.example.demo.aspect;
  import com.example.demo.annotation.ReadOperation; import com.example.demo.datasource.DBContextHolder; import com.example.demo.datasource.DBType; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component;
  @Aspect @Component @Order(1)  public class DataSourceAspect {
           @Pointcut("@annotation(com.example.demo.annotation.ReadOperation)")     public void readOperationPointcut() {     }
           @Pointcut("execution(public * com.example.demo.service.*.*(..))")     public void servicePointcut() {     }
           @Around("readOperationPointcut()")     public Object setReadDataSource(ProceedingJoinPoint joinPoint) throws Throwable {         try {             DBContextHolder.setDBType(DBType.SLAVE);             System.out.println("切换到从库 (Read)");              return joinPoint.proceed();         } finally {             DBContextHolder.clearDBType();              System.out.println("清除数据源类型");          }     }
                     @Around("servicePointcut() && !readOperationPointcut()")     public Object setWriteDataSource(ProceedingJoinPoint joinPoint) throws Throwable {         try {             DBContextHolder.setDBType(DBType.MASTER);             System.out.println("切换到主库 (Write)");              return joinPoint.proceed();         } finally {             DBContextHolder.clearDBType();              System.out.println("清除数据源类型");          }     } }
 
  | 
 
注意 @Order(1):这里将切面的优先级设置为最高(数值越小优先级越高),是为了确保数据源的切换发生在 Spring 事务管理器之前。否则,事务可能会在数据源切换之前绑定到错误的数据源上。
application.properties 配置
配置主库和从库的连接信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   |  spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC spring.datasource.master.username=root spring.datasource.master.password=your_master_password spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
 
  spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3307/slave_db?useSSL=false&serverTimezone=UTC spring.datasource.slave.username=root spring.datasource.slave.password=your_slave_password spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
 
  spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.open-in-view=true # 建议在生产环境中设置为false,以避免N+1问题和长事务
 
  | 
 
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 28 29 30 31 32 33 34 35 36 37 38
   |  package com.example.demo.service;
  import com.example.demo.annotation.ReadOperation; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
  import java.util.List;
  @Service public class UserService {
      @Autowired     private UserRepository userRepository;
           @Transactional     public User saveUser(User user) {         return userRepository.save(user);     }
           @ReadOperation     @Transactional(readOnly = true)      public List<User> getAllUsers() {         return userRepository.findAll();     }
           @ReadOperation     @Transactional(readOnly = true)     public User getUserById(Long id) {         return userRepository.findById(id).orElse(null);     } }
 
  |