方式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); } }
|