MySQL多数据源 二(基于spring+aop实现读写分离)

一,为什么要进行读写分离呢?

  因为数据库的“写操作”操作是比较耗时的(写上万条条数据到Mysql的可能要1分钟分钟)。但是数据库的“读操作”却比“写操作”耗时要少的多(从Mysql的读几万条数据条数据可能只要十秒钟),而我们在开发过程中大多数也是查询操作比较多。所以读写分离解决的是,数据库的“写操作”影响了查询的效率问题。

二,那么怎么来进行读写分离呢?

    首先,基于上一篇主从复制

   那么我们现在开始基于消费满+ AOP来实现读写分离

1:配置多数据源

      jdbc.properties文件如下:

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url_master=jdbc:mysql://localhost:3306/longlong_bike?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
jdbc.url_slaver1=jdbc:mysql://localhost:3306/longlong_bike?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
jdbc.url_slaver2=jdbc:mysql://localhost:3306/longlong_bike?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull

jdbc.username=root
jdbc.password=admin
 

2:的applicationContext文件如下:

<?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:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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-4.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">

    <!--打开注解-->
    <context:annotation-config/>

    <!--打开包扫描-->
    <context:component-scan base-package="com.coder520"/>

    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!--主数据库-->
    <bean id="dataSource_master" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url_master}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!--配置从数据库-->
    <bean id="dataSource_slaver1" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url_slaver1}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <bean id="dataSource_slaver2" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClassName}"/>
        <property name="url" value="${jdbc.url_slaver2}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!--自定义一个切换数据源的类DynamicDataSource(spring提供的有切换动态数据库的类AbstractRoutingDataSource)-->
    <bean id="dataSource" class="com.coder520.common.DynamicDataSource">
        <!--定义目标数据源对象,对应AbstractRoutingDataSource类中的map对象-->
        <property name="targetDataSources">
            <map>
                <entry key="master" value-ref="dataSource_master"/>
                <entry key="slaver1" value-ref="dataSource_slaver1"/>
                <entry key="slaver2" value-ref="dataSource_slaver2"/>
            </map>
        </property>
        <!--定义默认的数据源对象-->
        <property name="defaultTargetDataSource" ref="dataSource_master"/>
    </bean>



    <!--配置mybatis sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--配置数据库路径-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置sql-mapper路径-->
        <property name="mapperLocations" value="classpath:com/coder520/**/**.xml"/>
    </bean>
    <!--配置mapper-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.coder520"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

    <!--配置事务-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

配置了一主两从三个数据源,在弹簧中,给我们提供了可以切换数据源的类(AbstractRoutingDataSource)。它是一个抽象类,我们需要实现它的抽象方法。

3:自定义一个动态切换数据源的类DynamicDataSource,实现AbstractRoutingDataSource的抽象方法。代码如下:

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

public class DynamicDataSource extends AbstractRoutingDataSource {

    // 获取当前map对象的key,这里我们在定义一个类(DynamicDataSourceHolder)来实现,当然你也可以写在本类中
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDataSource();
    }
}
DynamicDataSourceHolder.getDataSource()是获取数据源。但是呢,spring中的数据源是唯一,每一个用户过来都是共用这个数据源的。我们知道高并发的情况下,多个用户共享一个资源,这是有线程问题的,这样获取数据源是不安全的,容易混乱。

因此我们要用到并发编程问题呢,我们要用到并发编程里面的一个类ThreadLocal这个类,这个类用来ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。


那么我们在两个从库中进行读操作如何公平的分配来读操作呢?我们自然想到要有轮询的思维。通过一个计时器来自增求模运算。这个计时器的只从-1开始,这样得到的结果就只有0和1了,根据0 和 1来分配两个从库进行读操作。
注意这个计时器如果用Inter类型的话,必然会出现线程安全问题的,因为这是共享的数据类型。因此我们可以用并发编程里面的AtomicInterger原子属性的类。解决线程安全问题。我们知道Integer是有范围的,我们不能让
这个计数器一直自增,这样下去会去问题的。因此还需要来一个计数器重置。

代码如下:

 

package com.coder520.common;

import java.util.concurrent.atomic.AtomicInteger;

public class DynamicDataSourceHolder {

    /**
     *  频换的切换数据源,在多线程下是非常可怕的,容易造成数据混乱,所以自然而然的就想到了并发编程的ThreadLocal类
     */
    // 定义一个ThreadLocal类来捆绑当前map的key
    private static final ThreadLocal<String> holder = new ThreadLocal<>();

    // 定义数据源的Key(主)
    private static final String MASTER = "master";

    // 定义数据源的Key(从)
    private static final String SLAVER_1 = "slaver1";
    private static final String SLAVER_2 = "slaver2";

    /**
     *  由于我们设置了两个从库,从库之前的切换我们很容易就想到了轮训算法,为了让轮训轮换的彻底,定义一个累加数
     *  在多线程情况下,这个累加数可能会出现问题,所以我就想到了用Atomic包的下类
     */
    // 定义一个从-1开始的计数器
    private static final AtomicInteger count = new AtomicInteger(-1);


    /**
     *  为了让代码变得更加美观,可以使用枚举来定义主库从库常量
     */
    // 01:设置数据源
    public static void setDataSource(DataSourceType dataSource){
        if(DataSourceType.MASTER == dataSource){
            // 输出一下,判断当前的数据库
            System.out.println("当前数据库:----------> master");
            holder.set(MASTER);
        }else if(DataSourceType.SLAVER == dataSource){
            // 轮训从数据库
            holder.set(roundRobin());
        }
    }

    private static String roundRobin() {
        int index = count.incrementAndGet();

        if(Integer.MAX_VALUE < index){
            index = -1;
        }

        if(index%2 == 0){
            System.out.println("当前数据库:----------> slaver_1");
            return SLAVER_1;
        }else{
            System.out.println("当前数据库:----------> slaver_2");
            return SLAVER_2;
        }
    }



    // 02:获取数据源
    public static String getDataSource(){
        return holder.get();
    }
}

这样,一个基本的基于弹簧的读写分离就完成了,不过需要在每一个服务方法中加上

DynamicDataSourceHolder.setDataSource(DataSourceType.SLAVER);

DynamicDataSourceHolder.setDataSource(DataSourceType.MASTER);

这样的方法,但我们为了避免写重复的代码,可以申请一个切面,让切面编程帮我们完成动态数据源的切换。

这里是控制器和服务的代码和运行结果:

  控制层:

@Controller
@RequestMapping("user")
public class UserController {

    @Autowired
    @Qualifier("userServiceImp")
    private UserService userService;

    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(){
        return userService.getUser(1l);
    }


    @RequestMapping("/addUser")
    @ResponseBody
    public User addUser(){
        User user = new User();
        user.setNickname("子龙");
        user.setMobile("18637685918");
        userService.addUser(user);
        return user;
    }
}

服务层:

@Service("userServiceImp")
public class UserServiceImp implements UserService {

    @Autowired
    private UserMapper userMapper;


    @Override
//    @DataSource(DataSourceType.SLAVE)
    public User getUser(Long id) {
        DynamicDataSourceHolder.setDataSource(DataSourceType.SLAVER);
        return userMapper.selectByPrimaryKey(id);
    }

    @Override
    @Transactional
//    @DataSource(DataSourceType.MASTER)
    public void addUser(User user) {
        DynamicDataSourceHolder.setDataSource(DataSourceType.MASTER);

        userMapper.insertSelective(user);

        User u = new User();
        user.setId(1l);
        userMapper.insertSelective(u);
    }
}

运行结果:

  访问的getUser方法:

 访问ADDUSER方法:

这里,可以看出来我们的动态数据源切换已经完成了。

我们发现在每一个方法上都加上这句代码,太难受了。怎么办呢? 

下面改进程序,用切面加注解的方法来完成:

  创建一个注解 

//在运行时生效
@Retention(RetentionPolicy.RUNTIME)
//注解的作用范围(作用在方法上)
@Target({ElementType.METHOD})
public @interface DataSource {

    DataSourceType value() default DataSourceType.MASTER;
}

定义一个切面

   首先在的applicationContext文件下,打开切面。

  定义切面

@Aspect
@Component
public class DataSourceAspect {

    @Pointcut(value = "execution(* com.coder520.service.*.*(..))")
    public void pointCut(){}

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) throws NoSuchMethodException {
        // 01:通过连接点获取切点对象
        Object target = joinPoint.getTarget();

        // 02:通过连接点获取切点名称
        String name = joinPoint.getSignature().getName();

        // 03:获取切点的字节码对象,为了通过反射获取到切点切的方法
        Class clazz = target.getClass();

        // 04:获取参数类型
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();

        // 05:获取当前切点所切的方法
        Method method = clazz.getMethod(name,parameterTypes);

        // 06: 判断是否含有DataSource注解
        if(method != null && method.isAnnotationPresent(DataSource.class)){
            // 07:获取注解的值
            DataSource annotation = method.getAnnotation(DataSource.class);

            // 08:设置数据源
            DynamicDataSourceHolder.setDataSource(annotation.value());
        }
    }
}

在通知方法上出现这个标记就说明切面配置成功

服务层代码:

@Service("userServiceImp")
public class UserServiceImp implements UserService {

    @Autowired
    private UserMapper userMapper;


    @Override
    @DataSource(DataSourceType.SLAVER)
    public User getUser(Long id) {
//        DynamicDataSourceHolder.setDataSource(DataSourceType.SLAVER);
        return userMapper.selectByPrimaryKey(id);
    }

    @Override
    @Transactional
    @DataSource(DataSourceType.MASTER)
    public void addUser(User user) {
//        DynamicDataSourceHolder.setDataSource(DataSourceType.MASTER);

        userMapper.insertSelective(user);

        User u = new User();
        user.setId(1l);
        userMapper.insertSelective(u);
    }
}

直接打上注解就可以了

运行结果:

      

读写分离可能导致的问题:

    一:数据不一致

   在高并发的情况下,可能会导致数据不同步,具体原因是:我刚插入了一条数据就开始查看该数据,在这种情况下,从库可能还在加载二进制日志,没能同步到从库上,这个时候查询就会导致了数据不同步。

    处理方法:

         1:强一致性:任何一次读都能读取到某个数据最近的一次修改

                  在表中添加一个标记,如果主从同步还没能完成,就强行让它访问主数据库。不推荐,我们主从同步,读写分离的目的就是为了减轻主数据库的压力,这样做的话,还会加重主数据库的压力。

        2:弱一致性:数据更新后,能容忍后续的访问,但是只能访问部分数据或访问不到数据。

                  推荐使用弱一致性,我们可以先把数据写在缓存中,设置一个过期时间,当缓存过期后,主数据库从数据库也已经同步完成。但是在特大并发的情况下,缓存也兜不住的。这样我们也没办法,只能牺牲用户的体验,让他更新数据后,延迟几秒才能访问。

     二:修改查询一起操作时

       这种情况下,我们只能放弃读写分离了。

猜你喜欢

转载自blog.csdn.net/qq_36957587/article/details/84534426