Mysql 之基于SpringBoot实现读写分离

AbstractRoutingDataSource动态数据源切换

在这里插入图片描述

快速构建SpringMVC项目

项目结构:
在这里插入图片描述
web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         id="WebApp_ID" version="2.5">
    <display-name>spingmvc</display-name>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring-basic.xml</param-value>
    </context-param>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!-- 配置SpringMVC框架入口 -->
    <servlet>
        <servlet-name>spring-mvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <!--
            tomcat启动时完成初始化
            不配置,在第一次请求后完成初始化
         -->
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>spring-mvc</servlet-name>
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>

</web-app>

在resource目录下创建配置文件:log4j.properties、spring-mvc.xml、spring-basic.xml、mybatis-configuration.xml。
log4j.properties:

log4j.rootLogger=DEBUG,A1
log4j.logger.org.mybatis = DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-[%p] %m%n

spring-mvc.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <mvc:annotation-driven />
    <context:component-scan base-package="com.mvc.controller">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    </context:component-scan>
</beans>

spring-basic.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:annotation-config/>
    <context:component-scan base-package="com.mvc">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    </context:component-scan>
</beans>

控制器:

import com.mvc.entity.User;
import com.mvc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {

    @Autowired
    UserService userService;

    @RequestMapping("/demo")
    @ResponseBody
    public String demo() {
        User user = userService.demo();
        return user.toString();
    }
}

sql脚本:

CREATE TABLE `user`  (
  `id` int(255) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

编写AbstractRoutingDataSource动态数据源切换

编写设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前:

// 根据当前线程来选择具体的数据源
public class HandlerDataSource {

    public static final ThreadLocal<String> handlerThreadLocal = new ThreadLocal<String>();

    // 提供给AOP去设置当前的线程的数据源的信息
    public static void putDataSource(String datasource) {
        handlerThreadLocal.set(datasource);
    }  

    // 提供给AbstractRoutingDataSource的实现类,通过key选择数据源
    public static String getDataSource() {  
        return handlerThreadLocal.get();
    }  

    // 使用默认的数据源
    public static void clean() {
        handlerThreadLocal.remove();
	}
} 

编写AbstractRoutingDataSource的实现类,HandlerDataSource就是提供给我们动态选择数据源的数据的信息,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

// 多数据源的选择实现类
public class MultipleDataSourceToChoose extends AbstractRoutingDataSource {

    // 根据Key获取数据源的信息,上层抽象函数的钩子
    @Override
    protected Object determineCurrentLookupKey() {
        return HandlerDataSource.getDataSource();
    }
}

编写设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上,value是当前数据源的一个别名用于标识我们的数据源的信息:

import java.lang.annotation.*;

// 创建拦截设置数据源的注解
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicSwitchDataSource {
    String value();
}

AOP拦截类的实现,通过拦截上面的注解,在其执行之前处理设置当前执行SQL的数据源的信息,HandlerDataSource.putDataSource(….),这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息:

import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

// 使用AOP拦截特定的注解去动态的切换数据源
@Aspect
@Component
@Order(1)
public class HandlerDataSourceAop {

    private static Logger logger = Logger.getLogger(HandlerDataSourceAop.class);

    // @within在类上设置
    // @annotation在方法上进行设置
    @Pointcut("@within(com.mvc.datasource.DynamicSwitchDataSource)||@annotation(com.mvc.datasource.DynamicSwitchDataSource)")
    public void pointcut() {}

    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
        DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//获取方法上的注解
        if(annotationClass == null){
            annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//获取类上面的注解
            if(annotationClass == null) return;
        }
        //获取注解上的数据源的值的信息
        String dataSourceKey = annotationClass.value();
        if(dataSourceKey !=null){
            //给当前的执行SQL的操作设置特殊的数据源的信息
            HandlerDataSource.putDataSource(dataSourceKey);
        }
        logger.info("AOP动态切换数据源,className"+joinPoint.getTarget().getClass().getName()+"methodName"+method.getName()+";dataSourceKey:"+dataSourceKey==""?"默认数据源":dataSourceKey);
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉当前设置的数据源,让默认的数据源不受影响
        HandlerDataSource.clean();
    }
}

spring-mvc.xml、spring-basic.xml分别配置aop的自动代理:
如果不在spring-mvc.xml配置中加入aop扫描,切面只能被父容器装载,子容器是看不到的。

<!-- 配置自动为Spring容器中那些配置@aspectJ切面的bean创建代理 -->
<!-- proxy-target-class默认"false",更改为"true"使用CGLib动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="true" />

配置两个数据源:

<bean id="readDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/read" />
    <property name="username" value="root" />
    <property name="password" value="123456" />
    <property name="maximumPoolSize" value="10" />
    <property name="minimumIdle" value="10" />
    <!-- SQL查询,用来验证从连接池取出的连接 -->
    <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
    <!-- 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒 -->
    <property name="connectionTimeout" value="6000" />
</bean>

<bean id="writeDataSource" class="com.zaxxer.hikari.HikariDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/write" />
    <property name="username" value="root" />
    <property name="password" value="123456" />
    <property name="maximumPoolSize" value="10" />
    <property name="minimumIdle" value="10" />
    <!-- SQL查询,用来验证从连接池取出的连接 -->
    <property name="connectionTestQuery" value="SELECT 1 FROM DUAL"/>
    <!-- 等待连接池分配连接的最大时长(毫秒),超过这个时长还没可用的连接则发生SQLException, 缺省:30秒 -->
    <property name="connectionTimeout" value="6000" />
</bean>

配置之前我们实现的数据源选择的中间层AbstractRoutingDataSource的实现类,这里的key就是数据源信息的别名,通过这个key可以选择到数据源的信息。MultipleDataSourceToChoose就是上面写的数据源选择器的实现类:

<!-- 配置默认数据源选择器-->
<bean id="dataSource" class="com.mvc.datasource.MultipleDataSourceToChoose" lazy-init="true">
    <description>数据源</description>
    <property name="targetDataSources">
        <!-- 选择数据源信息 -->
        <map key-type="java.lang.String" value-type="javax.sql.DataSource">
            <entry key="readDataSource" value-ref="readDataSource" />
            <entry key="writeDataSource" value-ref="writeDataSource" />
        </map>
    </property>
    <!-- 设置默认的目标数据源 -->
    <property name="defaultTargetDataSource" ref="readDataSource" />
</bean>

配置SessionFactory:

<!-- 配置SessionFactory -->
<bean id="sqlSessionFactory"
      class="org.mybatis.spring.SqlSessionFactoryBean"
      p:dataSource-ref="dataSource"
      p:configLocation="classpath:mybatis-configuration.xml"
      p:mapperLocations="classpath:mapper/*.xml"/><!-- configLocation为mybatis属性 mapperLocations为所有mapper-->

<!-- spring与mybatis整合配置,扫描所有dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"
    p:basePackage="com.mvc.mapper"
    p:sqlSessionFactoryBeanName="sqlSessionFactory"/>

编写访问数据库相关代码:
mybatis-configuration.xml:

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
	<settings>
		<setting name="logImpl" value="LOG4J"/>
		<!-- 全局映射器启用缓存 -->
		<setting name="cacheEnabled" value="true" />
		<!-- 查询时,关闭关联对象即时加载以提高性能 -->
		<setting name="lazyLoadingEnabled" value="true" />
		<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
		<setting name="aggressiveLazyLoading" value="false" />
		<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
		<setting name="multipleResultSetsEnabled" value="true" />
		<!-- 允许使用列标签代替列名 -->
		<setting name="useColumnLabel" value="true" />
		<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
		<!-- <setting name="useGeneratedKeys" value="true" /> -->
		<!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
		<setting name="autoMappingBehavior" value="FULL" />
		<!-- 对于批量更新操作缓存SQL以提高性能 -->
		<!-- 
		<setting name="defaultExecutorType" value="BATCH" /> -->
		<!-- 数据库超过25000秒仍未响应则超时 -->
		<setting name="defaultStatementTimeout" value="2000" />
	</settings>
	<!-- 全局别名设置,在映射文件中只需写别名,而不必写出整个类路径 -->
	 <typeAliases>
         <typeAlias alias="User" type="com.mvc.entity.User" />
     </typeAliases>

</configuration>  

UserMapper.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.mvc.mapper.UserMapper">
    <resultMap id="BaseResultMap" type="com.mvc.entity.User">
        <id column="id" property="id" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
    </resultMap>
    <sql id="Base_Column_List">
    id, name
  </sql>
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer">
        select
        <include refid="Base_Column_List"/>
        from user
        where id = #{id,jdbcType=INTEGER}
    </select>
</mapper>

User:

public class User {
    private Integer id;
    private String name;
    // 省略getter、setter、toString方法
}

UserMapper.java:

public interface UserMapper {
    User selectByPrimaryKey(Integer id);
}

Service接口和实现类:

public interface IUserService {
    User demo();
}

// 实现类
import com.mvc.datasource.DynamicSwitchDataSource;
import com.mvc.entity.User;
import com.mvc.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService implements IUserService{

    @Autowired(required = false)
    private UserMapper userMapper;

    @Override
    @DynamicSwitchDataSource("readDataSource")
    public User demo(){
        return userMapper.selectByPrimaryKey(1);
    }
}

最后启动测试。

实现原理

在这里插入图片描述
MultipleDataSourceToChoose的继承结构图,是DataSource的子类,由于无论我们是使用Mybatis还是使用Hibernate进行SQL操作的时候总会执行getConnection(),无论我们的数据源是否使用了数据库连接池,因为数据库连接池的主要作用就是保持一堆的Connection不进行关闭的处理,节省我们的关闭和打开连接的开销。Connection getConnection() throws SQLException;所以这句话总是要执行的,只是AbstractRoutingDataSource这个类给我们进行了一些中间的处理,在获取Connection的时候会去寻找保存的DataSource的引用,到底是选择哪个DataSource进行处理。

下面是代码分析:
在这里插入图片描述
targetDataSources,是一个Map对于数据源的引用:
在这里插入图片描述
对于实现SQL的Connection getConnection() throws SQLException的实现,其实就是代理模式找到之前Map的引用,通过key,而这个key就是我们灵活配置的key,通过这个key就可以寻找到这个值:
在这里插入图片描述
这里说的非常的详细,通过钩子函数让子类去实现,寻找特定的key,然后选择DataSource 的时候就可以很灵活的使用了:
在这里插入图片描述

当当网的sharding-jdbc分库分表技术

配置分库分表规则:

<!-- 配置分表规则器,sharding-columns:分表规则依赖的名(根据onlyid取模分表),algorithm-expression:分表规则实现表达式 -->
<rdb:strategy id="historySharding" sharding-columns="onlyid" algorithm-expression="history_${onlyid.longValue()%3}"/>

<rdb:data-source id="readShardingDataSource">
    <!-- 这里填写关联数据源(多个数据源用逗号隔开) -->
    <rdb:sharding-rule data-sources="readDataSource">
        <rdb:table-rules>
            <!--  logic-table:逻辑表名-->
            <!-- actual-tables:数据库实际的表名,这里支持inline表达式,比如:history_${0..2}会解析成history_0,history_1,history_2-->
            <rdb:table-rule logic-table="history" actual-tables="history_${0..2}" table-strategy="historySharding"/>
        </rdb:table-rules>
        <rdb:default-database-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.NoneDatabaseShardingAlgorithm"/>
        <rdb:default-table-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.table.NoneTableShardingAlgorithm"/>
    </rdb:sharding-rule>
</rdb:data-source>

<rdb:data-source id="writeShardingDataSource">
    <rdb:sharding-rule data-sources="writeDataSource">
        <rdb:table-rules>
            <rdb:table-rule logic-table="history" actual-tables="history_${0..2}" table-strategy="historySharding"/>
        </rdb:table-rules>
        <rdb:default-database-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.database.NoneDatabaseShardingAlgorithm"/>
        <rdb:default-table-strategy sharding-columns="none" algorithm-class="com.dangdang.ddframe.rdb.sharding.api.strategy.table.NoneTableShardingAlgorithm"/>
    </rdb:sharding-rule>
</rdb:data-source>

修改数据源指向rdb配置的分库分表数据源定义:

<!-- 配置默认数据源选择器-->
<bean id="dataSource" class="com.mvc.datasource.MultipleDataSourceToChoose" lazy-init="true">
    <description>数据源</description>
    <property name="targetDataSources">
        <!-- 选择数据源信息 -->
        <map key-type="java.lang.String" value-type="javax.sql.DataSource">
            <entry key="readDataSource" value-ref="readShardingDataSource" />
            <entry key="writeDataSource" value-ref="writeShardingDataSource" />
        </map>
    </property>
    <!-- 设置默认的目标数据源 -->
    <property name="defaultTargetDataSource" ref="readShardingDataSource" />
</bean>

sql脚本:

CREATE TABLE `history`  (
  `id` bigint(255) NOT NULL AUTO_INCREMENT,
  `onlyid` bigint(255) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SpringAOP拦截Controller和Service问题

spring配置:

<context:component-scan base-package="com.yq" >
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<aop:aspectj-autoproxy />

spring-mvc配置:

<context:component-scan base-package="com.yq" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<aop:aspectj-autoproxy />

切面类:

@Component
@Aspect
public class LogAspect {
    @Pointcut("execution(* com.yq.service..*(..))")
    private void servicePointCut() {}
    @Pointcut("execution(* com.yq.controller..*(..))")
    private void controllerPointCut() {}
    
    @Around("servicePointCut()")
    public Object logAroundService(ProceedingJoinPoint jointPoint) throws Throwable {
        System.out.println(jointPoint+"开始");
        Object proceed = jointPoint.proceed();
        System.out.println(jointPoint+"结束");
        return proceed;
    }

    @Around("controllerPointCut()")
    public Object logAroundController(ProceedingJoinPoint jointPoint) throws Throwable {
        System.out.println(jointPoint+"开始");
        Object proceed = jointPoint.proceed();
        System.out.println(jointPoint+"结束");
        return proceed;
    }
}

注意:

  • controller需要使用cglib动态代理才可以拦截,高版本spring可以自动选择jdk或者cglib代理,在低版本中aop:aspectj-autoproxy必须加上proxy-target-class="true"才能指定使用cglib代理。
  • aop切面类LogAspect必须与目标类在同一个上下文环境。因为切面类LogAspect要同时切入controller和service,而我的spring上下文环境不包含controller,spring-mvc上下文环境不包含service,所以需要在spring和spring-mvc配置文件中都配置<aop:aspectj-autoproxy />

参考

基于springboot的mysql实现读写分离
AbstractRoutingDataSource动态数据源切换,AOP实现动态数据源切换
sharding-jdbc结合mybatis实现分库分表功能
spring aop拦截controller和service
spring boot使用AbstractRoutingDataSource实现动态数据源切换
@Transactional导致AbstractRoutingDataSource动态数据源无法切换的解决方法
Spring Boot + Mybatis 实现动态数据源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值