本文目录导读:

我将为您展示一个完整的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 扩展性
- 可以轻松添加多个从库
- 根据业务需求动态调整数据源
生产环境注意事项
- 主从延迟处理:需要处理主从数据同步的延迟问题
- 故障转移:实现从库故障自动切换
- 监控与报警:监控主从状态和延迟
- 负载均衡:可以配置多个从库进行负载均衡
- 连接池管理:合理配置连接池大小
这个案例实现了一个完整的读写分离方案,适用于大多数中小型应用场景。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播