Java读写分离案例

wen java案例 2

本文目录导读:

Java读写分离案例

  1. 项目结构
  2. Maven依赖 (pom.xml)
  3. 配置文件 (application.yml)
  4. 核心实现代码
  5. 数据库初始化脚本
  6. 使用方式
  7. 关键特性说明
  8. 生产环境注意事项

我将为您展示一个完整的Java读写分离案例,包含从架构设计到代码实现的全过程。

项目结构

spring-boot-rw-separation/
├── pom.xml
├── src/main/java/com/example/rw/
│   ├── RwApplication.java
│   ├── config/
│   │   ├── DataSourceConfig.java
│   │   ├── MyBatisConfig.java
│   │   └── DynamicDataSourceContextHolder.java
│   ├── controller/
│   │   └── UserController.java
│   ├── mapper/
│   │   └── UserMapper.java
│   ├── model/
│   │   └── User.java
│   ├── service/
│   │   └── UserService.java
│   └── aspect/
│       └── DataSourceAspect.java
└── src/main/resources/
    ├── application.yml
    └── mapper/
        └── UserMapper.xml

Maven依赖 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>rw-separation</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

配置文件 (application.yml)

server:
  port: 8080
spring:
  datasource:
    # 主库(写库)
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root123
    # 从库(读库)
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbc-url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
      username: root
      password: root123
# MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.rw.model
  configuration:
    map-underscore-to-camel-case: true

核心实现代码

1 动态数据源上下文持有者

package com.example.rw.config;
public class DynamicDataSourceContextHolder {
    // 使用ThreadLocal保存当前线程的数据源key
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    // 定义数据源key常量
    public static final String MASTER = "master";
    public static final String SLAVE = "slave";
    public static void setDataSource(String dataSourceKey) {
        CONTEXT_HOLDER.set(dataSourceKey);
    }
    public static String getDataSource() {
        return CONTEXT_HOLDER.get();
    }
    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }
}

2 动态数据源实现

package com.example.rw.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceKey = DynamicDataSourceContextHolder.getDataSource();
        // 如果为空或者没有设置,默认使用主库
        return dataSourceKey == null ? DynamicDataSourceContextHolder.MASTER : dataSourceKey;
    }
}

3 数据源配置

package com.example.rw.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
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 {
    @Value("${spring.datasource.master.jdbc-url}")
    private String masterUrl;
    @Value("${spring.datasource.master.username}")
    private String masterUsername;
    @Value("${spring.datasource.master.password}")
    private String masterPassword;
    @Value("${spring.datasource.slave.jdbc-url}")
    private String slaveUrl;
    @Value("${spring.datasource.slave.username}")
    private String slaveUsername;
    @Value("${spring.datasource.slave.password}")
    private String slavePassword;
    @Bean
    public DataSource masterDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(masterUrl);
        dataSource.setUsername(masterUsername);
        dataSource.setPassword(masterPassword);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setPoolName("masterDataSource");
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(5);
        return dataSource;
    }
    @Bean
    public DataSource slaveDataSource() {
        HikariDataSource dataSource = new HikariDataSource();
        dataSource.setJdbcUrl(slaveUrl);
        dataSource.setUsername(slaveUsername);
        dataSource.setPassword(slavePassword);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setPoolName("slaveDataSource");
        dataSource.setMaximumPoolSize(10);
        dataSource.setMinimumIdle(5);
        return dataSource;
    }
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DynamicDataSourceContextHolder.MASTER, masterDataSource());
        targetDataSources.put(DynamicDataSourceContextHolder.SLAVE, slaveDataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        return dynamicDataSource;
    }
}

4 数据源切面

package com.example.rw.aspect;
import com.example.rw.config.DynamicDataSourceContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
    // 定义切点:service层以select/find/get开头的方法使用从库
    @Pointcut("execution(* com.example.rw.service.*.*(..))")
    public void servicePointCut() {}
    @Before("servicePointCut() && (execution(* com.example.rw.service.*.select*(..)) || " +
            "execution(* com.example.rw.service.*.find*(..)) || " +
            "execution(* com.example.rw.service.*.get*(..)))")
    public void beforeReadOperation() {
        DynamicDataSourceContextHolder.setDataSource(DynamicDataSourceContextHolder.SLAVE);
        System.out.println("切换到从库");
    }
    @Before("servicePointCut() && (execution(* com.example.rw.service.*.insert*(..)) || " +
            "execution(* com.example.rw.service.*.add*(..)) || " +
            "execution(* com.example.rw.service.*.update*(..)) || " +
            "execution(* com.example.rw.service.*.delete*(..)))")
    public void beforeWriteOperation() {
        DynamicDataSourceContextHolder.setDataSource(DynamicDataSourceContextHolder.MASTER);
        System.out.println("切换到主库");
    }
    @After("servicePointCut()")
    public void afterOperation() {
        DynamicDataSourceContextHolder.clearDataSource();
    }
}

5 MyBatis配置

package com.example.rw.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;
@Configuration
@MapperScan("com.example.rw.mapper")
public class MyBatisConfig {
    @Bean
    public SqlSessionFactory sqlSessionFactory(
            @Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        // 设置MyBatis配置
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        sessionFactoryBean.setConfiguration(configuration);
        // 设置Mapper XML文件位置
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        sessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
        return sessionFactoryBean.getObject();
    }
}

6 实体类

package com.example.rw.model;
import lombok.Data;
import java.util.Date;
@Data
public class User {
    private Long id;
    private String username;
    private String email;
    private Integer age;
    private Date createTime;
    private Date updateTime;
}

7 Mapper接口

package com.example.rw.mapper;
import com.example.rw.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper {
    int insert(User user);
    int update(User user);
    int deleteById(@Param("id") Long id);
    User selectById(@Param("id") Long id);
    List<User> selectAll();
    List<User> selectByUsername(@Param("username") String username);
}

8 Mapper XML文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.rw.mapper.UserMapper">
    <insert id="insert" parameterType="com.example.rw.model.User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (
            username, email, age, create_time, update_time
        ) VALUES (
            #{username}, #{email}, #{age}, NOW(), NOW()
        )
    </insert>
    <update id="update" parameterType="com.example.rw.model.User">
        UPDATE user
        <set>
            <if test="username != null">username = #{username},</if>
            <if test="email != null">email = #{email},</if>
            <if test="age != null">age = #{age},</if>
            update_time = NOW()
        </set>
        WHERE id = #{id}
    </update>
    <delete id="deleteById">
        DELETE FROM user WHERE id = #{id}
    </delete>
    <select id="selectById" resultType="com.example.rw.model.User">
        SELECT id, username, email, age, create_time, update_time
        FROM user
        WHERE id = #{id}
    </select>
    <select id="selectAll" resultType="com.example.rw.model.User">
        SELECT id, username, email, age, create_time, update_time
        FROM user
        ORDER BY create_time DESC
    </select>
    <select id="selectByUsername" resultType="com.example.rw.model.User">
        SELECT id, username, email, age, create_time, update_time
        FROM user
        WHERE username = #{username}
    </select>
</mapper>

9 Service层

package com.example.rw.service;
import com.example.rw.mapper.UserMapper;
import com.example.rw.model.User;
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 UserMapper userMapper;
    @Transactional
    public int insert(User user) {
        return userMapper.insert(user);
    }
    @Transactional
    public int update(User user) {
        return userMapper.update(user);
    }
    @Transactional
    public int delete(Long id) {
        return userMapper.deleteById(id);
    }
    public User selectById(Long id) {
        return userMapper.selectById(id);
    }
    public List<User> selectAll() {
        return userMapper.selectAll();
    }
    public List<User> selectByUsername(String username) {
        return userMapper.selectByUsername(username);
    }
}

10 Controller层

package com.example.rw.controller;
import com.example.rw.model.User;
import com.example.rw.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/user")
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping
    public int createUser(@RequestBody User user) {
        return userService.insert(user);
    }
    @PutMapping
    public int updateUser(@RequestBody User user) {
        return userService.update(user);
    }
    @DeleteMapping("/{id}")
    public int deleteUser(@PathVariable Long id) {
        return userService.delete(id);
    }
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.selectById(id);
    }
    @GetMapping("/list")
    public List<User> getAllUsers() {
        return userService.selectAll();
    }
    @GetMapping("/username/{username}")
    public List<User> getUserByUsername(@PathVariable String username) {
        return userService.selectByUsername(username);
    }
}

11 启动类

package com.example.rw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class RwApplication {
    public static void main(String[] args) {
        SpringApplication.run(RwApplication.class, args);
    }
}

数据库初始化脚本

-- 主库 (master_db)
CREATE DATABASE IF NOT EXISTS master_db;
USE master_db;
CREATE TABLE IF NOT EXISTS user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100),
    age INT,
    create_time DATETIME,
    update_time DATETIME
);
-- 插入测试数据
INSERT INTO user (username, email, age, create_time, update_time) 
VALUES ('admin', 'admin@example.com', 25, NOW(), NOW());
-- 从库 (slave_db)
CREATE DATABASE IF NOT EXISTS slave_db;
USE slave_db;
CREATE TABLE IF NOT EXISTS user (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100),
    age INT,
    create_time DATETIME,
    update_time DATETIME
);

使用方式

  1. 启动应用:运行 RwApplication 主类

  2. 测试API

# 写入操作(使用主库)
curl -X POST http://localhost:8080/api/user \
  -H "Content-Type: application/json" \
  -d '{"username":"test1","email":"test1@example.com","age":20}'
# 读取操作(使用从库)
curl http://localhost:8080/api/user/1
curl http://localhost:8080/api/user/list
# 更新操作(使用主库)
curl -X PUT http://localhost:8080/api/user \
  -H "Content-Type: application/json" \
  -d '{"id":1,"username":"updated","email":"updated@example.com","age":30}'
# 删除操作(使用主库)
curl -X DELETE http://localhost:8080/api/user/1

关键特性说明

1 读写分离规则

  • 读操作(select/find/get):自动切换到从库
  • 写操作(insert/update/delete):自动切换到主库

2 数据一致性

  • 主从数据库通过MySQL复制机制保持同步
  • 配置MySQL的主从复制(binlog-based replication)

3 事务处理

  • 写操作自动使用主库,保证数据一致性
  • 可以通过 @Transactional 注解控制事务

4 扩展性

  • 可以轻松添加多个从库
  • 根据业务需求动态调整数据源

生产环境注意事项

  1. 主从延迟处理:需要处理主从数据同步的延迟问题
  2. 故障转移:实现从库故障自动切换
  3. 监控与报警:监控主从状态和延迟
  4. 负载均衡:可以配置多个从库进行负载均衡
  5. 连接池管理:合理配置连接池大小

这个案例实现了一个完整的读写分离方案,适用于大多数中小型应用场景。

抱歉,评论功能暂时关闭!