之前有个项目需要实现数据库读写分离,实现以后今天跟大家分享一下
demo需求:
分享的demo是为了实现对用户表的查询操作对从库操作,增删改对主库操作。
demo实现:
基于SpringAOP的实现方式有多种,可以使用aspectj直接编码,也可以使用spring+aspectj的配置方式,此处我应用的是后者,更简单明了。
- 依赖
首先是需要的maven依赖
我使用的spring版本:
<spring.version>4.2.2.RELEASE</spring.version>
<!-- SPRING begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId>
<version>${spring.version}</version> </dependency> -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- SPRING end -->
注意spring-aspects包的引入,别遗漏了。
- 工程
我新建一个springmvc的工程
spring-mvc.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:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<description>SpringMvc Configuration</description>
<!-- 注解扫描 -->
<context:component-scan base-package="com.smqi"/>
<!-- 开启面向切面编程-->
<aop:aspectj-autoproxy/>
<!--添加注解驱动-->
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">
<mvc:message-converters register-defaults="false">
<bean id="fastJsonHttpMessageConverter" class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="false" />
<property name="favorParameter" value="false" />
<property name="ignoreAcceptHeader" value="false" />
<property name="mediaTypes">
<value>
atom=application/atom+xml
html=text/html
json=application/json
*=*/*
</value>
</property>
</bean>
<mvc:view-controller path="/" view-name="redirect:/index/home"/>
<mvc:resources location="/content/" mapping="/content/**" />
<!-- 开启事务注解驱动 -->
<tx:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></beans:property>
<beans:property name="prefix" value="/WEB-INF/view/"></beans:property>
<beans:property name="suffix" value=".jsp"></beans:property>
</bean>
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">
error/403
</prop>
</props>
</property>
</bean>
</beans>
这里强调的是<aop:aspectj-autoproxy/>
的配置不可少,而且应该放在mvc配置文件下,作为DispatcherServlet启动时的初始化参数,否则aop会失效,无法切入到后面修改数据源的方法中去
spring-database.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:context="http://www.springframework.org/schema/context"
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-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
default-lazy-init="true">
<description>DataSource Configuration</description>
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<array>
<value>classpath:config/properties/jdbc.properties</value>
</array>
</property>
</bean>
<!-- 主(写)库数据源配置 -->
<!-- 常态数据源配置 -->
<bean id="masterDataSource1" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 -->
<property name="initialSize" value="${jdbc.initialSize}"/>
<!-- 最小连接池数量 -->
<property name="minIdle" value="${jdbc.minIdle}"/>
<!-- 最大连接池数量 -->
<property name="maxActive" value="${jdbc.maxActive}"/>
<!-- 有两个含义:1) Destroy线程会检测连接的间隔时间 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 -->
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/>
<!-- 连接池中连接,在时间段内一直空闲, 被逐出连接池的时间(默认为30分钟) -->
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/>
<!-- 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 -->
<property name="validationQuery" value="${jdbc.validationQuery}"/>
<!-- 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 -->
<property name="testWhileIdle" value="${jdbc.testWhileIdle}"/>
<!-- 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 -->
<property name="testOnBorrow" value="${jdbc.testOnBorrow}"/>
<!-- 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 -->
<property name="testOnReturn" value="${jdbc.testOnReturn}"/>
<!-- 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 -->
<property name="maxOpenPreparedStatements" value="${jdbc.maxOpenPreparedStatements}"/>
<!-- 对于长时间不使用的连接强制关闭 -->
<property name="removeAbandoned" value="${jdbc.removeAbandoned}"/>
<!-- 超过指定时间后开始关闭空闲连接 -->
<property name="removeAbandonedTimeout" value="${jdbc.removeAbandonedTimeout}"/>
<!-- 将当前关闭动作记录到日志 -->
<property name="logAbandoned" value="${jdbc.logAbandoned}"/>
<!-- 属性类型是字符串,通过别名的方式配置扩展插件,
常用的插件有:监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall -->
<!--<property name="filters" value="${jdbc.filtes}"/>-->
</bean>
<!-- 从(读)库数据源配置 -->
<!-- 鉴权数据源配置 -->
<bean id="slaveDataSource1" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
destroy-method="close">
<property name="url" value="${slave.jdbc.url}"/>
<property name="username" value="${slave.jdbc.username}"/>
<property name="password" value="${slave.jdbc.password}"/>
</bean>
<!-- 数据库读写分离配置 start -->
<bean id="readWriteDataSource" class="com.smqi.common.dynamicDS.ReadWriteDataSource">
<property name="writeDataSource" ref="masterDataSource1"/>
<property name="readDataSourceMap">
<map>
<entry key="readDataSource1" value-ref="slaveDataSource1"/>
<!--<entry key="readDataSource2" value-ref="slaveDataSource1"/>-->
</map>
</property>
</bean>
<!-- 读取分离方法拦截判断 -->
<bean id="readWriteDataSourceTransactionProcessor"
class="com.smqi.common.dynamicDS.ReadWriteDataSourceProcessor"/>
<!-- 事物管理方法选择配置,即读方法不开启新事务或在当前事务中,其余方法开启新事务 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="merge*" propagation="REQUIRED"/>
<tx:method name="del*" propagation="REQUIRED"/>
<tx:method name="remove*" propagation="REQUIRED"/>
<tx:method name="put*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="use*" read-only="true"/>
<tx:method name="get*" read-only="true"/>
<tx:method name="count*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<tx:method name="list*" read-only="true"/>
<tx:method name="select*" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 事务管理业务切面配置,只对业务逻辑层实施事务-->
<aop:config expose-proxy="true">
<!-- 一定范围的业务操作或精确拦截到某些数据操作 -->
<!--<aop:pointcut id="txPointcut" expression="(execution(* com.smqi.modules..service.impl..*.*(..))) or (execution(* com.smqi.manage..service.impl..*.*(..)))"/>-->
<aop:pointcut id="txPointcut" expression="execution(* com.smqi.modules..service.impl..*.*(..))"/>
<!-- 实现事务控制 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
<!-- 通过AOP切面实现读/写库选择 -->
<aop:aspect order="-2147483648" ref="readWriteDataSourceTransactionProcessor">
<!-- 数据库读/写库选择 -->
<aop:around pointcut-ref="txPointcut" method="determineReadOrWriteDB"/>
<!-- 操作日志记录,目前只对指定的注解实行记录 -->
<!--<aop:around pointcut-ref="txPointcut2" method="doAroundMethodForCtLog"/>-->
</aop:aspect>
</aop:config>
<!-- 数据库读写分离配置 end -->
<!-- 事务控制 -->
<bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="readWriteDataSource"></property>
</bean>
<!-- spring jdbc -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="readWriteDataSource"></constructor-arg>
</bean>
</beans>
jdbc.properties配置文件如下
#####主数据库
jdbc.url=jdbc:mysql://127.0.0.1:3306/masterds?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=123456
#####从数据库
slave.jdbc.url=jdbc:mysql://127.0.0.1:3307/slaveds?useUnicode=true&characterEncoding=UTF-8
slave.jdbc.username=root
slave.jdbc.password=123456
jdbc.initialSize = 1
jdbc.minIdle = 1
jdbc.maxActive = 40
jdbc.timeBetweenEvictionRunsMillis = 60000
jdbc.minEvictableIdleTimeMillis = 300000
jdbc.validationQuery = SELECT 'x'
jdbc.testWhileIdle = true
jdbc.testOnBorrow = false
jdbc.testOnReturn = false
jdbc.maxOpenPreparedStatements = -1
jdbc.removeAbandoned = true
jdbc.removeAbandonedTimeout = 1800
jdbc.logAbandoned = true
从配置可以看出,我应用的是本地的两个数据库,怎么安装,参考我的上一篇文章,其实就是为这篇文章服务的哈,当然你也可以指定其他服务器的数据库地址
新建两个数据库,即主(写)库和从(读)库
代码service层和dao层就是简单的增删改查,对数据库进行操作
package com.smqi.modules.demo.dao.impl;
import com.smqi.modules.demo.dao.DemoDao;
import com.smqi.modules.demo.entity.DemoUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* Created by smqi on 2016/10/31.
*/
@Repository
public class DemoDaoImpl implements DemoDao {
protected final Logger logger = LoggerFactory.getLogger(DemoDaoImpl.class);
@Autowired
private JdbcTemplate jdbcTemplate;
public void addDemo() {
jdbcTemplate.update("insert into demo_user (name,age) VALUES (\"主数据库\",23)");
}
public void getDemo() {
DemoUser user = new DemoUser();
String sql = "select * from demo_user where id = ?";
try {
user = jdbcTemplate.queryForObject(sql, new Object[]{1}, new RowMapper<DemoUser>() {
public DemoUser mapRow(ResultSet resultSet, int i) throws SQLException {
DemoUser user = new DemoUser();
user.setId(resultSet.getInt(1));
user.setName(resultSet.getString(2));
user.setAge(resultSet.getInt(3));
return user;
}
});
} catch (Exception e) {
System.out.println("数据库无此用户!");
}
System.out.println("获取id=1的用户名称:" + user.getName());
}
public void queryDemo() {
String sql = "select * from demo_user";
List<DemoUser> userList = jdbcTemplate.query(sql, new Object[]{}, new RowMapper<DemoUser>() {
public DemoUser mapRow(ResultSet resultSet, int i) throws SQLException {
DemoUser user = new DemoUser();
user.setId(resultSet.getInt(1));
return user;
}
});
logger.info("共获取用户列表{}条", userList.size());
}
public void deleteDemo() {
jdbcTemplate.update("DELETE from demo_user where id = 1 ");
}
public void updateDemo() {
jdbcTemplate.update("UPDATE demo_user SET name = '路飞' where id = 1");
}
}
- 核心方法
实现线程内安全的数据源选择,支持事务管理
具体实现逻辑可以参看代码(文章末附有链接)
package com.smqi.common.dynamicDS;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.util.CollectionUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* 读/写动态选择数据库实现
* 目前实现功能
* 默认按顺序轮询使用读库
* 默认选择写库
* 一写多读、当写时默认读操作到写库、当写时强制读操作到读库
*
* @author smqi
* @createTime 2016/10/30 14:26
*/
public class ReadWriteDataSource extends AbstractDataSource implements InitializingBean {
private static final Logger log = LoggerFactory.getLogger(ReadWriteDataSource.class);
private DataSource writeDataSource;
private Map<String, DataSource> readDataSourceMap;
private String[] readDataSourceNames;
private DataSource[] readDataSources;
private int readDataSourceCount;
private AtomicInteger counter = new AtomicInteger(1);
/**
* 设置读库(name, DataSource)
*
* @param readDataSourceMap
*/
public void setReadDataSourceMap(Map<String, DataSource> readDataSourceMap) {
this.readDataSourceMap = readDataSourceMap;
}
public void setWriteDataSource(DataSource writeDataSource) {
this.writeDataSource = writeDataSource;
}
public void afterPropertiesSet() throws Exception {
if (writeDataSource == null) {
throw new IllegalArgumentException("property 'writeDataSource' is required");
}
if (CollectionUtils.isEmpty(readDataSourceMap)) {
throw new IllegalArgumentException("property 'readDataSourceMap' is required");
}
readDataSourceCount = readDataSourceMap.size();
readDataSources = new DataSource[readDataSourceCount];
readDataSourceNames = new String[readDataSourceCount];
int i = 0;
for (Entry<String, DataSource> e : readDataSourceMap.entrySet()) {
readDataSources[i] = e.getValue();
readDataSourceNames[i] = e.getKey();
i++;
}
}
private DataSource determineDataSource() {
if (ReadWriteDataSourceDecision.isChoiceWrite()) {
// log.debug("current determine write datasource");
return writeDataSource;
}
if (ReadWriteDataSourceDecision.isChoiceNone()) {
// log.debug("no choice read/write, default determine write datasource");
return writeDataSource;
}
return determineReadDataSource();
}
private DataSource determineReadDataSource() {
// 按照顺序选择读库
// TODO 算法改进
int index = counter.incrementAndGet() % readDataSourceCount;
if (index < 0) {
index = -index;
}
String dataSourceName = readDataSourceNames[index];
// log.debug("current determine read datasource : {}", dataSourceName);
return readDataSources[index];
}
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}
}
package com.smqi.common.dynamicDS;
/**
* 读/写动态数据库 决策者
* 根据DataSourceType是write/read 来决定是使用读/写数据库
* 通过ThreadLocal绑定实现选择功能
*
* @author smqi
* @createTime 2016/10/30 11:13
*/
public class ReadWriteDataSourceDecision {
public enum DataSourceType {
write, read;
}
private static final ThreadLocal<DataSourceType> holder = new ThreadLocal<DataSourceType>();
public static void markWrite() {
holder.set(DataSourceType.write);
}
public static void markRead() {
holder.set(DataSourceType.read);
}
public static void reset() {
holder.set(null);
}
public static boolean isChoiceNone() {
return null == holder.get();
}
public static boolean isChoiceWrite() {
return DataSourceType.write == holder.get();
}
public static boolean isChoiceRead() {
return DataSourceType.read == holder.get();
}
}
package com.smqi.common.dynamicDS;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.NestedRuntimeException;
import org.springframework.core.PriorityOrdered;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
/**
* 读/写动态数据库选择处理器
* 通过AOP切面实现读/写选择
* <p>
* 1、首先将当前方法 与 根据之前【读/写动态数据库选择处理器】 提取的读库方法 进行匹配
* <p>
* 2、如果匹配,说明是读取数据:
* 2.1、forceChoiceReadWhenWrite:true,即强制走读库
* 2.2、如果之前是写操作且forceChoiceReadWhenWrite:false,将从写库进行读取
* 2.3、否则,到读库进行读取数据
* <p>
* 3、如果不匹配,说明默认将使用写库进行操作
* <p>
* 4、配置方式
* <aop:aspect order="-2147483648" ref="readWriteDataSourceTransactionProcessor">
* <aop:around pointcut-ref="txPointcut" method="determineReadOrWriteDB"/>
* </aop:aspect>
* 4.1、此处order = Integer.MIN_VALUE 即最高的优先级(请参考http://jinnianshilongnian.iteye.com/blog/1423489)
* 4.2、切入点:txPointcut 和 实施事务的切入点一样
* 4.3、determineReadOrWriteDB方法用于决策是走读/写库的
*
* @author smqi
* @createTime 2016/10/30 09:57
*/
public class ReadWriteDataSourceProcessor implements BeanPostProcessor,
PriorityOrdered {
private static final Logger log = LoggerFactory
.getLogger(ReadWriteDataSourceProcessor.class);
private boolean forceChoiceReadWhenWrite = false;
private Map<String, Boolean> readMethodMap = new HashMap<String, Boolean>();
/**
* 当之前操作是写的时候,是否强制从从库读 默认(false) 当之前操作是写,默认强制从写库读
*
* @param forceChoiceReadWhenWrite
*/
public void setForceChoiceReadWhenWrite(boolean forceChoiceReadWhenWrite) {
this.forceChoiceReadWhenWrite = forceChoiceReadWhenWrite;
}
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (!(bean instanceof NameMatchTransactionAttributeSource)) {
return bean;
}
try {
NameMatchTransactionAttributeSource transactionAttributeSource =
(NameMatchTransactionAttributeSource) bean;
Field nameMapField =
ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
nameMapField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, TransactionAttribute> nameMap = (Map<String,
TransactionAttribute>) nameMapField.get(transactionAttributeSource);
for (Map.Entry<String, TransactionAttribute> entry : nameMap.entrySet()) {
RuleBasedTransactionAttribute attr = (RuleBasedTransactionAttribute) entry.getValue();
// 仅对read-only的处理
if (!attr.isReadOnly()) {
continue;
}
String methodName = entry.getKey();
Boolean isForceChoiceRead = Boolean.FALSE;
if (forceChoiceReadWhenWrite) { // 不管之前操作是写,默认强制从读库读 (设置为NOT_SUPPORTED即可) // NOT_SUPPORTED会挂起之前的事务
attr.setPropagationBehavior(Propagation.NOT_SUPPORTED.value());
isForceChoiceRead = Boolean.TRUE;
} else { // 否则 设置为SUPPORTS(这样可以参与到写事务) attr.setPropagationBehavior(Propagation.SUPPORTS.value()); }
log.debug("read/write transaction process method:{} force read:{}", methodName,
isForceChoiceRead);
readMethodMap.put(methodName, isForceChoiceRead);
}
}
} catch (Exception e) {
throw new ReadWriteDataSourceTransactionException("process read/write transaction error", e);
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@SuppressWarnings({"serial", "unused"})
private class ReadWriteDataSourceTransactionException extends
NestedRuntimeException {
public ReadWriteDataSourceTransactionException(String message,
Throwable cause) {
super(message, cause);
}
}
public Object determineReadOrWriteDB(ProceedingJoinPoint pjp)
throws Throwable {
if (isChoiceReadDB(pjp.getSignature().getName())) {
ReadWriteDataSourceDecision.markRead();
} else {
ReadWriteDataSourceDecision.markWrite();
}
try {
return pjp.proceed();
} finally {
ReadWriteDataSourceDecision.reset();
}
}
private boolean isChoiceReadDB(String methodName) {
String bestNameMatch = null;
for (String mappedName : this.readMethodMap.keySet()) {
if (isMatch(methodName, mappedName)) {
bestNameMatch = mappedName;
break;
}
}
Boolean isForceChoiceRead = readMethodMap.get(bestNameMatch);
// 表示强制选择 读 库
if (isForceChoiceRead == Boolean.TRUE) {
return true;
}
// 如果之前选择了写库 现在还选择 写库
if (ReadWriteDataSourceDecision.isChoiceWrite()) {
return false;
}
// 表示应该选择读库
if (isForceChoiceRead != null) {
return true;
}
// 默认选择 写库
return false;
}
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
public int getOrder() {
// TODO Auto-generated method stub
return 0;
}
public String getIp() {
try {
InetAddress ia = InetAddress.getLocalHost();
return ia.getHostAddress();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
实现逻辑很清晰,我简单总结一下思路
-
首先是配置文件,定义了切入层,即哪些包需要加入aop切面。
-
数据源通过配置文件注入,默认从主(写)库,从库数据源可以配置多个,同时按一定算法进行选择
-
定义需要通过读库的方法规范,代码里通过read-only里来识别
-
定义线程安全的变量来存放当前数据源选择标识,即write or read,重新实现了DataSource接口的getConnection方法,在实现方法内实现数据源的选择
-
测试结果
主、从库的demo_user表开始都是空的
我们启动项目,执行
因为是写操作,我们查看主数据库发现插入了数据,从数据库依然是空的。
继续执行:
该get方法默认是查询id=1的用户,结果:
我们手动在读库加一条记录如下:
然后再执行下上述的get操作:
综上说明,新增对的是写库,查询的确是从读库查的。
最后,此demo工程的代码我已打成war包提交,可以点击下载,自行测试。