前言
随着系统中用户访问量的不断增加,数据库频繁访问将成为我们系统性能的瓶颈之一。经过对用户访问的分析,发现用户操作的特点是读多写少,而读操作通常耗时比较长,占用CPU较多。因为我们决定基于Spring AOP思想实现数据源的动态切换,进行数据库的读写分离。以下为相关逻辑以及伪代码。
- 创建数据源注解,用于service层区分读/写数据源
DataSource.java
package com.xdja.dataexchange.common.datasource;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lee
* @description: 数据源DataSource注解
* @date 2018-10-31 下午5:21:32
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
String value();
}
- 动态数据源切换,根据DataSource注解对当前线程动态切换读/写数据源。
HandleDataSource.java
package com.xdja.dataexchange.common.datasource;
/**
* @author lee
* @description: 动态切换数据源
* @date 2018-10-31 下午6:21:11
*/
public class HandleDataSource {
public static final ThreadLocal<String> holder = new ThreadLocal<String>();
/**
* 绑定当前线程数据源
* @param datasource
*/
public static void putDataSource(String datasource) {
holder.set(datasource);
}
/**
* 获取当前线程的数据源
* @return
*/
public static String getDataSource() {
return holder.get();
}
}
- 使用Spring的 AbstractRoutingDataSource 获取目标数据源
DynamicDataSource.java
package com.xdja.dataexchange.common.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author lee
* @description: 获取目标数据源
* @date 2018-10-31 下午6:20:16
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 获取与数据源相关的key 此key是Map<String,DataSource> resolvedDataSources 中与数据源绑定的key值
* 在通过determineTargetDataSource获取目标数据源时使用
*/
@Override
protected Object determineCurrentLookupKey() {
return HandleDataSource.getDataSource();
}
}
- 创建数据库切面,在dao层方法获取datasource对象之前,在切面中指定当前线程数据源
DataSourceAspect.java
package com.xdja.dataexchange.aop;
import com.xdja.dataexchange.common.datasource.DataSource;
import com.xdja.dataexchange.common.datasource.HandleDataSource;
import com.xdja.dataexchange.common.logger.LoggerUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
* @author lee
* @description: 数据库切面
* @date 2018-10-31 下午6:24:33
*/
public class DataSourceAspect {
/**
* 在dao层方法获取datasource对象之前,在切面中指定当前线程数据源
*/
public void before(JoinPoint point) {
Object target = point.getTarget();
String method = point.getSignature().getName();
// 获取目标类的接口, 所以@DataSource需要写在接口上
Class<?>[] classz = target.getClass().getInterfaces();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
.getMethod().getParameterTypes();
try {
Method m = classz[0].getMethod(method, parameterTypes);
if (m != null && m.isAnnotationPresent(DataSource.class)) {
DataSource data = m.getAnnotation(DataSource.class);
// 数据源放到当前线程中
HandleDataSource.putDataSource(data.value());
}
} catch (Exception e) {
LoggerUtil.error("service error", e);
}
}
}
- Spring配置Mybatis相关信息
applicationContext-mybatis.xml
<?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:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd"
default-lazy-init="false">
<!-- 配置写数据库 -->
<bean id="writeDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${write.jdbc.driver}"/>
<property name="jdbcUrl" value="${write.jdbc.url}"/>
<property name="user" value="${write.jdbc.username}"/>
<property name="password" value="${write.jdbc.password}"/>
<!-- 连接关闭时默认将所有未提交的操作回滚。默认为false -->
<property name="autoCommitOnClose" value="${write.autoCommitOnClose}"/>
<!-- 连接池中保留的最小连接数-->
<property name="minPoolSize" value="${write.minPoolSize}"/>
<!-- 连接池中保留的最大连接数。默认为15 -->
<property name="maxPoolSize" value="${write.maxPoolSize}"/>
<!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。默认为3 -->
<property name="initialPoolSize" value="${write.initialPoolSize}"/>
<!-- 最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃。默认为0秒 -->
<property name="maxIdleTime" value="${write.maxIdleTime}"/>
<!-- 当连接池中的连接用完时,C3P0一次性创建新连接的数目。默认为3 -->
<property name="acquireIncrement" value="${write.acquireIncrement}"/>
<!-- 定义在从数据库获取新连接失败后重复尝试获取的次数,默认为30 -->
<property name="acquireRetryAttempts" value="${write.acquireRetryAttempts}"/>
<!-- 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒,默认为0 -->
<property name="checkoutTimeout" value="${write.checkoutTimeout}"/>
<!--C3P0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能,通过多线程实现多个操作同时被执行。默认为3; -->
<property name="numHelperThreads" value="${write.numHelperThreads}"/>
</bean>
<!-- 配置读数据库 -->
<bean id="readDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${read.jdbc.driver}"/>
<property name="jdbcUrl" value="${read.jdbc.url}"/>
<property name="user" value="${read.jdbc.username}"/>
<property name="password" value="${read.jdbc.password}"/>
<!-- 连接关闭时默认将所有未提交的操作回滚。默认为false -->
<property name="autoCommitOnClose" value="${read.autoCommitOnClose}"/>
<!-- 连接池中保留的最小连接数-->
<property name="minPoolSize" value="${read.minPoolSize}"/>
<!-- 连接池中保留的最大连接数。默认为15 -->
<property name="maxPoolSize" value="${read.maxPoolSize}"/>
<!-- 初始化时获取的连接数,取值应在minPoolSize与maxPoolSize之间。默认为3 -->
<property name="initialPoolSize" value="${read.initialPoolSize}"/>
<!-- 最大空闲时间,超过空闲时间的连接将被丢弃。为0或负数则永不丢弃。默认为0秒 -->
<property name="maxIdleTime" value="${read.maxIdleTime}"/>
<!-- 当连接池中的连接用完时,C3P0一次性创建新连接的数目。默认为3 -->
<property name="acquireIncrement" value="${read.acquireIncrement}"/>
<!-- 定义在从数据库获取新连接失败后重复尝试获取的次数,默认为30 -->
<property name="acquireRetryAttempts" value="${read.acquireRetryAttempts}"/>
<!-- 当连接池用完时客户端调用getConnection()后等待获取新连接的时间,超时后将抛出SQLException,如设为0则无限期等待。单位毫秒,默认为0 -->
<property name="checkoutTimeout" value="${read.checkoutTimeout}"/>
<!--C3P0是异步操作的,缓慢的JDBC操作通过帮助进程完成。扩展这些操作可以有效的提升性能,通过多线程实现多个操作同时被执行。默认为3; -->
<property name="numHelperThreads" value="${read.numHelperThreads}"/>
</bean>
<!-- 动态数据源,根据service接口上的注解来决定取哪个数据源 -->
<bean id="dataSource" class="com.xdja.dataexchange.common.datasource.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry key="write" value-ref="writeDataSource"/>
<entry key="read" value-ref="readDataSource"/>
</map>
</property>
<property name="defaultTargetDataSource" ref="writeDataSource"/>
</bean>
<!-- 设置sessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 自动扫描mapping.xml文件 -->
<property name="mapperLocations" value="classpath:mapping/*.xml"/>
<property name="typeAliasesPackage" value="com.xdja.dataexchange.pojo,com.xdja.dataexchange.dto"/>
</bean>
<!-- 配置扫描器,DAO接口所在包名,Spring会自动查找其下的类 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.xdja.dataexchange.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 声明式开启 -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="1"/>
<!-- 为业务逻辑层的方法解析@DataSource注解 为当前线程的HandlerDataSource注入数据源 -->
<bean id="dataSourceAspect" class="com.xdja.dataexchange.aop.DataSourceAspect"/>
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="dataSourceAspect" order="2">
<aop:pointcut expression="execution(* com.xdja.dataexchange.service.impl..*.*(..))" id="pointcut"/>
<aop:before pointcut-ref="pointcut" method="before"/>
</aop:aspect>
</aop:config>
</beans>
以上为数据库读写分离的定义部分,这里只简述了关键的流程,具体的配置文件加载以及项目启动在上述中未作说明。
下面我们将具体介绍在service中使用读/写数据源的流程。
- 接口中读/写数据源的配置,如果接口需要使用读数据源,请在接口上方添加注解 @DataSource("readDataSource");如果接口需要使用写数据源,请在接口上方添加注解 @DataSource("writeDataSource")。
SyncObjectService.java
package com.xdja.dataexchange.service;
import com.xdja.dataexchange.common.datasource.DataSource;
import com.xdja.dataexchange.pojo.*;
import java.util.List;
import java.util.Map;
/**
* @author lee
* @description: 同步对象serivce
* @date 2018-11-1 下午7:11:33
*/
public interface SyncObjectService {
/**
* 保存同步对象
*
* @param logIndex
* @param obj
* @return
*/
@DataSource("writeDataSource")
public boolean saveDbSyncObj(long logIndex, SyncDatabaseObject obj);
/**
* 查询同步对象
*
* @param logIndex
* @param id
* @return
*/
@DataSource("readDataSource")
public SyncFileObject queryDbSyncObj(long logIndex, long id);
}
- serviceImpl实现层逻辑,可根据具体业务进行事务管理,用法为 @Transactional(rollbackFor = Exception.class) ,其中 Exception.class 可为自定义异常类
SyncObjectServiceImpl.java
package com.xdja.dataexchange.service.impl;
import com.xdja.dataexchange.common.logger.LoggerUtil;
import com.xdja.dataexchange.dao.SyncObjectDao;
import com.xdja.dataexchange.exception.ServiceException;
import com.xdja.dataexchange.pojo.*;
import com.xdja.dataexchange.service.SyncObjectService;
import com.xdja.dataexchange.utils.Utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author lee
* @description: 同步对象impl
* @date 2018-11-2 上午9:09:01
*/
@Service
public class SyncObjectServiceImpl implements SyncObjectService {
private final SyncObjectDao dao;
@Autowired
public SyncObjectServiceImpl(SyncObjectDao dao) {
this.dao = dao;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveDbSyncObj(long logIndex, SyncDatabaseObject obj) {
try {
// 相关保存逻辑
// ...
} catch (Exception e) {
LoggerUtil.warn("save sync object happened exception, detail:{}", Utils.getStackTrace(e));
throw new ServiceException("failed to save db sync object");
}
return true;
}
@Override
public SyncDatabaseObject queryDbSyncObj(long logIndex, long id) {
return dao.queryDbSyncObj(id);
}
}
以上为项目中使用的Spring+Mybatis实现Mysql读写分离的方案,仅供参考。
参考资料:
https://www.cnblogs.com/lidj/p/7337535.html
https://www.cnblogs.com/jeffen/p/6274038.html
网友评论