Mysql读写分离实践

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

首先是代码层面的
由于简单,使用了注解的方式来做读写分离
后面会讲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过滤,读写分离等功能.

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

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

推荐阅读更多精彩内容

  • 西江月•别春 柳岸烟波袅袅,翠山云影茫茫。往来飞燕戏池塘,风剪涟漪三两。 又是瘦春飞絮,奈何愁镜描妆。落花几度太匆...
    精品女装店阅读 429评论 3 8
  • 时间如高铁,岁月似动车。不知不觉2017这趟单程列车即将到达终点站。回首过去这一年所经历的旅途,才发觉一路来尽顾着...
    人杰地灵1阅读 128评论 0 0