在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求。因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术。读写分离就是就是一个Master数据库,多个Slave数据库,Master数据库负责数据的写操作,slave库负责数据读操作,通过slave库来降低Master库的负载。因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验。我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力。同时随着业务的增长,会对数据库进行拆分,根据业务将业务相关的数据库表拆分到不同的数据库中。不管是读写分离还是数据库拆分都是解决数据库压力的主要方式之一。本篇文章主要讲解Spring如何配置读写分离和多数据源手段。
1.读写分离
具体到开发中,如何方便的实现读写分离呢?目前常用的有两种方式:
- 第一种方式是最常用的方式,就是定义2个数据库连接,一个是MasterDataSource,另一个是SlaveDataSource。对数据库进行操作时,先根据需求获取dataSource,然后通过dataSource对数据库进行操作。这种方式配置简单,但是缺乏灵活新。
- 第二种方式动态数据源切换,就是在程序运行时,把数据源动态织入到程序中,从而选择读取主库还是从库。主要使用的技术是:annotation,Spring AOP ,反射。下面会详细的介绍实现方式。
在介绍实现方式之前,先准备一些必要的知识,spring的AbstractRoutingDataSource类。AbstractRoutingDataSource这个类是spring2.0以后增加的,我们先来看下AbstractRoutingDataSource的定义:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {}
AbstractRoutingDataSource继承了AbstractDataSource并实现了InitializingBean,因此AbstractRoutingDataSource会在系统启动时自动初始化实例。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, Object> targetDataSources; private Object defaultTargetDataSource; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); private Map<Object, DataSource> resolvedDataSources; private DataSource resolvedDefaultDataSource; ... }
AbstractRoutingDataSource继承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子类。DataSource 是javax.sql 的数据源接口,定义如下:
public interface DataSource extends CommonDataSource,Wrapper { Connection getConnection() throws SQLException; Connection getConnection(String username, String password) throws SQLException; }
DataSource接口定义了2个方法,都是获取数据库连接。我们在看下AbstractRoutingDataSource如何实现了DataSource接口:
public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); }
很显然就是调用自己的determineTargetDataSource() 方法获取到connection。determineTargetDataSource方法定义如下:
protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; }
我们最关心的还是下面2句话:
Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey);
determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根据lookupKey从Map中获得数据源。resolvedDataSources 和determineCurrentLookupKey定义如下:
private Map<Object, DataSource> resolvedDataSources; protected abstract Object determineCurrentLookupKey()
看到以上定义,我们是不是有点思路了,resolvedDataSources是Map类型,我们可以把MasterDataSource和SlaveDataSource存到Map中。通过写一个类DynamicDataSource继承AbstractRoutingDataSource,实现其determineCurrentLookupKey() 方法,该方法返回Map的key,master或slave。
public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { return DatabaseContextHolder.getCustomerType(); } }
定义DatabaseContextHolder
public class DatabaseContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); public static void setCustomerType(String customerType) { contextHolder.set(customerType); } public static String getCustomerType() { return contextHolder.get(); } public static void clearCustomerType() { contextHolder.remove(); } }
从DynamicDataSource 的定义看出,他返回的是DynamicDataSourceHolder.getDataSouce()值,我们需要在程序运行时调用DynamicDataSourceHolder.putDataSource()方法,对其赋值。下面是我们实现的核心部分,也就是AOP部分,DataSourceAspect定义如下:
@Aspect @Order(1) @Component public class DataSourceAspect { @Before(value = "execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))" + "||execution(* com.netease.nsip.DynamicDataSource.dao..*.add*(..))" + "||@org.springframework.transaction.annotation.Transactional * *(..)") public Object before(ProceedingJoinPoint joinPoint) throws Throwable { DatabaseContextHolder.setCustomerType("master"); Object object = joinPoint.proceed(); DatabaseContextHolder.setCustomerType("slave"); return object; } }
为了方便测试,我定义了2个数据库,Master库和Slave库,两个库中person表结构一致,但数据不同,properties文件配置如下:
#common db-driver=com.mysql.jdbc.Driver #master master-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC master-user=root master-password=root #salve slave-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC slave-user=root slave-password=root
Spring中的xml定义如下:
<!-- 配置数据源公共参数 --> <bean name="baseDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>${db-driver}</value> </property> </bean> <!-- 配置主数据源 --> <bean name="masterDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url"> <value>${master-url}</value> </property> <property name="username"> <value>${master-user}</value> </property> <property name="password"> <value>${master-password}</value> </property> </bean> <!--配置从数据源 --> <bean name="slavueDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url"> <value>${slave-url}</value> </property> <property name="username"> <value>${slave-user}</value> </property> <property name="password"> <value>${slave-password}</value> </property> </bean> <bean id="dataSource" class="com.netease.nsip.DynamicDataSource.commom.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="master" value-ref="masterDataSource" /> <entry key="slave" value-ref="slavueDataSource" /> </map> </property> <property name="defaultTargetDataSource" ref="slavueDataSource" /> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:SqlMapConfig.xml" /> <property name="dataSource" ref="dataSource" /> </bean> <!-- 持久层访问模板化的工具,线程安全,构建sqlSessionFactory --> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- 事务管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" order="200" /> <!-- 回滚方式 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*" rollback-for="Throwable" /> </tx:attributes> </tx:advice> <!-- 定义@Transactional的注解走事务管理器 --> <aop:config> <aop:pointcut id="transactionPointcutType" expression="@within(org.springframework.transaction.annotation.Transactional)" /> <aop:pointcut id="transactionPointcutMethod" expression="@annotation(org.springframework.transaction.annotation.Transactional)" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutType" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutMethod" /> </aop:config>
到目前读写分离已经配置好了,所有的以insert和add开头的dao层,以及带有Transaction注解的会走主库,其他的数据库操作走从库。当然也可以修改切入点表达式让update和delete方法走主库。上述方法是基于AOP的读写分离配置,下面使用实例结合注解讲述多数据源的配置。
2.多数据源配置
上面的实例使用AOP来配置读写分离,接下来将结合Spring注解配置多数据源,该方法也可以用于配置读写分离。先看下annotation的定义:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Profile { String value(); }
定义MultiDataSourceAspect ,在MultiDataSourceAspect根据注解获取数据源.
public class MultiDataSourceAspect { public void before(JoinPoint joinPoint) throws Throwable { Object target = joinPoint.getTarget(); String method = joinPoint.getSignature().getName(); Class<?>[] classz = target.getClass().getInterfaces(); Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()). getMethod().getParameterTypes(); try { Method m = classz[0].getMethod(method, parameterTypes); if (m != null&&m.isAnnotationPresent(Profile.class)) { Profile data = m .getAnnotation(Profile.class); DatabaseContextHolder.setCustomerType(data.value()); } } catch (Exception e) { } } }
同样为了测试,数据源properties文件如下:
#common db-driver=com.mysql.jdbc.Driver #master account-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC account-user=root account-password=root #salve goods-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC goods-user=root goods-password=root
Spring的XML文件定义如下:
<!-- 配置数据源公共参数 --> <bean name="baseDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>${db-driver}</value> </property> </bean> <!-- 配置主数据源 --> <bean name="accountDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url"> <value>${account-url}</value> </property> <property name="username"> <value>${account-user}</value> </property> <property name="password"> <value>${account-password}</value> </property> </bean> <!--配置从数据源 --> <bean name="goodsDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="url"> <value>${goods-url}</value> </property> <property name="username"> <value>${goods-user}</value> </property> <property name="password"> <value>${goods-password}</value> </property> </bean> <bean id="dataSource" class="com.netease.nsip.DynamicDataSource.commom.MultiDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="goods" value-ref="goodsDataSource" /> <entry key="account" value-ref="accountDataSource" /> </map> </property> </bean> <!-- 配置SqlSessionFactoryBean --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:multiSqlMapConfig.xml" /> <property name="dataSource" ref="dataSource" /> </bean> <!-- 持久层访问模板化的工具,线程安全,构建sqlSessionFactory --> <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean> <!-- 配置AOP --> <bean id="multiAspect" class="com.netease.nsip.DynamicDataSource.commom.MultiDataSourceAspect" /> <aop:config> <aop:aspect id="datasourceAspect" ref="multiAspect"> <aop:pointcut expression="execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))" id="tx" /> <aop:before pointcut-ref="tx" method="before" /> </aop:aspect> </aop:config>
dao层接口定义如下:
public interface IAccountDao { @Profile("account") public boolean insert(Accounts accounts); } public interface IGoodsDao { @Profile("goods") public boolean insert(Goods goods); }
Spring配置多数据源的主要方式如上所示,在实例中为了方便数据源的选择都在dao进行。而在日常开发的过程中事务通常在Service层,而事务又和数据源绑定,所以为了在Service层使用事务可以将数据源的选择在service层进行。