读写分离的分配,即如何在应用程序中将读操作和写操作路由到不同的数据库实例,可以通过几种不同的方法来实现。这些方法可以在应用程序层、数据库层或使用中间件来完成。以下是几种常见的实现方法:
应用程序层实现
在应用程序层实现读写分离,通常通过配置多个数据源并在代码中显式地选择适当的数据源。使用 AOP(面向切面编程)来自动选择数据源是一种常见的方法。
具体实现步骤
- 配置多数据源:配置一个主数据源(用于写操作)和多个从数据源(用于读操作)。
- 实现路由逻辑:通过 AOP 或其他方式在代码中选择适当的数据源。
- 使用自定义注解:标记需要路由到不同数据源的方法。
以下是详细的实现示例:
配置文件
在 application.yml
中配置主库和从库的信息。
# application.yml
spring:
datasource:
master:
url: jdbc:mysql://master-db:3306/mydb
username: root
password: root
slaves:
- url: jdbc:mysql://slave-db1:3306/mydb
username: root
password: root
- url: jdbc:mysql://slave-db2:3306/mydb
username: root
password: root
数据源配置
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Autowired
private MasterDataSourceProperties masterProperties;
@Autowired
private SlaveDataSourceProperties slaveProperties;
@Bean
@Primary
public DataSource dataSource() {
AbstractRoutingDataSource routingDataSource = new ReplicationRoutingDataSource();
HikariDataSource masterDataSource = new HikariDataSource();
masterDataSource.setJdbcUrl(masterProperties.getUrl());
masterDataSource.setUsername(masterProperties.getUsername());
masterDataSource.setPassword(masterProperties.getPassword());
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
for (int i = 0; i < slaveProperties.getSlaves().size(); i++) {
SlaveProperties slave = slaveProperties.getSlaves().get(i);
HikariDataSource slaveDataSource = new HikariDataSource();
slaveDataSource.setJdbcUrl(slave.getUrl());
slaveDataSource.setUsername(slave.getUsername());
slaveDataSource.setPassword(slave.getPassword());
targetDataSources.put("slave" + i, slaveDataSource);
}
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
}
路由数据源
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class ReplicationRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDataSourceType(String dataSourceType) {
contextHolder.set(dataSourceType);
}
public static void clearDataSourceType() {
contextHolder.remove();
}
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
}
数据源选择器
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
@Before("@annotation(com.example.annotation.Master)")
public void setWriteDataSourceType() {
ReplicationRoutingDataSource.setDataSourceType("master");
}
@Before("@annotation(com.example.annotation.Slave) || execution(* com.example.service..*.find*(..))")
public void setReadDataSourceType() {
ReplicationRoutingDataSource.setDataSourceType("slave0"); // 可实现负载均衡策略
}
}
自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Master {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Slave {
}
示例服务
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Master
@Transactional
public void saveUser(User user) {
userRepository.save(user);
}
@Slave
public User findUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}
使用中间件
使用中间件来实现读写分离也是一种常见的方法。中间件通常位于应用程序和数据库之间,负责根据操作类型将请求路由到适当的数据库实例。常见的中间件包括 MySQL 的 ProxySQL 和 MariaDB 的 MaxScale。
ProxySQL 示例配置
- 安装 ProxySQL:可以通过包管理器安装 ProxySQL。
- 配置 ProxySQL:在
proxysql.cnf
文件中配置主从数据库。
datadir="/var/lib/proxysql"
admin_variables=
{
admin_credentials="admin:admin"
mysql_ifaces="0.0.0.0:6032"
}
mysql_variables=
{
threads=4
max_connections=1024
}
mysql_servers =
(
{ address="master-db", port=3306, hostgroup=0, max_connections=1000, weight=1 },
{ address="slave-db1", port=3306, hostgroup=1, max_connections=1000, weight=1 },
{ address="slave-db2", port=3306, hostgroup=1, max_connections=1000, weight=1 }
)
mysql_users =
(
{ username="proxyuser", password="proxypassword", default_hostgroup=0, transaction_persistent=1 }
)
mysql_query_rules =
(
{ rule_id=1, match_pattern="^SELECT", destination_hostgroup=1, apply=1 }
)
- 启动 ProxySQL:使用
systemctl
或其他方式启动 ProxySQL。
systemctl start proxysql
数据库层实现
有些数据库本身提供了读写分离的功能。例如,MySQL 的复制机制允许配置一个主数据库和多个从数据库,然后通过连接池或驱动程序实现读写分离。
总结
读写分离的实现方法有多种,可以根据具体需求和技术栈选择适合的方法。在应用程序层实现读写分离较为灵活,可以精细控制读写操作的路由逻辑;使用中间件实现读写分离则可以简化应用程序的逻辑,但需要额外维护中间件的配置和管理;在数据库层实现读写分离可以利用数据库本身的功能,减少对应用程序的改动。