Spring AOP 实现数据库读写分离

互联网架构演进中,数据库往往是一个瓶颈,对其进行优化是不可或缺的一环,数据库层面优化分为两个阶段:读写分离、分库分表。

今天要说的是 数据库读写分离技术,其原理就是一个Master数据库,多个Slave数据库。Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询。因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验。
我们通常的做法就是把查询从主库中抽取出来,分发到多个从库上,减轻主库的压力。 采用读写分离技术的目标:有效减轻Master库的压力,又可以把用户查询数据的请求分发到不同的Slave库,从而保证系统的健壮性。

实现原理

在DAO的方法加上@DataSource注解,然后通过Spring AOP技术在运行时拦截DAO的方法,获取方法上的@DataSource注解值动态切换数据源。

实现

package com.bytebeats.spring4.sample.ds;

/**
 * 读写策略
 *
 * @author Ricky Fung
 * @create 2017-04-03 11:12
 */
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 2017-04-03 11:13
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSource {
    RoutingStrategy value();
}

Spring JDBC配置

MultipleDataSource源码如下:

package com.bytebeats.spring4.sample.ds;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源
 *
 * @author Ricky Fung
 * @create 2017-04-03 11:12
 */
public class MultipleDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {

        return DynamicRoutingContextHolder.getRouteStrategy();
    }
}

DynamicRoutingContextHolder

package com.bytebeats.spring4.sample.ds;

import org.springframework.util.Assert;

/**
 * ${DESCRIPTION}
 *
 * @author Ricky Fung
 * @create 2017-04-03 11:14
 */
public class DynamicRoutingContextHolder {
    private static final ThreadLocal<RoutingStrategy> 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();
    }
}

最后,来看看 spring-dao.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: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="poolPreparedStatements" value="true" /> -->
        <!-- <property name="maxPoolPreparedStatementPerConnectionSize" value="33" /> -->
        <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" />
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true" />
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="1800" />
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true" />
        <!-- 监控数据库 -->
        <!-- <property name="filters" value="stat" /> -->
        <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.MultipleDataSource">
        <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>

    <!--Spring JdbcTemplate-->
    <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="dataSource" />
    </bean>

</beans>

Spring AOP配置

定义切面:

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 2017-04-03 16:59
 */
public class DBReadWriteInterceptor {

    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);
        }
    }
}

spring-aop.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: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 -->
    <aop:aspectj-autoproxy />

    <bean id="readWriteInterceptor" class="com.bytebeats.spring4.sample.aop.DBReadWriteInterceptor" />

    <aop:config>
        <aop:aspect ref="readWriteInterceptor">
            <aop:pointcut id="rw" expression="execution(* com.bytebeats.spring4.sample.dao.*.*(..))" />
            <aop:before pointcut-ref="rw" method="before" />
        </aop:aspect>
    </aop:config>
</beans>

如何使用

假设有一个订单库,现在要实现订单表的插入和查询,OrderDao接口定义如下:

package com.bytebeats.spring4.sample.dao;

import com.bytebeats.spring4.sample.domain.Order;
import java.util.List;

/**
 * ${DESCRIPTION}
 *
 * @author Ricky Fung
 * @create 2017-04-03 12:12
 */
public interface OrderDao {

    long insert(Order order);

    List<Order> 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 2017-04-03 12:14
 */
@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<Order> queryOrders(){

        return jdbcTemplate.query("SELECT * FROM tb_order", new RowMapper<Order>() {
            @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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容