本文目录导读:

我将为您展示一个完整的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
);
使用方式
-
启动应用:运行
RwApplication主类 -
测试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 扩展性
- 可以轻松添加多个从库
- 根据业务需求动态调整数据源
生产环境注意事项
- 主从延迟处理:需要处理主从数据同步的延迟问题
- 故障转移:实现从库故障自动切换
- 监控与报警:监控主从状态和延迟
- 负载均衡:可以配置多个从库进行负载均衡
- 连接池管理:合理配置连接池大小
这个案例实现了一个完整的读写分离方案,适用于大多数中小型应用场景。