MySQL(110)如何进行数据库读写分离?

88 阅读3分钟

数据库读写分离是指将数据库的读操作和写操作分离到不同的数据库实例上进行,以提高系统的性能和可扩展性。通常情况下,会有一个主库(Master)处理写操作,多个从库(Slave)处理读操作。下面详细介绍如何实现数据库读写分离,并结合Java代码进行实现。

环境准备

假设我们使用Spring Boot和MySQL,且需要操作的表是orders表。我们已经有了主从数据库环境,并配置好相关的数据库实例。

项目依赖

pom.xml中添加必要的依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

数据源配置

在数据源配置类中配置主库和从库的数据源,并设置一个路由数据源来进行读写分离。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableTransactionManagement
public class DataSourceConfig {

    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/db_master")
                .username("root")
                .password("password")
                .driverClassName("com.mysql.cj.jdbc.Driver")
                .build();
    }

    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/db_slave")
                .username("root")
                .password("password")
                .driverClassName("com.mysql.cj.jdbc.Driver")
                .build();
    }

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return DbContextHolder.getDbType();
            }
        };

        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DbType.MASTER, masterDataSource);
        dataSourceMap.put(DbType.SLAVE, slaveDataSource);

        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        routingDataSource.setTargetDataSources(dataSourceMap);
        return routingDataSource;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(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();
    }
}

public enum DbType {
    MASTER,
    SLAVE
}

AOP切面实现

使用AOP切面实现读写分离。在读操作前设置数据源为从库,在写操作前设置数据源为主库。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class DataSourceAspect {

    @Before("execution(* com.example.service.*.find*(..)) || execution(* com.example.service.*.get*(..))")
    public void setReadDataSourceType() {
        DbContextHolder.setDbType(DbType.SLAVE);
    }

    @Before("execution(* com.example.service.*.insert*(..)) || execution(* com.example.service.*.update*(..)) || execution(* com.example.service.*.delete*(..))")
    public void setWriteDataSourceType() {
        DbContextHolder.setDbType(DbType.MASTER);
    }
}

数据库操作服务实现

实现具体的数据库操作服务类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void insertOrder(String orderId, String productName, double price) {
        String sql = "INSERT INTO orders (order_id, product_name, price) VALUES (?, ?, ?)";
        jdbcTemplate.update(sql, orderId, productName, price);
    }

    public List<Order> getOrdersByProductName(String productName) {
        String sql = "SELECT * FROM orders WHERE product_name = ?";
        return jdbcTemplate.query(sql, new Object[]{productName}, (rs, rowNum) ->
                new Order(rs.getString("order_id"), rs.getString("product_name"), rs.getDouble("price")));
    }
}

测试读写分离

通过调用OrderService中的方法进行测试:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class TestRunner implements CommandLineRunner {

    @Autowired
    private OrderService orderService;

    @Override
    public void run(String... args) throws Exception {
        // 插入数据
        orderService.insertOrder("order1", "Product A", 100.0);

        // 查询数据
        List<Order> orders = orderService.getOrdersByProductName("Product A");
        orders.forEach(System.out::println);
    }
}

结论

通过以上步骤,我们展示了如何实现数据库读写分离。关键点如下:

  1. 数据源配置:配置主库和从库的数据源,并设置路由数据源来进行读写分离。
  2. 数据源上下文:定义一个上下文类来存储当前的数据库类型(主库或从库)。
  3. AOP切面实现:使用AOP切面在读操作和写操作前分别设置数据源为从库和主库。
  4. 数据库操作服务实现:实现具体的数据库操作服务类,通过JdbcTemplate进行数据库操作。

这种方法可以有效地实现数据库读写分离,提高系统的性能和可扩展性。