Spring+MyBatis实现数据库读写分离方案

推荐第四种

方案1

通过MyBatis配置文件创建读写分离两个DataSource,每个SqlSessionFactoryBean对象的mapperLocations属性制定两个读写数据源的配置文件。将所有读的操作配置在读文件中,所有写的操作配置在写文件中。

优点:实现简单

缺点:维护麻烦,需要对原有的xml文件进行重新修改,不支持多读,不易扩展

实现方式

<bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init"
 destroy-method="close">
 <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
 <!-- 配置获取连接等待超时的时间 -->
 <property name="maxWait" value="60000"/>
 <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
 <property name="timeBetweenEvictionRunsMillis" value="60000"/>
 <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
 <property name="minEvictableIdleTimeMillis" value="300000"/>
 <property name="validationQuery" value="SELECT 'x'"/>
 <property name="testWhileIdle" value="true"/>
 <property name="testOnBorrow" value="false"/>
 <property name="testOnReturn" value="false"/>
 <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
 <property name="poolPreparedStatements" value="true"/>
 <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
 <property name="filters" value="config"/>
 <property name="connectionProperties" value="config.decrypt=true" />
</bean>
<bean id="readDataSource" parent="abstractDataSource">
 <!-- 基本属性 url、user、password -->
 <property name="url" value="${read.jdbc.url}"/>
 <property name="username" value="${read.jdbc.user}"/>
 <property name="password" value="${read.jdbc.password}"/>
 <!-- 配置初始化大小、最小、最大 -->
 <property name="initialSize" value="${read.jdbc.initPoolSize}"/>
 <property name="minIdle" value="10"/>
 <property name="maxActive" value="${read.jdbc.maxPoolSize}"/>
</bean>
<bean id="writeDataSource" parent="abstractDataSource">
 <!-- 基本属性 url、user、password -->
 <property name="url" value="${write.jdbc.url}"/>
 <property name="username" value="${write.jdbc.user}"/>
 <property name="password" value="${write.jdbc.password}"/>
 <!-- 配置初始化大小、最小、最大 -->
 <property name="initialSize" value="${write.jdbc.initPoolSize}"/>
 <property name="minIdle" value="10"/>
 <property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
</bean>
<bean id="readSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
 <property name="dataSource" ref="readDataSource"/>
 <property name="mapperLocations" value="classpath:mapper/read/*.xml"/>
</bean>
<bean id="writeSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
 <property name="dataSource" ref="writeDataSource"/>
 <property name="mapperLocations" value="classpath:mapper/write/*.xml"/>
</bean>

方案2

通过Spring AOP在业务层实现读写分离,在DAO层调用前定义切面,利用Spring的AbstractRoutingDataSource解决多数据源的问题,实现动态选择数据源

优点:通过注解的方法在DAO每个方法上配置数据源,原有代码改动量少,易扩展,支持多读

缺点:需要在DAO每个方法上配置注解,人工管理,容易出错

实现方式

//定义枚举类型,读写
public enum DynamicDataSourceGlobal {
 READ, WRITE;
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * RUNTIME
 * 定义注解
 * 编译器将把注释记录在类文件中,在运行时 VM 将保留注释,因此可以反射性地读取。
 * @author shma1664
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
 public DynamicDataSourceGlobal value() default DynamicDataSourceGlobal.READ;
}
/**
 * Created by IDEA
 * 本地线程设置和获取数据源信息
 * User: mashaohua
 * Date: 2016-07-07 13:35
 * Desc:
 */
public class DynamicDataSourceHolder {
 private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();
 public static void putDataSource(DynamicDataSourceGlobal dataSource){
 holder.set(dataSource);
 }
 public static DynamicDataSourceGlobal getDataSource(){
 return holder.get();
 }
 public static void clearDataSource() {
 holder.remove();
 }
}
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-14 10:56
 * Desc: 动态数据源实现读写分离
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
 private Object writeDataSource; //写数据源
 private List<Object> readDataSources; //多个读数据源
 private int readDataSourceSize; //读数据源个数
 private int readDataSourcePollPattern = 0; //获取读数据源方式,0:随机,1:轮询
 private AtomicLong counter = new AtomicLong(0);
 private static final Long MAX_POOL = Long.MAX_VALUE;
 private final Lock lock = new ReentrantLock();
 @Override
 public void afterPropertiesSet() {
 if (this.writeDataSource == null) {
  throw new IllegalArgumentException("Property 'writeDataSource' is required");
 }
 setDefaultTargetDataSource(writeDataSource);
 Map<Object, Object> targetDataSources = new HashMap<>();
 targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
 if (this.readDataSources == null) {
  readDataSourceSize = 0;
 } else {
  for(int i=0; i<readDataSources.size(); i++) {
  targetDataSources.put(DynamicDataSourceGlobal.READ.name() + i, readDataSources.get(i));
  }
  readDataSourceSize = readDataSources.size();
 }
 setTargetDataSources(targetDataSources);
 super.afterPropertiesSet();
 }
 @Override
 protected Object determineCurrentLookupKey() {
 DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
 if(dynamicDataSourceGlobal == null
  || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE
  || readDataSourceSize <= 0) {
  return DynamicDataSourceGlobal.WRITE.name();
 }
 int index = 1;
 if(readDataSourcePollPattern == 1) {
  //轮询方式
  long currValue = counter.incrementAndGet();
  if((currValue + 1) >= MAX_POOL) {
  try {
   lock.lock();
   if((currValue + 1) >= MAX_POOL) {
   counter.set(0);
   }
  } finally {
   lock.unlock();
  }
  }
  index = (int) (currValue % readDataSourceSize);
 } else {
  //随机方式
  index = ThreadLocalRandom.current().nextInt(0, readDataSourceSize);
 }
 return dynamicDataSourceGlobal.name() + index;
 }
 public void setWriteDataSource(Object writeDataSource) {
 this.writeDataSource = writeDataSource;
 }
 public void setReadDataSources(List<Object> readDataSources) {
 this.readDataSources = readDataSources;
 }
 public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
 this.readDataSourcePollPattern = readDataSourcePollPattern;
 }
}
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-07 13:39
 * Desc: 定义选择数据源切面
 */
public class DynamicDataSourceAspect {
 private static final Logger logger = Logger.getLogger(DynamicDataSourceAspect.class);
 public void pointCut(){};
 public void before(JoinPoint point)
 {
 Object target = point.getTarget();
 String methodName = point.getSignature().getName();
 Class<?>[] clazz = target.getClass().getInterfaces();
 Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
 try {
  Method method = clazz[0].getMethod(methodName, parameterTypes);
  if (method != null && method.isAnnotationPresent(DataSource.class)) {
  DataSource data = method.getAnnotation(DataSource.class);
  DynamicDataSourceHolder.putDataSource(data.value());
  }
 } catch (Exception e) {
  logger.error(String.format("Choose DataSource error, method:%s, msg:%s", methodName, e.getMessage()));
 }
 }
 public void after(JoinPoint point) {
 DynamicDataSourceHolder.clearDataSource();
 }
}
<?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">
 <bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
 <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
 <!-- 配置获取连接等待超时的时间 -->
 <property name="maxWait" value="60000"/>
 <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
 <property name="timeBetweenEvictionRunsMillis" value="60000"/>
 <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
 <property name="minEvictableIdleTimeMillis" value="300000"/>
 <property name="validationQuery" value="SELECT 'x'"/>
 <property name="testWhileIdle" value="true"/>
 <property name="testOnBorrow" value="false"/>
 <property name="testOnReturn" value="false"/>
 <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
 <property name="poolPreparedStatements" value="true"/>
 <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
 <property name="filters" value="config"/>
 <property name="connectionProperties" value="config.decrypt=true" />
 </bean>
 <bean id="dataSourceRead1" parent="abstractDataSource">
 <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
 <!-- 基本属性 url、user、password -->
 <property name="url" value="${read1.jdbc.url}"/>
 <property name="username" value="${read1.jdbc.user}"/>
 <property name="password" value="${read1.jdbc.password}"/>
 <!-- 配置初始化大小、最小、最大 -->
 <property name="initialSize" value="${read1.jdbc.initPoolSize}"/>
 <property name="minIdle" value="${read1.jdbc.minPoolSize}"/>
 <property name="maxActive" value="${read1.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSourceRead2" parent="abstractDataSource">
 <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
 <!-- 基本属性 url、user、password -->
 <property name="url" value="${read2.jdbc.url}"/>
 <property name="username" value="${read2.jdbc.user}"/>
 <property name="password" value="${read2.jdbc.password}"/>
 <!-- 配置初始化大小、最小、最大 -->
 <property name="initialSize" value="${read2.jdbc.initPoolSize}"/>
 <property name="minIdle" value="${read2.jdbc.minPoolSize}"/>
 <property name="maxActive" value="${read2.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSourceWrite" parent="abstractDataSource">
 <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
 <!-- 基本属性 url、user、password -->
 <property name="url" value="${write.jdbc.url}"/>
 <property name="username" value="${write.jdbc.user}"/>
 <property name="password" value="${write.jdbc.password}"/>
 <!-- 配置初始化大小、最小、最大 -->
 <property name="initialSize" value="${write.jdbc.initPoolSize}"/>
 <property name="minIdle" value="${write.jdbc.minPoolSize}"/>
 <property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSource" class="com.test.api.dao.datasource.DynamicDataSource">
 <property name="writeDataSource" ref="dataSourceWrite" />
 <property name="readDataSources">
  <list>
  <ref bean="dataSourceRead1" />
  <ref bean="dataSourceRead2" />
  </list>
 </property>
 <!--轮询方式-->
 <property name="readDataSourcePollPattern" value="1" />
 <property name="defaultTargetDataSource" ref="dataSourceWrite"/>
 </bean>
 <tx:annotation-driven transaction-manager="transactionManager"/>
 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 <property name="dataSource" ref="dataSource"/>
 </bean>
 <!-- 针对myBatis的配置项 -->
 <!-- 配置sqlSessionFactory -->
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
 <property name="dataSource" ref="dataSource"/>
 <property name="mapperLocations" value="classpath:mapper/*.xml"/>
 </bean>
 <!-- 配置扫描器 -->
 <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 <!-- 扫描包以及它的子包下的所有映射接口类 -->
 <property name="basePackage" value="com.test.api.dao.inte"/>
 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
 </bean>
 <!-- 配置数据库注解aop -->
 <bean id="dynamicDataSourceAspect" class="com.test.api.dao.datasource.DynamicDataSourceAspect" />
 <aop:config>
 <aop:aspect id="c" ref="dynamicDataSourceAspect">
  <aop:pointcut id="tx" expression="execution(* com.test.api.dao.inte..*.*(..))"/>
  <aop:before pointcut-ref="tx" method="before"/>
  <aop:after pointcut-ref="tx" method="after"/>
 </aop:aspect>
 </aop:config>
 <!-- 配置数据库注解aop -->
</beans>

方案3

通过Mybatis的Plugin在业务层实现数据库读写分离,在MyBatis创建Statement对象前通过拦截器选择真正的数据源,在拦截器中根据方法名称不同(select、update、insert、delete)选择数据源。

优点:原有代码不变,支持多读,易扩展

缺点:

实现方式

/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-19 15:40
 * Desc: 创建Connection代理接口
 */
public interface ConnectionProxy extends Connection {
 /**
 * 根据传入的读写分离需要的key路由到正确的connection
 * @param key 数据源标识
 * @return
 */
 Connection getTargetConnection(String key);
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.util.Assert;
public abstract class AbstractDynamicDataSourceProxy extends AbstractDataSource implements InitializingBean {
 private List<Object> readDataSources;
 private List<DataSource> resolvedReadDataSources;
 private Object writeDataSource;
 private DataSource resolvedWriteDataSource;
 private int readDataSourcePollPattern = 0;
 private int readDsSize;
 private boolean defaultAutoCommit = true;
 private int defaultTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED;
 public static final String READ = "read";
 public static final String WRITE = "write";
 private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
 @Override
 public Connection getConnection() throws SQLException {
 return (Connection) Proxy.newProxyInstance(
com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
  new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
  new RWConnectionInvocationHandler());
 }
 @Override
 public Connection getConnection(String username, String password)
  throws SQLException {
 return (Connection) Proxy.newProxyInstance( com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class.getClassLoader(),
  new Class[] {com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy.class},
  new RWConnectionInvocationHandler(username,password));
 }
 public int getReadDsSize(){
 return readDsSize;
 }
 public List<DataSource> getResolvedReadDataSources() {
 return resolvedReadDataSources;
 }
 public void afterPropertiesSet() throws Exception {
 if(writeDataSource == null){
  throw new IllegalArgumentException("Property 'writeDataSource' is required");
 }
 this.resolvedWriteDataSource = resolveSpecifiedDataSource(writeDataSource);
 resolvedReadDataSources = new ArrayList<DataSource>(readDataSources.size());
 for(Object item : readDataSources){
  resolvedReadDataSources.add(resolveSpecifiedDataSource(item));
 }
 readDsSize = readDataSources.size();
 }
 protected DataSource determineTargetDataSource(String key) {
 Assert.notNull(this.resolvedReadDataSources, "DataSource router not initialized");
 if(WRITE.equals(key)){
  return resolvedWriteDataSource;
 }else{
  return loadReadDataSource();
 }
 }
 public Logger getParentLogger() {
 // NOOP Just ignore
 return null;
 }
 /**
 * 获取真实的data source
 * @param dataSource (jndi | real data source)
 * @return
 * @throws IllegalArgumentException
 */
 protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
 if (dataSource instanceof DataSource) {
  return (DataSource) dataSource;
 }
 else if (dataSource instanceof String) {
  return this.dataSourceLookup.getDataSource((String) dataSource);
 }
 else {
  throw new IllegalArgumentException(
   "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
 }
 }
 protected abstract DataSource loadReadDataSource();
 public void setReadDsSize(int readDsSize) {
 this.readDsSize = readDsSize;
 }
 public List<Object> getReadDataSources() {
 return readDataSources;
 }
 public void setReadDataSources(List<Object> readDataSources) {
 this.readDataSources = readDataSources;
 }
 public Object getWriteDataSource() {
 return writeDataSource;
 }
 public void setWriteDataSource(Object writeDataSource) {
 this.writeDataSource = writeDataSource;
 }
 public void setResolvedReadDataSources(List<DataSource> resolvedReadDataSources) {
 this.resolvedReadDataSources = resolvedReadDataSources;
 }
 public DataSource getResolvedWriteDataSource() {
 return resolvedWriteDataSource;
 }
 public void setResolvedWriteDataSource(DataSource resolvedWriteDataSource) {
 this.resolvedWriteDataSource = resolvedWriteDataSource;
 }
 public int getReadDataSourcePollPattern() {
 return readDataSourcePollPattern;
 }
 public void setReadDataSourcePollPattern(int readDataSourcePollPattern) {
 this.readDataSourcePollPattern = readDataSourcePollPattern;
 }
 /**
 * Invocation handler that defers fetching an actual JDBC Connection
 * until first creation of a Statement.
 */
 private class RWConnectionInvocationHandler implements InvocationHandler {
 private String username;
 private String password;
 private Boolean readOnly = Boolean.FALSE;
 private Integer transactionIsolation;
 private Boolean autoCommit;
 private boolean closed = false;
 private Connection target;
 public RWConnectionInvocationHandler() {
 }
 public RWConnectionInvocationHandler(String username, String password) {
  this();
  this.username = username;
  this.password = password;
 }
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  // Invocation on ConnectionProxy interface coming in...
  if (method.getName().equals("equals")) {
  // We must avoid fetching a target Connection for "equals".
  // Only consider equal when proxies are identical.
  return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE);
  }
  else if (method.getName().equals("hashCode")) {
  // We must avoid fetching a target Connection for "hashCode",
  // and we must return the same hash code even when the target
  // Connection has been fetched: use hashCode of Connection proxy.
  return new Integer(System.identityHashCode(proxy));
  }
  else if (method.getName().equals("getTargetConnection")) {
  // Handle getTargetConnection method: return underlying connection.
  return getTargetConnection(method,args);
  }
  if (!hasTargetConnection()) {
  // No physical target Connection kept yet ->
  // resolve transaction demarcation methods without fetching
  // a physical JDBC Connection until absolutely necessary.
  if (method.getName().equals("toString")) {
   return "RW Routing DataSource Proxy";
  }
  else if (method.getName().equals("isReadOnly")) {
   return this.readOnly;
  }
  else if (method.getName().equals("setReadOnly")) {
   this.readOnly = (Boolean) args[0];
   return null;
  }
  else if (method.getName().equals("getTransactionIsolation")) {
   if (this.transactionIsolation != null) {
   return this.transactionIsolation;
   }
   return defaultTransactionIsolation;
   // Else fetch actual Connection and check there,
   // because we didn't have a default specified.
  }
  else if (method.getName().equals("setTransactionIsolation")) {
   this.transactionIsolation = (Integer) args[0];
   return null;
  }
  else if (method.getName().equals("getAutoCommit")) {
   if (this.autoCommit != null)
   return this.autoCommit;
   return defaultAutoCommit;
   // Else fetch actual Connection and check there,
   // because we didn't have a default specified.
  }
  else if (method.getName().equals("setAutoCommit")) {
   this.autoCommit = (Boolean) args[0];
   return null;
  }
  else if (method.getName().equals("commit")) {
   // Ignore: no statements created yet.
   return null;
  }
  else if (method.getName().equals("rollback")) {
   // Ignore: no statements created yet.
   return null;
  }
  else if (method.getName().equals("getWarnings")) {
   return null;
  }
  else if (method.getName().equals("clearWarnings")) {
   return null;
  }
  else if (method.getName().equals("isClosed")) {
   return (this.closed ? Boolean.TRUE : Boolean.FALSE);
  }
  else if (method.getName().equals("close")) {
   // Ignore: no target connection yet.
   this.closed = true;
   return null;
  }
  else if (this.closed) {
   // Connection proxy closed, without ever having fetched a
   // physical JDBC Connection: throw corresponding SQLException.
   throw new SQLException("Illegal operation: connection is closed");
  }
  }
  // Target Connection already fetched,
  // or target Connection necessary for current operation ->
  // invoke method on target connection.
  try {
  return method.invoke(target, args);
  }
  catch (InvocationTargetException ex) {
  throw ex.getTargetException();
  }
 }
 /**
  * Return whether the proxy currently holds a target Connection.
  */
 private boolean hasTargetConnection() {
  return (this.target != null);
 }
 /**
  * Return the target Connection, fetching it and initializing it if necessary.
  */
 private Connection getTargetConnection(Method operation,Object[] args) throws SQLException {
  if (this.target == null) {
  String key = (String) args[0];
  // No target Connection held -> fetch one.
  if (logger.isDebugEnabled()) {
   logger.debug("Connecting to database for operation '" + operation.getName() + "'");
  }
  // Fetch physical Connection from DataSource.
  this.target = (this.username != null) ?
   determineTargetDataSource(key).getConnection(this.username, this.password) :
   determineTargetDataSource(key).getConnection();
  // If we still lack default connection properties, check them now.
  //checkDefaultConnectionProperties(this.target);
  // Apply kept transaction settings, if any.
  if (this.readOnly.booleanValue()) {
   this.target.setReadOnly(this.readOnly.booleanValue());
  }
  if (this.transactionIsolation != null) {
this.target.setTransactionIsolation(this.transactionIsolation.intValue());
  }
  if (this.autoCommit != null && this.autoCommit.booleanValue() != this.target.getAutoCommit()) {
   this.target.setAutoCommit(this.autoCommit.booleanValue());
  }
  }
  else {
  // Target Connection already held -> return it.
  if (logger.isDebugEnabled()) {
   logger.debug("Using existing database connection for operation '" + operation.getName() + "'");
  }
  }
  return this.target;
 }
 }
}
import javax.sql.DataSource;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-19 16:04
 * Desc:
 */
public class DynamicRoutingDataSourceProxy extends AbstractDynamicDataSourceProxy {
 private AtomicLong counter = new AtomicLong(0);
 private static final Long MAX_POOL = Long.MAX_VALUE;
 private final Lock lock = new ReentrantLock();
 @Override
 protected DataSource loadReadDataSource() {
 int index = 1;
 if(getReadDataSourcePollPattern() == 1) {
  //轮询方式
  long currValue = counter.incrementAndGet();
  if((currValue + 1) >= MAX_POOL) {
  try {
   lock.lock();
   if((currValue + 1) >= MAX_POOL) {
   counter.set(0);
   }
  } finally {
   lock.unlock();
  }
  }
  index = (int) (currValue % getReadDsSize());
 } else {
  //随机方式
  index = ThreadLocalRandom.current().nextInt(0, getReadDsSize());
 }
 return getResolvedReadDataSources().get(index);
 }
}
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
/**
 * 拦截器
 */
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) })
public class DynamicPlugin implements Interceptor {
 public Object intercept(Invocation invocation) throws Throwable {
 Connection conn = (Connection)invocation.getArgs()[0];
 //如果是采用了我们代理,则路由数据源
 if(conn instanceof com.autohome.api.dealer.tuan.dao.rwmybatis.ConnectionProxy){
  StatementHandler statementHandler = (StatementHandler) invocation
   .getTarget();
  MappedStatement mappedStatement = null;
  if (statementHandler instanceof RoutingStatementHandler) {
  StatementHandler delegate = (StatementHandler) ReflectionUtils
   .getFieldValue(statementHandler, "delegate");
  mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
   delegate, "mappedStatement");
  } else {
  mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(
statementHandler, "mappedStatement");
  }
  String key = AbstractDynamicDataSourceProxy.WRITE;
  if(mappedStatement.getSqlCommandType() == SqlCommandType.SELECT){
  key = AbstractDynamicDataSourceProxy.READ;
  }else{
  key = AbstractDynamicDataSourceProxy.WRITE;
  }
  ConnectionProxy connectionProxy = (ConnectionProxy)conn;
  connectionProxy.getTargetConnection(key);
 }
 return invocation.proceed();
 }
 public Object plugin(Object target) {
 return Plugin.wrap(target, this);
 }
 public void setProperties(Properties properties) {
 //NOOP
 }
}
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import java.lang.reflect.*;
public class ReflectionUtils {
 private static final Log logger = LogFactory.getLog(ReflectionUtils.class);
 /**
 * 直接设置对象属性值,无视private/protected修饰符,不经过setter函数.
 */
 public static void setFieldValue(final Object object, final String fieldName, final Object value) {
 Field field = getDeclaredField(object, fieldName);
 if (field == null)
  throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
 makeAccessible(field);
 try {
  field.set(object, value);
 } catch (IllegalAccessException e) {
 }
 }
 /**
 * 直接读取对象属性值,无视private/protected修饰符,不经过getter函数.
 */
 public static Object getFieldValue(final Object object, final String fieldName) {
 Field field = getDeclaredField(object, fieldName);
 if (field == null)
  throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + object + "]");
 makeAccessible(field);
 Object result = null;
 try {
  result = field.get(object);
 } catch (IllegalAccessException e) {
 }
 return result;
 }
 /**
 * 直接调用对象方法,无视private/protected修饰符.
 */
 public static Object invokeMethod(final Object object, final String methodName, final Class<?>[] parameterTypes,
  final Object[] parameters) throws InvocationTargetException {
 Method method = getDeclaredMethod(object, methodName, parameterTypes);
 if (method == null)
  throw new IllegalArgumentException("Could not find method [" + methodName + "] on target [" + object + "]");
 method.setAccessible(true);
 try {
  return method.invoke(object, parameters);
 } catch (IllegalAccessException e) {
 }
 return null;
 }
 /**
 * 循环向上转型,获取对象的DeclaredField.
 */
 protected static Field getDeclaredField(final Object object, final String fieldName) {
 for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass
  .getSuperclass()) {
  try {
  return superClass.getDeclaredField(fieldName);
  } catch (NoSuchFieldException e) {
  }
 }
 return null;
 }
 /**
 * 循环向上转型,获取对象的DeclaredField.
 */
 protected static void makeAccessible(final Field field) {
 if (!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers())) {
  field.setAccessible(true);
 }
 }
 /**
 * 循环向上转型,获取对象的DeclaredMethod.
 */
 protected static Method getDeclaredMethod(Object object, String methodName, Class<?>[] parameterTypes) {
 for (Class<?> superClass = object.getClass(); superClass != Object.class; superClass = superClass
  .getSuperclass()) {
  try {
  return superClass.getDeclaredMethod(methodName, parameterTypes);
  } catch (NoSuchMethodException e) {
  }
 }
 return null;
 }
 /**
 * 通过反射,获得Class定义中声明的父类的泛型参数的类型.
 * eg.
 * public UserDao extends HibernateDao<User>
 *
 * @param clazz The class to introspect
 * @return the first generic declaration, or Object.class if cannot be determined
 */
 @SuppressWarnings("unchecked")
 public static <T> Class<T> getSuperClassGenricType(final Class clazz) {
 return getSuperClassGenricType(clazz, 0);
 }
 /**
 * 通过反射,获得Class定义中声明的父类的泛型参数的类型.
 * eg.
 * public UserDao extends HibernateDao<User>
 *
 * @param clazz The class to introspect
 * @return the first generic declaration, or Object.class if cannot be determined
 */
 @SuppressWarnings("unchecked")
 public static Class getSuperClassGenricType(final Class clazz, final int index) {
 Type genType = clazz.getGenericSuperclass();
 if (!(genType instanceof ParameterizedType)) {
  logger.warn(clazz.getSimpleName() + "'s superclass not ParameterizedType");
  return Object.class;
 }
 Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
 if (index >= params.length || index < 0) {
  logger.warn("Index: " + index + ", Size of " + clazz.getSimpleName() + "'s Parameterized Type: "
   + params.length);
  return Object.class;
 }
 if (!(params[index] instanceof Class)) {
  logger.warn(clazz.getSimpleName() + " not set the actual class on superclass generic parameter");
  return Object.class;
 }
 return (Class) params[index];
 }
 /**
 * 将反射时的checked exception转换为unchecked exception.
 */
 public static IllegalArgumentException convertToUncheckedException(Exception e) {
 if (e instanceof IllegalAccessException || e instanceof IllegalArgumentException
  || e instanceof NoSuchMethodException)
  return new IllegalArgumentException("Refelction Exception.", e);
 else
  return new IllegalArgumentException(e);
 }
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD SQL Map Config 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
 <plugins>
  <plugin interceptor="com.test.api.dao.mybatis.DynamicPlugin">
  </plugin>
 </plugins>
</configuration>
<?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">
 <bean id="abstractDataSource" abstract="true" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
  <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
  <!-- 配置获取连接等待超时的时间 -->
  <property name="maxWait" value="60000"/>
  <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
  <property name="timeBetweenEvictionRunsMillis" value="60000"/>
  <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
  <property name="minEvictableIdleTimeMillis" value="300000"/>
  <property name="validationQuery" value="SELECT 'x'"/>
  <property name="testWhileIdle" value="true"/>
  <property name="testOnBorrow" value="false"/>
  <property name="testOnReturn" value="false"/>
  <!-- 打开PSCache,并且指定每个连接上PSCache的大小 -->
  <property name="poolPreparedStatements" value="true"/>
  <property name="maxPoolPreparedStatementPerConnectionSize" value="20"/>
  <property name="filters" value="config"/>
  <property name="connectionProperties" value="config.decrypt=true" />
 </bean>
 <bean id="dataSourceRead1" parent="abstractDataSource">
  <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
  <!-- 基本属性 url、user、password -->
  <property name="url" value="${read1.jdbc.url}"/>
  <property name="username" value="${read1.jdbc.user}"/>
  <property name="password" value="${read1.jdbc.password}"/>
  <!-- 配置初始化大小、最小、最大 -->
  <property name="initialSize" value="${read1.jdbc.initPoolSize}"/>
  <property name="minIdle" value="${read1.jdbc.minPoolSize}"/>
  <property name="maxActive" value="${read1.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSourceRead2" parent="abstractDataSource">
  <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
  <!-- 基本属性 url、user、password -->
  <property name="url" value="${read2.jdbc.url}"/>
  <property name="username" value="${read2.jdbc.user}"/>
  <property name="password" value="${read2.jdbc.password}"/>
  <!-- 配置初始化大小、最小、最大 -->
  <property name="initialSize" value="${read2.jdbc.initPoolSize}"/>
  <property name="minIdle" value="${read2.jdbc.minPoolSize}"/>
  <property name="maxActive" value="${read2.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSourceWrite" parent="abstractDataSource">
  <property name="driverClassName" value="com.microsoft.sqlserver.jdbc.SQLServerDriver"/>
  <!-- 基本属性 url、user、password -->
  <property name="url" value="${write.jdbc.url}"/>
  <property name="username" value="${write.jdbc.user}"/>
  <property name="password" value="${write.jdbc.password}"/>
  <!-- 配置初始化大小、最小、最大 -->
  <property name="initialSize" value="${write.jdbc.initPoolSize}"/>
  <property name="minIdle" value="${write.jdbc.minPoolSize}"/>
  <property name="maxActive" value="${write.jdbc.maxPoolSize}"/>
 </bean>
 <bean id="dataSource" class="com.test.api.dao.datasource.DynamicRoutingDataSourceProxy">
  <property name="writeDataSource" ref="dataSourceWrite" />
  <property name="readDataSources">
   <list>
    <ref bean="dataSourceRead1" />
    <ref bean="dataSourceRead2" />
   </list>
  </property>
  <!--轮询方式-->
  <property name="readDataSourcePollPattern" value="1" />
 </bean>
 <tx:annotation-driven transaction-manager="transactionManager"/>
 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource"/>
 </bean>
 <!-- 针对myBatis的配置项 -->
 <!-- 配置sqlSessionFactory -->
 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->
  <property name="dataSource" ref="dataSource"/>
  <property name="mapperLocations" value="classpath:mapper/*.xml"/>
  <property name="configLocation" value="classpath:mybatis-plugin-config.xml" />
 </bean>
 <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg ref="sqlSessionFactory" />
 </bean>
 <!-- 通过扫描的模式,扫描目录下所有的mapper, 根据对应的mapper.xml为其生成代理类-->
 <bean id="mapper" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  <property name="basePackage" value="com.test.api.dao.inte" />
  <property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
 </bean>
</beans>

方案4

如果你的后台结构是spring+mybatis,可以通过spring的AbstractRoutingDataSource和mybatis Plugin拦截器实现非常友好的读写分离,原有代码不需要任何改变。推荐第四种方案

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.HashMap;
import java.util.Map;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-14 10:56
 * Desc: 动态数据源实现读写分离
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
 private Object writeDataSource; //写数据源
 private Object readDataSource; //读数据源
 @Override
 public void afterPropertiesSet() {
  if (this.writeDataSource == null) {
   throw new IllegalArgumentException("Property 'writeDataSource' is required");
  }
  setDefaultTargetDataSource(writeDataSource);
  Map<Object, Object> targetDataSources = new HashMap<>();
  targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
  if(readDataSource != null) {
   targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource);
  }
  setTargetDataSources(targetDataSources);
  super.afterPropertiesSet();
 }
 @Override
 protected Object determineCurrentLookupKey() {
  DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
  if(dynamicDataSourceGlobal == null
    || dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
   return DynamicDataSourceGlobal.WRITE.name();
  }
  return DynamicDataSourceGlobal.READ.name();
 }
 public void setWriteDataSource(Object writeDataSource) {
  this.writeDataSource = writeDataSource;
 }
 public Object getWriteDataSource() {
  return writeDataSource;
 }
 public Object getReadDataSource() {
  return readDataSource;
 }
 public void setReadDataSource(Object readDataSource) {
  this.readDataSource = readDataSource;
 }
}
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-07-14 10:58
 * Desc:
 */
public enum DynamicDataSourceGlobal {
 READ, WRITE;
}
public final class DynamicDataSourceHolder {
 private static final ThreadLocal<DynamicDataSourceGlobal> holder = new ThreadLocal<DynamicDataSourceGlobal>();
 private DynamicDataSourceHolder() {
  //
 }
 public static void putDataSource(DynamicDataSourceGlobal dataSource){
  holder.set(dataSource);
 }
 public static DynamicDataSourceGlobal getDataSource(){
  return holder.get();
 }
 public static void clearDataSource() {
  holder.remove();
 }
}
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-08-10 14:34
 * Desc:
 */
public class DynamicDataSourceTransactionManager extends DataSourceTransactionManager {
 /**
  * 只读事务到读库,读写事务到写库
  * @param transaction
  * @param definition
  */
 @Override
 protected void doBegin(Object transaction, TransactionDefinition definition) {
  //设置数据源
  boolean readOnly = definition.isReadOnly();
  if(readOnly) {
   DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.READ);
  } else {
   DynamicDataSourceHolder.putDataSource(DynamicDataSourceGlobal.WRITE);
  }
  super.doBegin(transaction, definition);
 }
 /**
  * 清理本地线程的数据源
  * @param transaction
  */
 @Override
 protected void doCleanupAfterCompletion(Object transaction) {
  super.doCleanupAfterCompletion(transaction);
  DynamicDataSourceHolder.clearDataSource();
 }
}
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
 * Created by IDEA
 * User: mashaohua
 * Date: 2016-08-10 11:09
 * Desc:
 */
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
  MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
  MappedStatement.class, Object.class, RowBounds.class,
  ResultHandler.class }) })
public class DynamicPlugin implements Interceptor {
 protected static final Logger logger = LoggerFactory.getLogger(DynamicPlugin.class);
 private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
 private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>();
 @Override
 public Object intercept(Invocation invocation) throws Throwable {
  boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
  if(!synchronizationActive) {
   Object[] objects = invocation.getArgs();
   MappedStatement ms = (MappedStatement) objects[0];
   DynamicDataSourceGlobal dynamicDataSourceGlobal = null;
   if((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == null) {
    //读方法
    if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
     //!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
     if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
      dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
     } else {
      BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
      String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
      if(sql.matches(REGEX)) {
       dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
      } else {
       dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
      }
     }
    }else{
     dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
    }
    logger.warn("设置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), dynamicDataSourceGlobal.name(), ms.getSqlCommandType().name());
    cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
   }
   DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
  }
  return invocation.proceed();
 }
 @Override
 public Object plugin(Object target) {
  if (target instanceof Executor) {
   return Plugin.wrap(target, this);
  } else {
   return target;
  }
 }
 @Override
 public void setProperties(Properties properties) {
  //
 }
}

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • Spring+Mybatis 实现aop数据库读写分离与多数据库源配置操作

    在数据库层面大都采用读写分离技术,就是一个Master数据库,多个Slave数据库.Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询.因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验.我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力. 废话不多说,多数据源配置和主从数据配置原理一样 1.首先配置  jdbc.prope

  • Spring Boot 集成Mybatis实现主从(多数据源)分离方案示例

    本文将介绍使用Spring Boot集成Mybatis并实现主从库分离的实现(同样适用于多数据源).延续之前的Spring Boot 集成MyBatis.项目还将集成分页插件PageHelper.通用Mapper以及Druid. 新建一个Maven项目,最终项目结构如下: 多数据源注入到sqlSessionFactory POM增加如下依赖: <!--JSON--> <dependency> <groupId>com.fasterxml.jackson.core<

  • Spring+MyBatis实现数据读写分离的实例代码

    本文介绍了Spring Boot + MyBatis读写分离,有需要了解Spring+MyBatis读写分离的朋友可参考.希望此文章对各位有所帮助. 其最终实现功能: 默认更新操作都使用写数据源 读操作都使用slave数据源 特殊设置:可以指定要使用的数据源类型及名称(如果有名称,则会根据名称使用相应的数据源) 其实现原理如下: 通过Spring AOP对dao层接口进行拦截,并对需要指定数据源的接口在ThradLocal中设置其数据源类型及名称 通过MyBatsi的插件,对根据更新或者查询操作

  • SpringMVC4+MyBatis+SQL Server2014实现数据库读写分离

    前言 基于mybatis的AbstractRoutingDataSource和Interceptor用拦截器的方式实现读写分离,根据MappedStatement的boundsql,查询sql的select.insert.update.delete,根据起判断使用读写连接串. 开发环境 SpringMVC4.mybatis3 项目结构 读写分离实现 1.pom.xml <dependencies> <dependency> <groupId>junit</grou

  • Spring+MyBatis实现数据库读写分离方案

    推荐第四种 方案1 通过MyBatis配置文件创建读写分离两个DataSource,每个SqlSessionFactoryBean对象的mapperLocations属性制定两个读写数据源的配置文件.将所有读的操作配置在读文件中,所有写的操作配置在写文件中. 优点:实现简单 缺点:维护麻烦,需要对原有的xml文件进行重新修改,不支持多读,不易扩展 实现方式 <bean id="abstractDataSource" abstract="true" class=

  • mysql+spring+mybatis实现数据库读写分离的代码配置

    场景:一个读数据源一个读写数据源. 原理:借助spring的[org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource]这个抽象类实现,看名字可以了解到是一个路由数据源的东西,这个类中有一个方法 /** * Determine the current lookup key. This will typically be * implemented to check a thread-bound transaction

  • Discuz!NT数据库读写分离方案详解

    目前在Discuz!NT这个产品中,数据库作为数据持久化工具,必定在并发访问频繁且负载压力较大的情况下成 为系统性能的'瓶颈'.即使使用本地缓存等方式来解决频繁访问数据库的问题,但仍旧会有大量的并发请求要访问动态数据,虽然 SQL2005及2008以上版本中性能不断提升,查询计划和存储过程运行得越来越高效,但最终还是 要面临'瓶颈'这一问 题.当然这也是许多大型网站不断研究探索各式各样的方案来有效降低数据访问负荷的原 因, 其中的'读写分离'方案就是一种被广泛采用的方案.      Discuz

  • Spring boot实现数据库读写分离的方法

    背景 数据库配置主从之后,如何在代码层面实现读写分离? 用户自定义设置数据库路由 Spring boot提供了AbstractRoutingDataSource根据用户定义的规则选择当前的数据库,这样我们可以在执行查询之前,设置读取从库,在执行完成后,恢复到主库. 实现可动态路由的数据源,在每次数据库查询操作前执行 ReadWriteSplitRoutingDataSource.java import org.springframework.jdbc.datasource.lookup.Abst

  • spring集成mybatis实现mysql数据库读写分离

    前言 在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈.幸运的是目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库的数据更新同步到另一台服务器上.网站利用数据库的这一功能,实现数据库读写分离,从而改善数据库负载压力.如下图所示: 应用服务器在写数据的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库,这样当应用服务器读数据的时候,就可以通过从数据库获得数据.为了便于应用程序访问读写分离后的数据库,通常在应用服务器使用专门的数

  • Spring AOP切面解决数据库读写分离实例详解

    Spring AOP切面解决数据库读写分离实例详解 为了减轻数据库的压力,一般会使用数据库主从(master/slave)的方式,但是这种方式会给应用程序带来一定的麻烦,比如说,应用程序如何做到把数据写到master库,而读取数据的时候,从slave库读取.如果应用程序判断失误,把数据写入到slave库,会给系统造成致命的打击. 解决读写分离的方案很多,常用的有SQL解析.动态设置数据源.SQL解析主要是通过分析sql语句是insert/select/update/delete中的哪一种,从而对

  • 使用Spring AOP实现MySQL数据库读写分离案例分析(附demo)

     一.前言 分布式环境下数据库的读写分离策略是解决数据库读写性能瓶颈的一个关键解决方案,更是最大限度了提高了应用中读取 (Read)数据的速度和并发量. 在进行数据库读写分离的时候,我们首先要进行数据库的主从配置,最简单的是一台Master和一台Slave(大型网站系统的话,当然会很复杂,这里只是分析了最简单的情况).通过主从配置主从数据库保持了相同的数据,我们在进行读操作的时候访问从数据库Slave,在进行写操作的时候访问主数据库Master.这样的话就减轻了一台服务器的压力. 在进行读写分离

  • SpringBoot+Mybatis-Plus实现mysql读写分离方案的示例代码

    1. 引入mybatis-plus相关包,pom.xml文件 2. 配置文件application.property增加多库配置 mysql 数据源配置 spring.datasource.primary.jdbc-url=jdbc:mysql://xx.xx.xx.xx:3306/portal?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=

随机推荐