spring+mybatis数据库读写分离

前言

之前由于公司性质不同,没涉及到读写分离。刚好前一阵子现公司让我做一些读写分离相关的事 ,顺便就对spring+mybatis读写分离学习了一下。

配置+代码编写

  本片随笔是在mybatis学习日记-day01的基础上来完成。

  要做读写分离需要先对spring的数据源配置做修改:

  <!-- 配置主库数据源 -->
    <bean id="masterDataSource"
          class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName">
            <value>${jdbc_driverClassName}</value>
        </property>
        <property name="url">
            <value>${master_url}</value>
        </property>
        <property name="username">
            <value>${master_username}</value>
        </property>
        <property name="password">
            <value>${master_password}</value>
        </property>
        <!-- 连接池启动时创建的初始化连接数量(默认值为0) -->
        <property name="initialSize" value="1"/>
        <!-- 连接池中可同时连接的最大的连接数 默认 8-->
        <property name="maxActive" value="150"/>
        <!-- 连接池中最小的空闲的连接数,低于这个数量会被创建新的连接 -->
        <property name="minIdle" value="5"/>
        <!-- 连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限制 默认8 -->
        <property name="maxIdle" value="30"/>
        <!-- 最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待 -->
        <property name="maxWait" value="60000"/>
        <!-- 超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收(默认为false,调整为true)-->
        <property name="removeAbandoned" value="true"/>
        <!-- 超过时间限制,回收没有用(废弃)的连接(默认为 300秒,调整为180) -->
        <property name="removeAbandonedTimeout" value="180"/>
        <!-- 默认提交 -->
        <property name="defaultAutoCommit" value="true"/>
    </bean>
  <!--配置从库数据源>
    <bean id="slaveDataSource"
          class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName">
            <value>${jdbc_driverClassName}</value>
        </property>
        <property name="url">
            <value>${slave_url}</value>
        </property>
        <property name="username">
            <value>${slave_username}</value>
        </property>
        <property name="password">
            <value>${slave_password}</value>
        </property>
        <!-- 连接池启动时创建的初始化连接数量(默认值为0) -->
        <property name="initialSize" value="1"/>
        <!-- 连接池中可同时连接的最大的连接数 默认 8-->
        <property name="maxActive" value="150"/>
        <!-- 连接池中最小的空闲的连接数,低于这个数量会被创建新的连接 -->
        <property name="minIdle" value="5"/>
        <!-- 连接池中最大的空闲的连接数,超过的空闲连接将被释放,如果设置为负数表示不限制 默认8 -->
        <property name="maxIdle" value="30"/>
        <!-- 最大等待时间,当没有可用连接时,连接池等待连接释放的最大时间,超过该时间限制会抛出异常,如果设置-1表示无限等待 -->
        <property name="maxWait" value="60000"/>
        <!-- 超过removeAbandonedTimeout时间后,是否进 行没用连接(废弃)的回收(默认为false,调整为true)-->
        <property name="removeAbandoned" value="true"/>
        <!-- 超过时间限制,回收没有用(废弃)的连接(默认为 300秒,调整为180) -->
        <property name="removeAbandonedTimeout" value="180"/>
        <!-- 默认提交 -->
        <property name="defaultAutoCommit" value="true"/>
    </bean>

配置了主从数据源之后spring需要我们自己定义一个数据源切换类,在spring配置文件中配置该类的bean:

<bean id="dataSource" class="com.jarry.datasource.DynamicDataSource">
<property name="defaultDataSource" value="SLAVE" /> <property name="writeDataSource" ref="masterDataSource" /> <property name="readDataSources"> <list> <ref bean="slaveDataSource" /> <!-- 可设置多个从库 --> </list> </property> <!--轮询方式--> <property name="readDataSourcePollPattern" value="1" /> <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean>
<!--设置aspectj自动代理,其实就是基于注解来定义aop类-->
<aop:aspectj-autoproxy/>

其中DynamicDataSource.java即为自定义的类,该类需要继承AbstractRoutingDataSource.java,这个类后面再详细说明。

在这里,我想用注解的方式(毕竟方便和自由)来动态切换主从数据源

什么是动态切换数据源?

我所理解的动态切换数据源就是在service层开启事务之前先进入某个切面进行代码编织,编织的代码内容就是切换数据源的过程。

  所以我需要自定义一个注解@DataSource,我需要让该注解加到service层,当检测到某个方法被注解之后,进入自定义切面,来根据注解的具体内容完成数据源的切换。我的注解类定义如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 数据源注解
 *
 * @author xujian
 * @create 2018-04-27 19:54
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    DynamicDataSourceGlobal value() default DynamicDataSourceGlobal.MASTER;//默认为主数据源
}
DynamicDataSourceGlobal该类是我定义的一个枚举类,包含两个实例MASTER(主数据源)、SLAVE(从数据源)。
下面来看看切面类DynamicDataSourceAspect的定义:
 1 import org.aspectj.lang.JoinPoint;
 2 import org.aspectj.lang.annotation.After;
 3 import org.aspectj.lang.annotation.Aspect;
 4 import org.aspectj.lang.annotation.Before;
 5 import org.aspectj.lang.annotation.Pointcut;
 6 import org.aspectj.lang.reflect.MethodSignature;
 7 import org.slf4j.Logger;
 8 import org.slf4j.LoggerFactory;
 9 import org.springframework.stereotype.Component;
10 
11 import java.lang.reflect.Method;
12 
13 /**
14  * 定义选择数据源切面
15  */
16 @Aspect
17 @Component
18 public class DynamicDataSourceAspect {
19 
20     private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
21 
22     @Pointcut("execution(* com.jarry.service.impl.*.*(..))")
23     public void pointCut(){};
24 
25     @Before("pointCut()")
26     public void before(JoinPoint point)
27     {
28         Object target = point.getTarget();//获取被代理类
29         String methodName = point.getSignature().getName();//获取当前方法签名的名称
30         Class<?>[] clazz = target.getClass().getInterfaces();//获取被代理类实现的接口
31         Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();//获取当前方法的参数类型
32         try {
33             Method method = clazz[0].getMethod(methodName, parameterTypes);//根据参数类型和方法名称找第一个实现接口中对应的方法
34             if (method != null && method.isAnnotationPresent(DataSource.class)) {//如果方法存在并且被DataSource注解
35                 DataSource data = method.getAnnotation(DataSource.class);
36                 DynamicDataSourceHolder.putDataSource(data.value());//把注解的值放入线程变量,以备后面取用
37             }
38         } catch (Exception e) {
39             logger.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, e.getMessage()));
40         }
41     }
42 
43     @After("pointCut()")
44     public void after(JoinPoint point) {
45         DynamicDataSourceHolder.clearDataSource();
46     }
47 }
扫描二维码关注公众号,回复: 84913 查看本文章
可以看到上面代码中涉及到DynamicDataSourceHolder类:
 1 /**
 2  * 本地线程设置和获取数据源信息
 3  */
 4 public class DynamicDataSourceHolder {
 5 
 6     private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();//为每一个访问该变量的线程创建一个变量副本,各线程该变量互不影响
 7 
 8     public static void putDataSource(DynamicDataSourceGlobal dataSource){
 9         holder.set(dataSource);
10     }
11 
12     public static DynamicDataSourceGlobal getDataSource(){
13         return holder.get();
14     }
15 
16     public static void clearDataSource() {
17         holder.remove();
18     }
19 
20 }

最后,压轴的来了:

DynamicDataSource

  1 import org.slf4j.Logger;
  2 import org.slf4j.LoggerFactory;
  3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  4 
  5 import java.util.HashMap;
  6 import java.util.List;
  7 import java.util.Map;
  8 import java.util.concurrent.ThreadLocalRandom;
  9 import java.util.concurrent.atomic.AtomicLong;
 10 import java.util.concurrent.locks.Lock;
 11 import java.util.concurrent.locks.ReentrantLock;
 12 
 13 /**
 14  * 动态数据源切换
 15  *该类通过xml进行初始化,继承了spring的数据源路由基类
 16  * @author xujian
 17  * @create 2018-04-27 18:17
 18  **/
 19 public class DynamicDataSource extends AbstractRoutingDataSource {
 20     private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
 21 
 22     private String defaultDataSource = DynamicDataSourceGlobal.SLAVE.name(); // 默认读数据源
 23 
 24     private Object writeDataSource; // 写数据源
 25 
 26     private List<Object> readDataSources; // 多个读数据源
 27 
 28     private int readDataSourceSize; // 读数据源个数
 29 
 30     private int readDataSourcePollPattern = 0; // 获取读数据源方式,0:随机,1:轮询
 31 
 32     private AtomicLong counter = new AtomicLong(0);
 33 
 34     private static final Long MAX_POOL = Long.MAX_VALUE;
 35 
 36     private final Lock lock = new ReentrantLock();
 37 
 38     @Override
 39     public void afterPropertiesSet() {//自定义属性注入之后调用的方法
 40         if (this.writeDataSource == null) {
 41             throw new IllegalArgumentException("Property 'writeDataSource' is required");
 42         }
 43         setDefaultTargetDataSource(writeDataSource);//设置默认数据源
 44         Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
 45         targetDataSources.put(DynamicDataSourceGlobal.MASTER.name(), writeDataSource);
 46         if (this.readDataSources == null) {
 47             readDataSourceSize = 0;
 48         } else {
 49             for(int i=0; i<readDataSources.size(); i++) {
 50                 targetDataSources.put(DynamicDataSourceGlobal.SLAVE.name() + i, readDataSources.get(i));
 51             }
 52             readDataSourceSize = readDataSources.size();
 53         }
 54         setTargetDataSources(targetDataSources);//设置目标数据源(多个):在spring中注册所有数据源,
 55         // 数据源以key(数据源名称:MASTER/SLAVE)-value(masterDataSource/slaveDataSource)存在
 56         super.afterPropertiesSet();//自定义操作完成以后调用基类的同名方法
 57     }
 58 
 59     @Override
 60     protected Object determineCurrentLookupKey() {//判断返回当前应该使用那个数据源
 61 
 62         DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
 63 
 64         if (dynamicDataSourceGlobal != null && dynamicDataSourceGlobal == DynamicDataSourceGlobal.MASTER){
 65             logger.info("当前使用数据源:"+DynamicDataSourceGlobal.MASTER.name());
 66             return DynamicDataSourceGlobal.MASTER.name();
 67         }
 68         if (readDataSourceSize <= 0){
 69             logger.info("当前使用数据源:"+DynamicDataSourceGlobal.MASTER.name());
 70             return DynamicDataSourceGlobal.MASTER.name();
 71         }
 72 
 73         int index = 1;
 74 
 75         if (readDataSourcePollPattern == 1) {
 76             //轮询方式
 77             long currValue = counter.incrementAndGet();
 78             if ((currValue + 1) >= MAX_POOL) {//防止越界
 79                 try {
 80                     lock.lock();
 81                     if ((currValue + 1) >= MAX_POOL) {
 82                         counter.set(0);
 83                     }
 84                 } finally {
 85                     lock.unlock();
 86                 }
 87             }
 88             index = (int) (currValue % readDataSourceSize);
 89         } else {
 90             //随机方式选择一个从数据源
 91             index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
 92         }
 93         logger.info("当前使用数据源:"+DynamicDataSourceGlobal.SLAVE.name() + index);
 94         return DynamicDataSourceGlobal.SLAVE.name() + index;
 95     }
 96 
 97     public void setWriteDataSource(Object writeDataSource) {
 98         this.writeDataSource = writeDataSource;
 99     }
100 
101     public void setReadDataSources(List<Object> readDataSources) {
102         this.readDataSources = readDataSources;
103     }
104 
105     public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
106         this.readDataSourcePollPattern = readDataSourcePollPattern;
107     }
108 
109     public String getDefaultDataSource() {
110         return defaultDataSource;
111     }
112 
113     public void setDefaultDataSource(String defaultDataSource) {
114         this.defaultDataSource = defaultDataSource;
115     }
116 }

测试

单元测试

    @Test
    public void testGetUserByUserName(){
        User u = studentService.getUserByUserName("songqiaojun");
        System.out.println("-----------["+u.toString()+"]--------------");
    }
    
    @Test
    public void testSaveUser(){
        User u = new User();
        u.setUserName("pengxin03");
        u.setName("彭鑫");
        studentService.addUser(u);
    }
public interface ... { 
User getUserByUserName(String userName);//调用该方法应该切换为从库(读库) @DataSource
void addUser(User user);//该方法签名加了注解@DataSource,应该走主库(写库)
}

测试结果

testGetUserByUserName():

public void testSaveUser():

猜你喜欢

转载自www.cnblogs.com/xuxiaojian/p/8796914.html
今日推荐