数据库读写分离方法浅析

原创作品,出自 “晓风残月xj” 博客,欢迎转载,转载时请务必注明出处(http://blog.csdn.net/xiaofengcanyuexj)。

由于各种原因,可能存在诸多不足,欢迎斧正!


       随着业务的快速发展,表不断增多,结构不断扩大,数据量也在慢慢积累,最近数据库DB压力较大,有些慢查询日志 。个人总结原因如下:表数量和结构日渐复杂,单表数量增多,联表查询等大sql ,应用层大事务等等。

1、表数量和结构日渐复杂
     随着业务的扩张,这种规模带来的问题最通用的解决方案应该是分库分表。分库有2种:水平拆分和垂直拆分。其中,垂直分库是将不同业务含义的表拆分到不同的数据库中;水平分库是将相同表中不同业务含义的数据行拆分到不同的数据库中。同理,分表也有垂直拆分和水平拆分的区别。其中,垂直分表是将表的不同字段拆分成不同的表;水平分表是将表的不同数据行拆分到不同表中,比如历史数据切分,冷热数据分离等等。

2、单表数量增多

      可以通过分表分库解决,不过相对工作量大点。简单直接粗暴的解决方案是将过期或者无效或者相对不重要的数据分离或者清理掉,可以定期同步到相对隔绝的历史表中,也可以应用层定时任务直接删除数据(物理删除和逻辑删除都可以),不过最好是定时任务或者手工同步到历史表中备份,以防以后用到。

3、联表查询等大sql 

    拆分联表查询等大sql语句,可以减少大sql造成的读写锁竞争、利用DB本身的查询缓存、方便应用层预加载设置缓存,但拆分sql分多次查询潜在的问题是占用网络资源较多,可能应用机器会占用很大内存保存临时查询结果等。当然,一般内网网速和机器内存都不会是性能瓶颈,所以可以拆分大sql是不错的选择。将直接mapper访问抽象成dao层,对service透明。

4、应用层大事务

    事务的特性是老生常谈的ACID,即原子性,一致性,隔离性,持久性。一般web框架对于事务的支持应该都是比较友好的,比如spring mvc只需要配置+注解@Transactional。事务容易加剧锁竞争,即便有些数据库,比如mysql已经采用MVCC(高版本并发控制),但是在某些关键共享变量上也会加锁,会比较影响性能。


   上面都是快速发展的数据库DB容易遇到的问题和潜在的解决方案,当然比较肤浅,考虑到接触到的系统出现慢查询时从库slave并未完全使用,更多的是容灾备份,所以假期尝试将部分读请求打到slave上,做到读写分离,master写数据+少量读请求,slave只读数据。一般数据库主从同步都不可避免存在一定的时延,考虑到有些主流程对时效性要求比较强的读请求,应该读主库,这类问题的解决方案也有很多,主要有如下几种:

1、半同步复制,即等待写请求主库同步待从库后,写请求才返回,这样读从库就能读到最新的数据,mysql。

2、强制读主库,对于特定的读请求,直接强制读主库。

3、数据库中间件,所有读写请求都走中间件,写请求到主库,记录所有路由到写库的key,在经验主从同步时间窗口内,有读请求访问中间件,就把这个key上的读请求路由到主库。

4、缓存写key法,数据库中间件方案较重,较轻的是应用层使用缓存,当写请求发生的时候同时向缓存中插入一条带有过期失效时间的记录,当读请求到达时先读缓存key,如果没有值,则直接读从库;否则强制读主库。         

。。。

     各种方法 的优缺点及详细介绍,可以参看DB主从一致性架构优化4种方法

    

     笔者实际的问题是尝试读写分离,下面记录一下读写分离方法,欢迎斧正。DB读写分离理论上方案比较多,如中间件转发、应用层分离,数据库驱动等等,各种方法的优缺点如下:

1、中间件转发

通过mysql中间件做主从集群,Mysql Proxy、Amoeba、Atlas等中间件貌似都能符合需求。

优点:对应用透明

缺点:需要代理,增加网络等性能开销

2、应用层分离

应用层路由数据源实现读写分离,通过AOP或者注解来动态选择数据源

优点:无需中间件,策略可选,可用来负载均衡

缺点:耦合度高

3、数据库驱动

Replication Driver或者使用Replication协议头,Replication Driver根据connection的readonly属性路由数据源,数据库驱动

优点:对应用透明,无需中间件

缺点:需要DB支持Replication协议


Java web可以分为两个层次:JDBC层的封装,ORM框架层的实现。

  结合实际情况较轻的方案是应用层分离,比如配置不同mapper访问主从数据源、AOP切片路由读写请求等。

1、不同mapper访问主从数据源一般来说,对于不同的数据源使用不同包的mapper访问,就可以读写分离,但是几乎相同代码要拷贝多份

2、AOP切片路由读写请求利用反射,动态决定运行时的数据源,实现读写分离

   编写动态数据源路由 DynamicDataSource类继承AbstractRoutingDataSource,并实现determineCurrentLookupKey()方法,determineCurrentLookupKey()是spring数据库连接池在建立连接时都要调用的。然后通过在mapper或者dao层切片,感觉最好在dao层切片,所有事务都定义在dao层;这样对于事务的处理也比较方便,因为要保持相同事务内的不同连接到同一个数据源(当然是master了),可以单独对事务注解@transactional进行切片。

import com.google.common.collect.ImmutableMap;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.sql.DataSource;

 /**
 * 实现描述:切换动态数据源
 *
 * @author jin.xu
 * @version v1.0.0
 * @see
 * @since 16-10-6 下午3:40
 */
@Component
public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DAAL_HOLDER = new ThreadLocal<String>();
    @Resource
    private DataSource masterDataSource;
    @Resource
    private DataSource slaveDataSource;

    @Override
    public void afterPropertiesSet() {
        setTargetDataSources(ImmutableMap.<Object, Object> of("master", masterDataSource, "slave", slaveDataSource));
        setDefaultTargetDataSource(spiderDataSource);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DAAL_HOLDER.get();
    }

    public static void switchToMaster() {
        DAAL_HOLDER.set("master");
    }

    public static void switchToSlave() {
        DAAL_HOLDER.set("slave");
    }

    public static void reset() {
        DAAL_HOLDER.remove();
    }

}


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 实现描述:DB读写分离切片
 *
 * @author jin.xu
 * @version v1.0.0
 * @see
 * @since 16-10-6 下午4:53
 */
@Aspect
@Component
public class DBAspect {

    /**
     * 内部api监控
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("execution (* com.csdn.jinxu.dal.dao..*.*(..))")
    public Object dbLog(ProceedingJoinPoint joinPoint) throws Throwable {
        Long startTime = 0l;
        Long endTime = 0l;
        Object result = null;
        String method =null;
        try {
            startTime = System.currentTimeMillis();
            method = joinPoint.getSignature().getName();
            if(isSwitchToSlave(method)){
                DynamicDataSource.switchToSlave();
            }else{
                DynamicDataSource.switchToMaster();
            }
            result = joinPoint.proceed();
            endTime=System.currentTimeMillis();
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                String request = Arrays.toString(joinPoint.getArgs());
                String response =result.toString();
            } catch (Exception e) {
            }
            DynamicDataSource.reset();
        }
        return result;
    }

    /**
    *扩展配置粗略
    */
    private boolean isSwitchToSlave(String method){
        boolean isBFlag=false;
        if(null!=method&&(method.startsWith("find")
                ||method.startsWith("count")
                ||method.startsWith("get"))){
            isBFlag=true;
        }
        return isBFlag;
    }

}



    路漫漫其修远兮,很多时候感觉想法比较幼稚。首先东西比较简单,其次工作也比较忙,还好周末可以抽时间处理这个。由于相关知识积累有限,欢迎大家提意见斧正,在此表示感谢!后续版本会持续更新…





  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值