互联网架构演进中 数据库优化是不可或缺的一环,数据库层面优化分为两个阶段:读写分离、分库分表。
今天要说的是 数据库读写分离技术,其原理就是一个Master数据库,多个Slave数据库。Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询。因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验。
我们通常的做法就是把查询从主库中抽取出来,分发到多个从库上,减轻主库的压力。
采用读写分离技术的目标:有效减轻Master库的压力,又可以把用户查询数据的请求分发到不同的Slave库,从而保证系统的健壮性。
在DAO实现类的方法加上@RoutingDataSource注解,然后通过Spring AOP技术在运行时拦截DAO的方法,获取方法上的@RoutingDataSource注解值动态切换数据源。
1、spring-dao.xml
<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:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
default-lazy-init="false">
<bean id="parentDataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close" abstract="true" init-method="init" >
<property name="initialSize" value="2" />
<property name="maxActive" value="10" />
<property name="minIdle" value="5" />
<property name="maxWait" value="30000" />
<property name="validationQuery" value="SELECT 1" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="testWhileIdle" value="true" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="25200000" />
<property name="removeAbandoned" value="true" />
<property name="removeAbandonedTimeout" value="1800" />
<property name="logAbandoned" value="true" />
<property name="filters" value="mergeStat" />
bean>
<bean id="masterDataSource" parent="parentDataSource">
<property name="url" value="#{jdbc['master.jdbc.url']}" />
<property name="username" value="#{jdbc['master.jdbc.username']}" />
<property name="password" value="#{jdbc['master.jdbc.password']}" />
<property name="driverClassName" value="#{jdbc['master.jdbc.driver']}" />
<property name="maxActive" value="15" />
bean>
<bean id="slaveDataSource" parent="parentDataSource">
<property name="url" value="#{jdbc['slave1.jdbc.url']}" />
<property name="username" value="#{jdbc['slave1.jdbc.username']}" />
<property name="password" value="#{jdbc['slave1.jdbc.password']}" />
<property name="driverClassName" value="#{jdbc['slave1.jdbc.driver']}" />
bean>
<bean id="dataSource" class="com.bytebeats.spring4.sample.ds.DynamicRoutingDataSource">
<property name="targetDataSources">
<map key-type="com.bytebeats.spring4.sample.ds.RoutingStrategy">
<entry key="WRITE" value-ref="masterDataSource"/>
<entry key="READ" value-ref="slaveDataSource"/>
map>
property>
<property name="defaultTargetDataSource" ref="masterDataSource"/>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="masterDataSource" />
bean>
beans>
2、动态数据源实现
DynamicRoutingDataSource.java
package com.bytebeats.spring4.sample.ds;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态数据源
*
* @author Ricky Fung
* @create 2016-12-30 11:12
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicRoutingContextHolder.getRouteStrategy();
}
}
DynamicRoutingContextHolder.java
package com.bytebeats.spring4.sample.ds;
import org.springframework.util.Assert;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-12-30 11:14
*/
public class DynamicRoutingContextHolder {
private static final ThreadLocal contextHolder =
new ThreadLocal<>();
public static void setRouteStrategy(RoutingStrategy customerType) {
Assert.notNull(customerType, "customerType cannot be null");
contextHolder.set(customerType);
}
public static RoutingStrategy getRouteStrategy() {
return (RoutingStrategy) contextHolder.get();
}
public static void clearRouteStrategy() {
contextHolder.remove();
}
}
RoutingStrategy.java
package com.bytebeats.spring4.sample.ds;
/**
* 读写策略
*
* @author Ricky Fung
* @create 2016-12-30 11:14
*/
public enum RoutingStrategy {
WRITE, READ;
}
package com.bytebeats.spring4.sample.annotation;
import com.bytebeats.spring4.sample.ds.RoutingStrategy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @date 2016-12-30 15:26
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoutingDataSource {
RoutingStrategy value();
}
1、spring-aop.xml
<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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
<aop:aspectj-autoproxy />
<bean id="readWriteSeparateAspect" class="com.bytebeats.spring4.sample.aop.DBReadWriteSeparateAspect" />
<aop:config>
<aop:aspect ref="readWriteSeparateAspect">
<aop:pointcut id="rw" expression="execution(* com.bytebeats.spring4.sample.dao.*.*(..))" />
<aop:before pointcut-ref="rw" method="before" />
aop:aspect>
aop:config>
beans>
2、切面类
package com.bytebeats.spring4.sample.aop;
import com.bytebeats.spring4.sample.annotation.RoutingDataSource;
import com.bytebeats.spring4.sample.ds.DynamicRoutingContextHolder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @date 2016-12-30 16:59
*/
public class DBReadWriteSeparateAspect {
private final Logger logger = LoggerFactory.getLogger(getClass());
public void before(JoinPoint point) {
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class> clazz = target.getClass();
logger.info("before class:{} method:{} execute", clazz.getName(), methodName);
Class>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
try {
Method method = clazz.getMethod(methodName, parameterTypes);
if (method != null && method.isAnnotationPresent(RoutingDataSource.class)) {
RoutingDataSource data = method.getAnnotation(RoutingDataSource.class);
DynamicRoutingContextHolder.setRouteStrategy(data.value());
logger.info("class:{} method:{} 切换数据源:{} 成功", clazz.getName(), methodName, data.value());
}
} catch (Exception e) {
logger.error("数据源切换切面异常", e);
}
}
}
假设有一个订单库,现在要实现订单表的插入和查询,IOrderDao接口定义如下:
package com.bytebeats.spring4.sample.dao;
import com.bytebeats.spring4.sample.domain.Order;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-12-30 11:15
*/
public interface IOrderDao {
long insert(Order order);
List queryOrders();
}
我们期望insert路由到主库上,queryOrders路由到从库上,那么我们只需要在这两个方法上增加@RoutingDataSource注解即可,OrderDaoImpl 代码如下:
package com.bytebeats.spring4.sample.dao.impl;
import com.bytebeats.spring4.sample.annotation.RoutingDataSource;
import com.bytebeats.spring4.sample.dao.IOrderDao;
import com.bytebeats.spring4.sample.domain.Order;
import com.bytebeats.spring4.sample.ds.RoutingStrategy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
/**
* ${DESCRIPTION}
*
* @author Ricky Fung
* @create 2016-12-30 11:16
*/
@Repository("orderDao")
public class OrderDaoImpl implements IOrderDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@RoutingDataSource(RoutingStrategy.WRITE)
@Override
public long insert(Order order) {
String sql = "INSERT INTO tb_order(customer_name,total_price,amount,address) VALUES (?,?,?,?)";
return jdbcTemplate.update(sql, order.getCustomerName(),
order.getTotalPrice(), order.getAmount(), order.getAddress());
}
@RoutingDataSource(RoutingStrategy.READ)
@Override
public List queryOrders(){
return jdbcTemplate.query("SELECT * FROM tb_order", new RowMapper() {
@Override
public Order mapRow(ResultSet rs, int i) throws SQLException {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setCustomerName(rs.getString("customer_name"));
order.setTotalPrice(rs.getDouble("total_price"));
order.setAmount(rs.getInt("amount"));
order.setAddress(rs.getString("address"));
return order;
}
});
}
}
点此下载源码:https://github.com/TiFG/spring4-in-action/tree/master/spring-ch5-rw