详细介绍一下如何在Spring Boot中实现数据库的读写分离?
在分布式高并发系统中我们可以通过数据库的读写分离操作来提升系统的性能以及可扩展性,在SpringBoot中也提供了很多快捷方便的配置来实现数据库的读写分离配置,通过读写分离配置将对数据的读写操作分配到不同的数据库实例上从而达到提升系统性能的目的。下面我们就来详细介绍一下如何在Spring Boot应用程序中实现数据的读写分离操作。
配置多数据源
为了实现读写分离配置,首先需要配置多个数据源的支持,一个数据源用来进行写操作,而另一个数据源用来进行读操作,在SpringBoot中可以通过DataSource来配置实现多数据源操作。
配置主库和从库的数据源
假设,我们有两个数据库一个是主库用来进行写操作,一个是从库用来进行读操作,然后我们需要在配置文件中分别配置两个数据库链接,如下所示。
spring:
datasource:
write:
url: jdbc:mysql://localhost:3306/write_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
read:
url: jdbc:mysql://localhost:3307/read_db
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
配置数据源Bean
接下来就需要在配置类中分别创建主从二库的DataSource Bean,为了更好的管理这些数据源操作,我们通过@Primary注解来指定主数据源(即写库)作为默认数据源。
@Configuration
public class DataSourceConfig {
@Bean(name = "writeDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.write")
public DataSource writeDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "readDataSource")
@ConfigurationProperties(prefix = "spring.datasource.read")
public DataSource readDataSource() {
return DataSourceBuilder.create().build();
}
}
实现动态数据源路由
在数据库的读写分离操作中,我们需要根据不同的操作来选择不同的数据源来进行操作,一般情况下,写操作会从主库操作,而读操作则是由从库来实现,如下所示,我们可以通过动态数据路由操作来实现这个操作。
创建动态数据源
首先定义了一个继承自AbstractRoutingDataSource的DynamicDataSource动态数据源控制类类,然后重写了其中的determineCurrentLookupKey()方法来决定当前操作应该使用哪个数据源。
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 获取当前操作的数据库类型(主库或从库)
return DataSourceContextHolder.getDatabaseType();
}
}
创建一个上下文持有者
为了能够在不同线程的上下文中,存储当前线程使用的数据库的类型,我们可以通过ThreadLocal来实现线程单独变量的存储操作,用来标识不同的数据源类型,如下所示。
public class DataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(String databaseType) {
contextHolder.set(databaseType);
}
public static String getDatabaseType() {
return contextHolder.get();
}
public static void clearDatabaseType() {
contextHolder.remove();
}
}
配置动态数据源Bean
完成上面的操作之后,需要在SpringBoot中配置DynamicDataSource实现,并且将其注册到容器中来进行数据源操作的管理,如下所示。
@Configuration
public class DataSourceConfig {
@Bean
public DynamicDataSource dataSource(@Qualifier("writeDataSource") DataSource writeDataSource,
@Qualifier("readDataSource") DataSource readDataSource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("write", writeDataSource);
targetDataSources.put("read", readDataSource);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(writeDataSource); // 默认使用主库
return dynamicDataSource;
}
}
配置事务管理器
完成了数据源的管理之后,需要对不同数据源的事务管理进行配置,来保证不同数据源中的数据事务的一致性和完整性,如下所示。需要配置一个事务管理器来对其进行管理配置。
@Configuration
public class TransactionConfig {
@Bean
public DataSourceTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
使用AOP切换数据源
为了在不同的数据操作的时候能够自动切换到不同的数据处理库中,我们可以通过AOP来实现动态数据源的切换操作。
创建一个切面类
为了实现动态数据源的自动切换,我们需要创建一个切面类来识别用户数据操作。如下所示。
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(ReadOnly)") // 只读操作注解
public Object setReadDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
DataSourceContextHolder.setDatabaseType("read"); // 设置为从库
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.clearDatabaseType();
}
}
@Around("@annotation(WriteOnly)") // 写操作注解
public Object setWriteDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
DataSourceContextHolder.setDatabaseType("write"); // 设置为主库
try {
return joinPoint.proceed();
} finally {
DataSourceContextHolder.clearDatabaseType();
}
}
}
创建自定义注解
然后我们可以创建自定义的注解用来标识那些方法是读操作,那些方法是写操作,如下所示,我们定义了两个注解分别来表示读操作和写操作。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteOnly {
}
总结
到这里我们就已经完成来了读写操作的数据库配置,下面我们就可以在服务层中通过上面定义好的注解来标识读写操作。
@Service
public class UserService {
@ReadOnly
public List<User> getUsers() {
return userRepository.findAll(); // 从库读取数据
}
@WriteOnly
public void saveUser(User user) {
userRepository.save(user); // 主库写入数据
}
}
通过以上步骤,我们成功地在 Spring Boot 项目中实现了数据库的读写分离。