美文网首页
Spring AOP 实现数据库读写分离

Spring AOP 实现数据库读写分离

作者: FX_SKY | 来源:发表于2017-04-01 23:19 被阅读278次

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

    今天要说的是 数据库读写分离技术,其原理就是一个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

    相关文章

      网友评论

          本文标题:Spring AOP 实现数据库读写分离

          本文链接:https://www.haomeiwen.com/subject/eumwottx.html