方式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
# application.yml

spring:
main:
allow-bean-definition-overriding: true # 允许ShardingSphere覆盖DataSource Bean
datasource:
type: com.zaxxer.hikari.HikariDataSource # 使用HikariCP连接池

# ShardingSphere 配置
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 为逻辑数据源名称,应用程序连接此名称
read-write-splitting-datasource:
writeDataSourceName: master # 指定写数据源为 master
readDataSourceNames: # 指定读数据源,可以有多个,ShardingSphere会进行负载均衡
- slave1
loadBalancer: # 读负载均衡策略
type: ROUND_ROBIN # 轮询 (默认)。可选:RANDOM(随机)、WEIGHT (权重)

# JPA 配置 (如果你使用 JPA)
spring.jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
open-in-view: true # 生产环境建议设置为 false

配置说明:

  • 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);
}

// 读操作:自动路由到从库(ShardingSphere根据SQL类型判断)
@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 的优势

  1. 对业务代码零侵入:这是 ShardingSphere 最大的优势。你的 Service、DAO 层代码无需任何改动,就像在使用一个普通的数据源一样。
  2. SQL 自动解析和路由:ShardingSphere 会自动解析 SQL 语句,判断是读操作还是写操作,并将其路由到相应的数据源。
  3. 负载均衡:支持多种读库负载均衡策略(轮询、随机、权重)。
  4. 事务支持:它能自动感知 Spring 的事务,确保在事务内部,所有操作(无论读写)都路由到主库,以保证事务的原子性和隔离性。这解决了许多手动实现方案中事务与数据源切换的复杂性。
  5. 高可用:ShardingSphere 还支持故障转移,当主库或从库发生故障时,可以自动进行切换。
  6. 功能强大:除了读写分离,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
// src/main/java/com/example/demo/datasource/DBType.java
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
// src/main/java/com/example/demo/datasource/DBContextHolder.java
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
// src/main/java/com/example/demo/datasource/DynamicDataSource.java
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 {
// 读操作时,可以进行简单的负载均衡,这里假设只有一个从库,如果多个可以随机选择
// 比如,如果有 slave1, slave2 可以 ThreadLocalRandom.current().nextInt(2) 选择
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
// src/main/java/com/example/demo/config/DataSourceConfig.java
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 // 标记为主数据源,Spring会自动注入到需要DataSource的地方
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
// src/main/java/com/example/demo/annotation/ReadOperation.java
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
// src/main/java/com/example/demo/aspect/DataSourceAspect.java
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 {

// 定义切点,拦截所有标记了 @ReadOperation 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.ReadOperation)")
public void readOperationPointcut() {
}

// 定义切点,拦截所有public方法(这里可以更细粒度,例如只拦截Service层方法)
@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("清除数据源类型"); // 调试信息
}
}

// 环绕通知,对于没有明确标记为读操作的方法,默认使用主库 (写操作)
// 注意:如果一个方法同时有 @ReadOperation 和其他的写操作,需要考虑优先级
// 这里简单处理为如果不是读操作,就认为是写操作,使用主库
@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
# Master DataSource
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

# Slave DataSource
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

# JPA 配置 (如果你使用 JPA)
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
// src/main/java/com/example/demo/service/UserService.java
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;

// 写操作,默认走主库,AOP 会将其识别为写操作
@Transactional
public User saveUser(User user) {
return userRepository.save(user);
}

// 读操作,标记 @ReadOperation 注解,AOP 会将其识别为读操作,走从库
@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);
}
}