Springboot+Mysql+AOP的读写分离

Springboot+Mysql+AOP的读写分离

  • 相关依赖
<!--引入springboot的web支持-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- springboot+mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>

<!-- Aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- set get方法 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.8</version>
</dependency>

<!-- Junit测试 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!--SpringWeb Test-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--⽂件⽀持-->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

<!--集成Redis二级缓存-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- log4j -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.12</version>
</dependency>
  • application.yaml
#服务基本配置
server:
  port: 8888
  servlet:
    context-path: /
  #解决get乱码
  tomcat:
    uri-encoding: UTF-8
#数据源
spring:
  datasource:
    master:
      driver-class-name: com.mysql.jdbc.Driver
      jdbc-url: jdbc:mysql://10.15.0.10:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
      username: root
      password: root
    slave1:
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://10.15.0.11:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
        username: root
        password: root
    slave2:
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://10.15.0.12:3306/project?useUnicode=true&characterEncoding=UTF8&serverTimezone=UTC&useSSL=false
        username: root
        password: root

  #文件上传
  servlet:
    multipart:
      enabled: true
      max-file-size: 10MB
      location: C:/Users/YT/Desktop/uploadTmpDir
  #post表单提交乱码
  http:
    encoding:
      charset: utf-8
  #配置redis
  redis:
    host: hbase
    port: 6379
    timeout: 5s
    lettuce:
      shutdown-timeout: 100s
      pool:
        max-active: 10
        max-idle: 8
        max-wait: 5ms
        min-idle: 1

#配置mapper
mybatis:
  #开启Mybatis的二级缓存
  configuration:
    cache-enabled: true
  • UserDefinDatasourceConfig

自定义一个数据源配置类 交由工厂管理

@Configuration
public class UserDefinDatasourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.slave1")
    public DataSource slave1DataSource() {
        return DataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.slave2")
    public DataSource slave2DataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 当⾃定义数据源,⽤户必须覆盖SqlSessionFactory创建
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory
    sqlSessionFactory(@Qualifier("proxyDataSource") DataSource dataSource)
            throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        //实体类所在的包
        sqlSessionFactoryBean.setTypeAliasesPackage("com.baizhi.entity");
        //Mapper类所放置的位置
        sqlSessionFactoryBean.setMapperLocations(new
                PathMatchingResourcePatternResolver().getResources("classpath*:com/baizhi/mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    //中间件
    public DataSource proxyDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                      @Qualifier("slave1DataSource") DataSource slave1DataSource,
                                      @Qualifier("slave2DataSource") DataSource slave2DataSource){
        DataSourceProxy proxy = new DataSourceProxy();
        //设置默认数据源
        proxy.setDefaultTargetDataSource(masterDataSource);
        Map<Object, Object> map = new HashMap<>();
        map.put("master",masterDataSource);
        map.put("slave-01",slave1DataSource);
        map.put("slave-02",slave2DataSource);
        //注册所有数据源
        proxy.setTargetDataSources(map);
        return proxy;
    }
    /**
     * 当⾃定义数据源,⽤户必须覆盖SqlSessionTemplate,开启BATCH处理模式
     * @param sqlSessionFactory
     * @return
     */
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory")
                                                         SqlSessionFactory sqlSessionFactory) {
        //开启Mybatis的批处理模式
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);

    }

    /***
     * 当⾃定义数据源,⽤户必须注⼊,否则事务控制不⽣效
     * @param dataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager platformTransactionManager(@Qualifier("proxyDataSource")
                                       DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}
  • 定义一个中间类 DataSourceProxy

代理类 程序自动识别返回值是master 还是slave01和slave02 判断当前执行的业务是读操作还是写操作

public class DataSourceProxy extends AbstractRoutingDataSource {
    private static final Logger log = LoggerFactory.getLogger(DataSourceProxy.class);

    private String masterDBKey = "master";
    private List<String> slaveDBKey = Arrays.asList("slave-01","slave-02");
    //轮循
    private static final AtomicInteger round = new AtomicInteger(0);
    /***
     * 需要在该方法中,判断当前用户是读操作还是写操作
     * 如果是写操作@return master
     * 读操作则@return slaveDBKey
     */
    @Override
    //determineCurrentLookupKey()返回查询TargeDataSources的数据源 操作数据库
    protected Object determineCurrentLookupKey() {
        System.out.println("DataSource代理类---------------------");
        String dbKey = null;
        OperType operType = OperTypeContextHolder.getOperType();
        System.out.println(operType+"operType操作类型");
        //如果当前操作类型是写操作
        if(operType.equals(OperType.WRIRTE)){
            dbKey=masterDBKey;
            System.out.println(dbKey+"这里应该是写操作");
        }else{
            //轮循返回 0 1 2 3 4
            int value = round.getAndIncrement();
            if(value<0){
                round.set(0);
            }
            Integer index=round.get()%slaveDBKey.size();
            System.out.println(index+"------------------------------------------");
            dbKey=slaveDBKey.get(index);
            System.out.println(dbKey+"这里应该是读操作");
        }
        log.debug("当前DBKey:"+dbKey);
        return dbKey;
    }
}

此时,我们需要定义一个操作类型,方便代理类根据操作类型来判断读写操作等逻辑

  • 定义一个枚举类型

这里我们将定义一个枚举类型

/**
 * 枚举定义操作类型 写操作和读操作
 * */
public enum OperType {
    WRIRTE,READ;
}

到了这一步,我们需要通过线程 进行本地变量来传递操作类型

  • 通过线程本地变量传递操作类型

同一个线程返回同一个值

/**
 * 通过线程本地变量传递操作类型
 * */
public class OperTypeContextHolder {
    private static final ThreadLocal<OperType> OPER_TYPE_THREAD_LOCAL=new ThreadLocal<>();

    //存操作类型
    public static void setOperType(OperType operType){
        OPER_TYPE_THREAD_LOCAL.set(operType);
        System.out.println(operType+"操作类型上下文");
    }
    //取操作类型
    public static OperType getOperType(){
        return OPER_TYPE_THREAD_LOCAL.get();
    }
    //清除操作类型
    public static void clear(){
        OPER_TYPE_THREAD_LOCAL.remove();
    }
}
  • 定义一个注释类

用来区分出当前执行的业务是读操作还是写操作

/**
 * 定义注释
 * 标记当前业务方法是否是读操作
 * */
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD})
public @interface SlaveDB {
}

在userServiceImpl类中的读操作上添加此注解

@Service("UserService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDAO userDao;
    
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true)
@Override
@SlaveDB
public List<User> queryUserByPage(Integer pageNow, Integer pageSize, String column, Object value) {
    List<User> users = userDao.queryUserByPage(pageNow, pageSize, column, value);
    return users;
	}
}
  • AOP
//定义一个切面类
@Aspect
/**
 * 默认切面运行在事务之后
 * @Order(0)注解值越小优先级越高
 * */
@Order(0)//在事务之前运行切面
@Component//将普通类实例化到Spring容器中
public class ServiceMethodAOP {
    private static final Logger log = LoggerFactory.getLogger(ServiceMethodAOP.class);

    @Around("execution(* com.baizhi.service..*.*(..))")
    public Object methodInterceptor(ProceedingJoinPoint pjp){
        System.out.println("--------------AOP------------------");
        Object result = null;
        try {
            //获取当前的方法信息
            MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
            //获取方法对象
            Method method = methodSignature.getMethod();
            log.debug("方法名:"+method.getName());
            //判断方法是否存在注解@SlaveDB
            boolean present = method.isAnnotationPresent(SlaveDB.class);
            System.out.println(present+"注解是否为空");
            OperType operType = null;
            //如果是写方法
            if(!present){
                operType=OperType.WRIRTE;
            }else{
                //如果是读方法
                operType=OperType.READ;
            }
            OperTypeContextHolder.setOperType(operType);
            //当前操作类型
            log.debug("当前操作"+operType);
            //调用目标方法
            result = pjp.proceed();
            //清除线程变量
            OperTypeContextHolder.clear();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return result;
    }
}

注意: 切面类必须要在事务之前执行,默认是在之后执行,加上@Order(0)注解方可解决此问题

执行流程

① Spring工厂启动时,会将加有@Configuration注解的类交给工厂.在这个类里面,工厂会根据你的yml配置文件,加载所有的数据源.并成一个key-value 集合类型,以DataSource保存在JVM中.

② 当接口AbstractRoutingDataSource(抽象的路由选择数据源)的实现类返回一个key时,系统会根据这个key找到相应的数据源.系统就会连接这个数据源.

③ 程序执行到业务层时,会走AOP层,在执行业务之前,会获取当前操作的读写属性并存储到我们定义的状态持有者类中.环绕通知再调用proceed之前,要拿到连接(即数据源).

④ 这时,我们定义的接口实现类发挥作用,通过注解来判断操作类型是读操作还是写操作,将状态持有者中获取的操作状态,返回对应的key,系统自动根据key返回对应的数据源,执行业务.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值