技术架构性能优化数据库

Mysql读写分离实践

2019-01-27  本文已影响47人  知止9528

有使用注解,或者使用代理的.

首先是代码层面的
由于简单,使用了注解的方式来做读写分离
后面会讲mysql层面主从的配置

Spring为我们提供了一个类org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
核心思想就是通过AOP拿到注解上的数据源,然后在AbstractRoutingDataSource 的实现类中将数据源返回对应的数据源

public class CustomerRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        LocalDS ds = CustomerContextHolder.getLocalDS();
        logger.info("返回数据源对象【" + ds + "】");
        if(ds != null){
            return ds.getCustomerType();
        }
        return null;
    }
}
public enum CustomerType {
    /**
     * 主库
     */
    MASTER,
    /**
     * 从库
     */
    SLAVE;
}

AOP拦截

@Aspect
@Order(-10000)
@Component
public class AopDatasource {
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 添加业务逻辑方法切入点
     */
    @Pointcut("execution(* com.njq.nongfadai.service..*.*(..))")
    public void dynamicDS() {
    }

    /**
     * 
     * Description: 请添加方法说明: 处理事务之前动态切换数据源
     * @param point
     * @throws Throwable 参数
     */
    @Before("dynamicDS()")
    public void dynamicDSSwitchBefore(JoinPoint point) throws Throwable {
        Object target = point.getTarget();
        String method = point.getSignature().getName();
        Class<?> classz = target.getClass(); // 获取目标类
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
                .getMethod().getParameterTypes();
        
        LocalDS ds = CustomerContextHolder.getLocalDS();
        if(ds == null){
            ds = new LocalDS(CustomerType.MASTER, 0);
            logger.info("默认主数据源");
        }
        try {
            Method m = classz.getMethod(method, parameterTypes);
            if (m != null && m.isAnnotationPresent(DynDatasource.class)) {
                DynDatasource data = m.getAnnotation(DynDatasource.class);
                logger.info("用户选择数据库库类型:" + data.value());
                ds.setCustomerType(data.value());// 数据源放到当前线程中
            }
        } catch (Exception e) {
            logger.error(classz.getName() + "." + method + " 动态切换数据源异常:", e);
        } finally{
            // 设置数据源
            logger.info("设置数据源【{}】", ds);
            CustomerContextHolder.setLocalDS(ds); // 数据源放到当前线程中
        }

    }
    
    /**
     * 
     * Description: 请添加方法说明: 处理事务之后清除动态数据源
         * 不论正常执行还是异常退出都会执行 
     */
    @After("dynamicDS()")
    public void dynamicDSSwitchAfter(){
        LocalDS ds = CustomerContextHolder.getLocalDS();
        logger.info("清空当前线程数据源【{}】",ds);
        CustomerContextHolder.clearLocalDS();
    }
}

DynDatasource 注解如下

@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface DynDatasource {
    /**
     * 默认走主库
     * Description: 请添加方法说明: 
     * @return 参数
     */
    CustomerType value() default CustomerType.MASTER;

}

关于数据源的配置

<bean id="masterDataSource" name="masterDataSource"
        class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 指定连接数据库的驱动 -->
        <property name="driverClass" value="${jdbc.driverClassName}" />
        <!-- 指定连接数据库的URL -->
        <property name="jdbcUrl" value="${jdbc.url}" />
        <!-- 指定连接数据库的用户名 -->
        <property name="user" value="${jdbc.username}" />
        <!-- 指定连接数据库的密码 -->
        <property name="password" value="${jdbc.password}" />
        <!-- 指定连接池中保留的最大连接数. Default:15 -->
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
        <!-- 指定连接池中保留的最小连接数 -->
        <property name="minPoolSize" value="${jdbc.minPoolSize}" />
        <!-- 指定连接池的初始化连接数 取值应在minPoolSize 与 maxPoolSize 之间.Default:3 -->
        <property name="initialPoolSize" value="${jdbc.initialPoolSize}" />
        <!-- 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。 Default:0 -->
        <property name="maxIdleTime" value="${jdbc.maxIdleTime}" />
        <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数. Default:3 -->
        <property name="acquireIncrement" value="${jdbc.acquireIncrement}" />
        <!-- JDBC的标准,用以控制数据源内加载的PreparedStatements数量。 但由于预缓存的statements属于单个connection而不是整个连接池所以设置这个参数需要考虑到多方面的因数.如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default:0 -->
        <property name="maxStatements" value="${jdbc.maxStatements}" />
        <!-- 每60秒检查所有连接池中的空闲连接.Default:0 -->
        <property name="idleConnectionTestPeriod" value="${jdbc.idleConnectionTestPeriod}" />
    </bean>
    
    <bean id="slaveDataSource" name="slaveDataSource"
        class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!-- 指定连接数据库的驱动 -->
        <property name="driverClass" value="${jdbc.slave.driverClassName}" />
        <!-- 指定连接数据库的URL -->
        <property name="jdbcUrl" value="${jdbc.slave.url}" />
        <!-- 指定连接数据库的用户名 -->
        <property name="user" value="${jdbc.slave.username}" />
        <!-- 指定连接数据库的密码 -->
        <property name="password" value="${jdbc.slave.password}" />
        <!-- 指定连接池中保留的最大连接数. Default:15 -->
        <property name="maxPoolSize" value="${jdbc.slave.maxPoolSize}" />
        <!-- 指定连接池中保留的最小连接数 -->
        <property name="minPoolSize" value="${jdbc.slave.minPoolSize}" />
        <!-- 指定连接池的初始化连接数 取值应在minPoolSize 与 maxPoolSize 之间.Default:3 -->
        <property name="initialPoolSize" value="${jdbc.slave.initialPoolSize}" />
        <!-- 最大空闲时间,60秒内未使用则连接被丢弃。若为0则永不丢弃。 Default:0 -->
        <property name="maxIdleTime" value="${jdbc.slave.maxIdleTime}" />
        <!-- 当连接池中的连接耗尽的时候c3p0一次同时获取的连接数. Default:3 -->
        <property name="acquireIncrement" value="${jdbc.slave.acquireIncrement}" />
        <!-- JDBC的标准,用以控制数据源内加载的PreparedStatements数量。 但由于预缓存的statements属于单个connection而不是整个连接池所以设置这个参数需要考虑到多方面的因数.如果maxStatements与maxStatementsPerConnection均为0,则缓存被关闭。Default:0 -->
        <property name="maxStatements" value="${jdbc.slave.maxStatements}" />
        <!-- 每60秒检查所有连接池中的空闲连接.Default:0 -->
        <property name="idleConnectionTestPeriod" value="${jdbc.slave.idleConnectionTestPeriod}" />
    </bean>

<bean id="dataSource" class="com.yjm.datasource.CustomerRoutingDataSource">
        <property name="targetDataSources">
            <map key-type="com.yjm.datasource.CustomerType">
                <entry key="MASTER" value-ref="masterDataSource" />
                <entry key="SLAVE" value-ref="slaveDataSource" />
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="masterDataSource" />
    </bean>

Mysql层面的搭建

首先得介绍Mysql的集群

Mysql主主互备架构图如下


主主互备架构图.jpg

使用Keepalived来监控DB1和DB2的运行状态,同时维护一个VIP,此IP用来对外提供连续服务.详细的可见对Keepalived的介绍


MMM架构


MMM双Master节点应用架构.jpg

这里是使用了MMM套件来进行管理,需要5个IP地址,两个Master节点各有一个固定不变的物理IP地址,另外还有两个只读IP和一个可写IP,这三个虚拟IP(两个读IP和一个写IP)不会固定在任何一个节点上,相反,它会在两个Master节点之间来回切换,如何切换取决于可用性

在双Master节点的基础上,增加多个Slave节点,即可实现双主多从节点应用架构


MMM双主多从节点架构图.png

MMM的优势就是不仅可以监控两个Master节点的运行状态,还可以监控多个Slave节点的运行状态,整个切换过程完全不需要手工更改同步复制的配置,如果使用Keepalived的话,还需要手工写脚本监控每个节点的运行状态

Amoeba读写分离架构.png

Amoeba是一个分布式数据库的前端代理,主要是在应用层访问Mysql的时候充当SQL路由的功能,具有负载均衡,高可用,SQL过滤,读写分离等功能.

当然该架构比较复杂,后面有时间再实践一下吧

上一篇 下一篇

猜你喜欢

热点阅读