前言
之前由于公司性质不同,没涉及到读写分离。刚好前一阵子现公司让我做一些读写分离相关的事 ,顺便就对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():