客户端对数据库的访问实现读写分离,从主机上写入数据,从机上读取数据,可以减轻数据库的压力。
实现对主从机数据库的读写分离,源码地址 https://github.com/CaesarLinsa/SpringTest/tree/master
1.配置多数据源,并注入到sqlSessionFactoryBean中,配置见下:
<!--1. 写数据源配置 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="${jdbc.initialSize}"></property> <!-- 连接池最大数量 --> <property name="maxActive" value="${jdbc.maxActive}"></property> <!-- 连接池最大空闲 --> <property name="maxIdle" value="${jdbc.maxIdle}"></property> </bean> <!--1. 读数据源配置 --> <bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${readjdbc.driver}" /> <property name="url" value="${readjdbc.url}" /> <property name="username" value="${readjdbc.username}" /> <property name="password" value="${readjdbc.password}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="${readjdbc.initialSize}"></property> <!-- 连接池最大数量 --> <property name="maxActive" value="${readjdbc.maxActive}"></property> <!-- 连接池最大空闲 --> <property name="maxIdle" value="${readjdbc.maxIdle}"></property> </bean> <!-- 动态配置数据源 --> <bean id ="dataSources" class= "com.soft.datasource.DynamicDataSource" > <property name ="targetDataSources"> <map key-type ="java.lang.String"> <entry value-ref ="dataSource" key= "dataSource"></entry > <entry value-ref ="readDataSource" key= "readDataSource"></entry > </map > </property > </bean >
两数据源的配置文件jdbc.propertise见下:
jdbc.username=root jdbc.password= jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/SpringTest?useUnicode=true&characterEncoding=UTF-8 jdbc.initialSize=0 jdbc.maxActive=20 jdbc.maxIdle=20 jdbc.minIdle=1 jdbc.maxWait=60000 readjdbc.username=root readjdbc.password=123 readjdbc.driver=com.mysql.jdbc.Driver readjdbc.url=jdbc:mysql://192.168.1.102:3306/springMVC_test?useUnicode=true&characterEncoding=UTF-8 readjdbc.initialSize=0 readjdbc.maxActive=20 readjdbc.maxIdle=20 readjdbc.minIdle=1 readjdbc.maxWait=60000
2.重写AbstractRoutingDataSource,定义获取数据源是readDataSource或者是DataSource,只需要在Set方法中注入此字符串即可。但考虑到并发访问数据源的切换,使用ThreadLocal本地存储方式。定义ThreadLocal,DataSourceContextHolder类。当ThreadLocal中set 数据源,并从重写的DynamicDataSource类中获取。若访问完成则释放ThreadLocal。
DataSourceContextHolder类,只是对字符串的并发持有:
public class DataSourceContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setDbType(String dbType) { contextHolder.set(dbType); } public static String getDbType() { return ((String) contextHolder.get()); } public static void clearDbType() { contextHolder.remove(); } }
DynamicDataSource类,根据不同的数据源字段,进行不同的jdbc访问
public class DynamicDataSource extends AbstractRoutingDataSource { @Override public Logger getParentLogger() { return null; } @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder. getDbType(); } }
可以在Service中使用DataSourceContextHolder.setDbType('数据源字段'),实现对不同主机访问。数据源字段定义为一个DataSourceType类,见下:
public class DataSourceType { public static final String DateBase_Slave="readDataSource"; public static final String DateBase_master="dataSource"; }
但考虑到每个方法都要写相同的东西,此处又对service进行AOP注入加注解,当方法上面有@DataBase(value = DataSourceType.DateBase_master),则是主机操作,@DataBase(value = DataSourceType.DateBase_Slave),则是对从机的操作。DataBase注解类见下:
import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface DataBase { String value() default ""; }
在AOP中需要考虑,方法上方是否存在DataSource注解,若存在则获取其Value值,并Set到DataSourceContextHolder中,若无,则方法前后不进行任何操作,具体代码见下:
package com.soft.AOP; import com.soft.datasource.DataSourceContextHolder; import com.soft.util.DataBase; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.annotation.Annotation; @Aspect @Component public class DateBaseAOP { /** * @param joinPoint * 方法上有DataBase注解则获取DataBse中value值,动态注入数据源 * */ @Around(value="execution(* com.soft.service.*.*(..))") public Object aroundService(ProceedingJoinPoint joinPoint){ MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); Annotation[] Annotations = methodSignature.getMethod().getDeclaredAnnotations(); DataBase dataBase = findDataBase(Annotations); Object value=null; if(dataBase!=null){ DataSourceContextHolder. setDbType(dataBase.value()); } try { value=joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } if(dataBase!=null){ DataSourceContextHolder.clearDbType(); } return value; } /** * * @param annotations 查询方法上@DataBase注解是否存在 * @return 存在返回@DataBase注解对象,不存在则返回null */ DataBase findDataBase( Annotation[] annotations ) { for ( Annotation annotation : annotations ) { if (annotation.annotationType() == DataBase.class) { return DataBase.class.cast(annotation); } } return null; } }
AOP一定要注意,在bean.xml,扫描注解下面加上支持AOP,否则AOP无法生效。另外切面方法一定要有返回值,否在在Service方法中有返回值而切入方法没有,无法获取查询数据库的数据。
<context:component-scan base-package="com.soft" /> <aop:aspectj-autoproxy proxy-target-class="true"/>