**高并发、微服务 、性能调优实战案例100讲，所有案例均源于个人工作实战，均配合代码落地**

加我微信：itsoku，所有案例均提供在线答疑。



# 第27节 SpringBoot读写分离实战

<span style="font-weight:bold; color:red">目前整个课程59块钱，100个案例，含所有源码 & 文档 & 技术支持，可点击左下角小黄车了解</span>

## 本文内容

- 通过一个注解搞定读写分离
- 支持查询强制路由到主库

## 涉及到的技术

- SpringBoot 2.7.13
- MyBatis
- 多数据源路由
- AOP环绕通知

## 背景

大多数系统都是读多写少，为了降低数据库的压力，可以对主库创建多个从库，从库自动从主库同步数据，程序中将写的操作发送到主库，将读的操作发送到从库去执行。

也可以强制读主库，这种可以解决，主从延迟的情况下，主库写入数据后立即查询从库，查询不到的问题。



## 实现思路

1. 可以定义一个读写分离的注解：@ReadWrite(value=路由策略)，路由策略主要有下面3种

   ```java
   public enum ReadWriteRoutingStrategy {
       MASTER, //路由到主库
       SLAVE, //路由到从库
       HIT_MASTER //强制路由到主库
   }
   ```

2. @ReadWrite注解用于标注在service层需要做读写分离的方法上，若没有该注解，则自动路由到主库

3. 通过Aop拦截@ReadWrite标注的方法，从@ReadWrite这个注解中取出读写策略，放到ThreadLocal中，如下

   ```java
   ThreadLocal<ReadWriteRoutingStrategy> readWriteRoutingStrategyThreadLocal = new ThreadLocal<>();
   
   //从@ReadWrite注解中取出读写策略，放到ThreadLocal中
   ReadWrite readWrite;
   readWriteRoutingStrategyThreadLocal.set(readWrite.value());
   ```

4. 需要一个有路由功能够的数据源，他里面需要有个targetDataSourcesMap，用于维护路由策略和真实数据源的映射关系，如下

   ```java
   //路由策略和数据源映射关系，放在一个map中（key：路由策略，value：对应的实际数据源）
   Map<Object, Object> targetDataSourcesMap = new HashMap<>();
   //主库路由配置
   targetDataSourcesMap.put(ReadWriteRoutingStrategy.MASTER, 主库数据源);
   //从库路由配置
   targetDataSourcesMap.put(ReadWriteRoutingStrategy.SLAVE, 从库数据源);
   //强制路由主库的配置
   targetDataSourcesMap.put(ReadWriteRoutingStrategy.HIT_MASTER, 从库数据源);
   ```

5. 操作数据库的时候，先从 readWriteRoutingStrategyThreadLocal.get() 中获取路由的策略，作为查找的key，然后从上面路由数据源的targetDataSourcesMap中找到实际的数据源，然后去执行db操作就可以了



## 先看下效果

### 创建主从库

下面准备2个数据库：javacode2018_master（主库）、javacode2018_slave（从库）

2个库中都创建一个t_user表，分别插入了一条用户数据，数据内容不一样，稍后用这个数据来验证走的是主库还是从库。

```sql
DROP DATABASE IF EXISTS javacode2018_master;
CREATE DATABASE IF NOT EXISTS javacode2018_master;
USE javacode2018_master;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT '姓名'
);
INSERT INTO t_user (id, name) VALUE (1, 'master库');


DROP DATABASE IF EXISTS javacode2018_slave;
CREATE DATABASE IF NOT EXISTS javacode2018_slave;
USE javacode2018_slave;
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
  id   INT PRIMARY KEY       AUTO_INCREMENT,
  name VARCHAR(256) NOT NULL DEFAULT ''
  COMMENT '姓名'
);
INSERT INTO t_user (id, name) VALUE (1, 'slave库');
```

### 启动springboot应用

```java
com.itsoku.lesson027.Lesson027Application
```

### 测试用例

> 这个案例会演示4种情况
>
> 1. 没有标注@RreadWrite的方法，默认情况会走主库
> 2. 标注有@ReadWrite(Master)会走主库
> 3. 标注有@ReadWrite(Slave)会走从库
> 4. 标注有@ReadWrite(Slave)的方法，可以通过硬编码，强制将其路由到主库

```
com.itsoku.lesson027.controller.UserController#test
```

### 浏览器中访问

```http
http://localhost:8080/test
```

#### 输出

```json
{
  "success": true,
  "data": {
    "user": {
      "id": 1,
      "name": "master库"
    },
    "userFromMaster": {
      "id": 1,
      "name": "master库"
    },
    "userFromSlave": {
      "id": 1,
      "name": "slave库"
    },
    "userHitMaster": {
      "id": 1,
      "name": "master库"
    }
  },
  "msg": null,
  "code": null
}
```



## 源码解析

### @ReadWrite注解

标注在service上需要做读写分离的方法上

```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadWrite {

    /**
     * 获取路由策略（主库、从库、还是强制路由到主库？）
     *
     * @return
     */
    ReadWriteRoutingStrategy value();
}
```

### ReadWriteRoutingStrategy：读写路由策略类（3个策略）

```java
public enum ReadWriteRoutingStrategy {
    MASTER, //路由到主库
    SLAVE, //路由到从库
    HIT_MASTER //强制路由到主库
}
```

### ReadWriteDataSource：读写分离的数据源

> 这是一个复合型的数据源，他继承了org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
>
> 他里面会维护一个数据源的map，这个map的key就是路由的策略，value就是实际的数据源
>
> 里面还有个 determineCurrentLookupKey方法，如下 ，用于获取路由的策略，通过这个方法的返回值，就可以找到目标数据源。

```java
public class ReadWriteDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        //这里我们从ThreadLocal中获取路由的策略
        return ReadWriteRoutingStrategyHolder.getReadWriteRoutingStrategy();
    }
}
```

### ReadWriteRoutingStrategyHolder：使用ThreadLocal记录当前读写路由策略

```java
public class ReadWriteRoutingStrategyHolder {
    private static ThreadLocal<ReadWriteRoutingStrategy> readWriteRoutingStrategyThreadLocal = new ThreadLocal<>();

    public static void setReadWriteRoutingStrategy(ReadWriteRoutingStrategy readWriteRoutingStrategy) {
        readWriteRoutingStrategyThreadLocal.set(readWriteRoutingStrategy);
    }

    /**
     * 路由到主库
     */
    public static void master() {
        setReadWriteRoutingStrategy(ReadWriteRoutingStrategy.MASTER);
    }

    /**
     * 路由到从库
     */
    public static void slave() {
        setReadWriteRoutingStrategy(ReadWriteRoutingStrategy.SLAVE);
    }

    /**
     * 强制走主库执行 execute的代码
     *
     * @param execute
     * @param <T>
     * @return
     */
    public static <T> T hitMaster(Supplier<T> execute) {
        ReadWriteRoutingStrategy old = getReadWriteRoutingStrategy();
        try {
            setReadWriteRoutingStrategy(ReadWriteRoutingStrategy.HIT_MASTER);
            return execute.get();
        } finally {
            readWriteRoutingStrategyThreadLocal.set(old);
        }
    }

    /**
     * 获取读写策略
     *
     * @return
     */
    public static ReadWriteRoutingStrategy getReadWriteRoutingStrategy() {
        return readWriteRoutingStrategyThreadLocal.get();
    }
}
```

### ReadWriteAspect：AOP环绕通知

拦截所有标注有 @ReadWrite注解的方法，从这个注解中取出读写策略，将读写策略放到上面ReadWriteRoutingStrategyHolder中的readWriteRoutingStrategyThreadLocal中

```java
@Aspect
public class ReadWriteAspect {

    /**
     * 环绕通知，拦截所有方法上标注有 @ReadWrite注解的方法
     *
     * @param joinPoint
     * @param readWrite
     * @return
     * @throws Throwable
     */
    @Around("@annotation(readWrite)")
    public Object around(ProceedingJoinPoint joinPoint, ReadWrite readWrite) throws Throwable {
        //从ThreadLocal中获取读写策略
        ReadWriteRoutingStrategy readWriteRoutingStrategy = ReadWriteRoutingStrategyHolder.getReadWriteRoutingStrategy();
        // 若选择了强制路由到主库，则执行执行业务
        if (readWriteRoutingStrategy == ReadWriteRoutingStrategy.HIT_MASTER) {
            return joinPoint.proceed();
        }
        // 否则，从@ReadWrite注解中获取读写策略，放到ThreadLocal中，然后去执行业务
        ReadWriteRoutingStrategyHolder.setReadWriteRoutingStrategy(readWrite.value());
        return joinPoint.proceed();
    }
}
```

### ReadWriteConfiguration：读写分离Spring配置类

> 定义读写分离需要用到的bean，重点关注数据源的配置。

```java
com.itsoku.lesson027.ds.ReadWriteConfiguration
```

### 使用

如下，在需要做读写分离的方法上标注@ReadWrite注解，并通过其value属性指定路由策略，是走主库还是从库

```java
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * 从主库获取用户
     *
     * @return
     */
    @ReadWrite(ReadWriteRoutingStrategy.MASTER)
    public UserPO getUserFromMaster() {
        return this.userMapper.getUser(1);
    }

    /**
     * 从库获取用户
     *
     * @return
     */
    @ReadWrite(ReadWriteRoutingStrategy.SLAVE)
    public UserPO getUserFromSlave() {
        return this.userMapper.getUser(1);
    }
}
```

## 其他问题

### 不加@ReadWrite注解会怎么样？

通过ReadWriteDataSource.setDefaultTargetDataSource方法设置兜底的数据源，如下，这里用主数据源进行兜底，也就是说没有路由策略或者根据路由策略找不到对应的数据源时，就会用setDefaultTargetDataSource指定的这个数据源进行兜底。

```java
//创建我们自定义的路由数据源
ReadWriteDataSource readWriteDataSource = new ReadWriteDataSource();
//设置默认兜底的数据源
readWriteDataSource.setDefaultTargetDataSource(this.masterDataSource());
```



### 强制走主库如何使用？

> 为什么需要强制走主库？
>
> 由于主从同步需要一个时间，数据写入主库后，立即去读从库，此时若数据还未同步到从库，会导致读取不到的问题，这个时候可以强制走主库查询解决这个问题。

使用下面这个方法，传入需要执行的查询逻辑，便可强制走主库查询

```java
com.itsoku.lesson027.ds.ReadWriteRoutingStrategyHolder#hitMaster(Supplier<T> execute)
```



## 案例源码

源码同样是放在我的《高并发&微服务&性能调优实战案例100讲》的代码中（lesson027模块中），有兴趣的可以点击左下角的小黄车了解下，感谢大家的观看。



# 高并发 & 微服务 & 性能调优实战案例100讲

## 已更新 27 节课

<span style="font-weight:bold; color:red">目前整个课程59块钱，含所有源码 & 文档 & 技术支持，一杯咖啡的价格，还没下手的朋友，赶紧了，马上要涨价了</span>。

```java
1. 分片上传实战
2. 通用并发处理工具类实战
3. 实现一个好用接口性能压测工具类
4. 超卖问题的4种解决方案，也是防止并发修改数据出错的通用方案
5. Semaphore实现接口限流实战
6. 并行查询，优化接口响应速度实战
7. 接口性能优化之大事务优化
8. 通用的Excel动态导出功能实战
9. 手写线程池管理器，管理&监控所有线程池
10. 动态线程池
11. SpringBoot实现动态Job实战
12. 并行查询，性能优化利器，可能有坑
13. 幂等的4种解决方案，吃透幂等性问题
14. 接口通用返回值设计与实现
15. 接口太多，各种dto、vo不计其数，如何命名？
16. 一个业务太复杂了，方法太多，如何传参？
17. 接口报错，如何快速定位日志？
18. 线程数据共享必学的3个工具类：ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal
19. 通过AOP统一打印请求链路日志，排错效率飞升
20. 大批量任务处理常见的方案（模拟余额宝发放收益）
21. 并发环境下，如何验证代码是否正常？
22. MySql和Redis数据一致性
23. SpringBoot数据脱敏优雅设计与实现
24. 一行代码搞定系统操作日志
25. Aop简化MyBatis分页功能
26. ThreadLocal 遇到线程池有大坑 & 通用解决方案
27. SpringBoot读写分离实战（一个注解搞定读写分离 && 强制路由主库）
```



## 课程部分大纲，连载中。。。。

以下课程均来源于个人多年的实战，均提供原理讲解 && 源码落地

1. 分片上传实战
2. 通用并发处理工具类实战
3. 实现一个好用接口性能压测工具类
4. 超卖问题的4种解决方案，也是防止并发修改数据出错的通用方案
5. Semaphore实现接口限流实战
6. 并行查询，优化接口响应速度实战
7. 接口性能优化之大事务优化
8. 通用的Excel动态导出功能实战
9. 手写线程池管理器，管理&监控所有线程池
10. 动态线程池
11. SpringBoot实现动态Job实战
12. 并行查询，性能优化利器，可能有坑
13. 幂等的4种解决方案，吃透幂等性问题
14. 接口通用返回值设计与实现
15. 接口太多，各种dto、vo不计其数，如何命名？
16. 一个业务太复杂了，方法太多，如何传参？
17. 接口报错，如何快速定位日志？
18. 线程数据共享必学的3个工具类：ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal
19. 通过AOP统一打印请求链路日志，排错效率飞升
20. 大批量任务处理常见的方案（模拟余额宝发放收益）
21. 并发环境下，如何验证代码是否正常？
22. MySql和Redis数据一致性
23. SpringBoot数据脱敏优雅设计与实现
24. 一行代码搞定系统操作日志
25. Aop简化MyBatis分页功能
26. ThreadLocal 遇到线程池有大坑 & 通用解决方案
27. SpringBoot读写分离实战（一个注解搞定读写分离 && 强制路由主库）
28. MQ专题：MQ典型的使用场景
29. MQ专题：如何确保消息不丢失？
30. MQ专题：事务消息落地
31. MQ专题：消息幂等消费通用方案
32. MQ专题：延迟消息通用方案实战
33. MQ专题：顺序消息通用方案实战
34. MQ专题：消息积压问题
35. 分布式事务：事务消息实现事务最终一致性
36. 分布式事务：通用的TCC分布式事务生产级代码落地实战
37. 分布式锁案例实战
38. 微服务中如何传递上下文？实战
39. 微服务链路日志追踪实战（原理&代码落地）
40. SpringBoot实现租户数据隔离
41. MyBatis进阶：封装MyBatis，实现通用的无SQL版CRUD功能，架构师必备
42. MyBatis进阶：自己实现通用分表功能，架构师必备
43. MyBatis进阶：实现多租户隔离ORM框架
44. SpringBoot中实现自动监听PO的变化，自动生成表结构
45. 分布式专题：其他实战课程等
46. 性能调优：如何排查死锁？
47. 性能调优：如何排查内存溢出？
48. 性能调优：CPU被打满，如何排查？
49. 性能调优：生产代码没生效，如何定位？
50. 性能调优：接口太慢，如何定位？
51. 性能调优：如何查看生产上接口的入参和返回值？
52. 性能调优：远程debug
53. 生产上出现了各种故障，如何定位？
54. db和缓存一致性，常见的方案
55. Redis场景案例。。。
56. 系统资金账户设计案例（一些系统涉及到资金操作）
57. 其他等各种实战案例。。。