多数据源动态切换

多数据源跳库组件及分析

  1. 连接池介绍
  2. 多数据源使用
  3. 多数据源应用场景
  4. 多数据源配置
    1. spring + druid 多数据源配置
    2. springboot + druid 多数据源配置
  5. 多数据源跳库组件化
  6. 多数据源问题及解决办法

连接池介绍

首先说下连接池:(应该都不陌生)
数据源即DataSource, 在java中有很多对DataSource的实现, 如C3P0, Druid. 从连接池中获取某个数据库的连接, 减少创建和关闭连接带来的性能消耗。 同时基本上所有的连接池实现都有对于连接池的一些配置如 用户名密码,url, maxWait,maxActive 等等。
一个数据源管理一组相同属性的数据库的连接
由于数据源的配置在网上都有详细的说明,在这里就不重复了, 根据自己业务场景使用合适的参数。

多数据源的应用场景

在业务中往往采用1个关系型数据库如mysql.
到目前为止, 遇到过下面几种多数据源的应用场景。

  1. 分库分表
    当业务变得越来越复杂, 数据变得越来月庞大, 即使简单的sql查询性能也不见有多好, 这时候我们可以采用分库分表的方式。 当然切分的方式有很多,比如按照业务垂直切分, 在这里不做讨论。
  1. 读写分离
    对数据库的更新以及删除等操作和查询操作是在不同数据库进行的。

在一个数据库进行操作, 对于开发是最理想的, 拆分数据库在一定程度会增大模块的复杂性,根据自己的需求, 恰当使用多数据源。 当然最理想的情况,对于开发来讲, 即是多数据源的切换是透明的。

多数据源使用

环境 mybatis, spring

多数据源即在一个应用中配置多个不同的连接池。
对于数据源的使用是相对容易的。
由于某些架构或业务中, 有时候是要进行多个数据库的操作,这就涉及到多个数据源的创建以及数据源的切换

在这里我先讲对于多数据源的创建, 也就是数据源是在项目启动时读取xml的配置创建数据源, 后面我会介绍如何动态创建数据源。

配置

多个 druid 数据源配置


<bean id="druiddataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
    ...
    这里是数据源的配置 如 user_name
</bean>

针对多数据源, 要创建多个数据源, 可以通过parent来配置,
<bean parent="druiddataSource" id="xxx">
    <!-- 数据库基本信息配置 -->
    <property name="url" value="${jdbc.url}" /> 
</bean>
<bean parent="druiddataSource" id="xxx2">
    <!-- 数据库基本信息配置 -->
    <property name="url" value="${jdbc.url2}" /> 
</bean>
<bean parent="druiddataSource" id="xxx3">
    <!-- 数据库基本信息配置 -->
    <property name="url" value="${jdbc.url3}" /> 
</bean>
... 其他数据源

以上就是多数据源的配置, 以上的配置仅是url配置不同, 其他配置相同, 如果其他连接池配置不同, 设置相应的property即可

在jdbc编程中我们知道, 获取到对应的连接池, 我们就可以调用dataSource.getConnection() 方法获取连接, 但是对于任何与数据库打交道, 我们并不需要关心连接的获取与关闭,以及对于连接的其他细节, 这时候诸如Mybatis, hibernate, jdbcTemplate 的框架就出现了, 这些框架屏蔽了底层的细节。 我们就能很容易使用controller-service-dao 3层模型来进行开发, 下面就介绍一下 使用Mybatis来动态切换数据源。

  1. mybatis 配置 + 切换配置
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />  <!--注意这里的dataSource-->
        <!--其他mybatis配置 如mybatisLocation, 这里就不再提了-->
</bean>
<!--这里的dataSource 不能直接是druid这类的dataSource, 而是需要一个能够动态获取的dataSource, 在后面我会进行源码分析。-->

<bean class="com.xxx.datasource.DynamicDataSource" id="dataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <entry value-ref="xxx1" key="value1"/> <!--这里的xxx1就是之前配置的数据源, 要使用几个数据源就配置几个-->
                            ...
        </map>
    </property>
</bean>

那么再看一下com.xxx.datasource.DynamicDataSource这个是怎么样的呢


public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 获取线程下当前的数据源
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DBContextHolder.getDBType();
    }
}
//在这里说明 返回的Object 是跟之前设置map里的key 进行匹配, 这样才能通过key来取出,
//在切换的时候只用设置DBContextHolder.setDBType(“要的”)就可以了 ,DBContextHolder 是一个本地线程。
// 其实该DynamicDataSource也是java.sql.DataSource。 只不过它将多个数据源通过key-value的形式放在绑定。
key就是我们切换数据源的依据。

以上是spring xml+ druid的配置, 再来看看springboo的配置

druid + springboot 配置

使用springboot, 也就不用xml, 如何配置数据源呢? 如果在了解了上面的原理后,并且有一定的spring 基础, 那么配置动态数据源将变得轻松。

@Configuration
public class DevDruidConfiguration implements ApplicationContextAware{

    // 默认数据源
    @ConfigurationProperties("spring.datasource.druid.key1")
    @Bean
    public DataSource key1(){
        return DruidDataSourceBuilder.create().build();
    }
    
    @Primary
    @ConfigurationProperties("spring.datasource.druid.key2")
    @Bean
    public DataSource key2(){
        return DruidDataSourceBuilder.create().build();
    }
    
        public ApplicationContext getContext() {
        return context;
    }
    
    // dynamic DataSource bean 配置
    @Bean
    public DynamicDataSource dynamicDataSource() {
    DynamicDataSource dataSource = new DynamicDataSource();
        ApplicationContext c = getContext();
        Map<Object,Object> target = new HashMap<>();
        target.put("key1"               ,                 "key1"                          );
        target.put("key2"               ,                 "key2"                          );
        dataSource.setTargetDataSources(target);
        dataSource.setDataSourceLookup(new BeanFactoryDataSourceLookup(c.getAutowireCapableBeanFactory()));
        dataSource.setDefaultTargetDataSource("hpdes");
         return dataSource;
    }
    
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

}


mybatis 配置
@Configuration
public class MybatisConfiguration extends MybatisAutoConfiguration {

//    public SqlSessionFactory sqlSessionFactory

    public MybatisConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        super(properties, interceptorsProvider, resourceLoader, databaseIdProvider, configurationCustomizersProvider);
    }


    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource")DataSource dynamicDataSource) throws Exception {
        return super.sqlSessionFactory(dynamicDataSource);
    }
    

}




springbooot application.yml 配置
spring:
  datasource:
    druid:
      key1:
        url: jdbc:mysql://@mysqlUrl@:3306/db1?characterEncoding=utf-8
        name: key1
      key2:
        url: jdbc:mysql://@mysqlUrl@:3306/db2?characterEncoding=utf-8
        name: key2
      initial-size: 1  # 初始化
      min-idle: 1      #
      ...
      


动态数据源切换源码分析

说了这么多, 不从源码级别说,估计很多人都是蒙圈的, 但是在此之前我先概括一下, 既然是动态切换, 你首先要设置一个标识符说明要哪个数据源, 那么再获取的时候, 通过这个标识符去找对应的数据源, 而最容易找的集合就是通过Map进行存储 key是标志符号, value是DataSource. 下面就进行源码分析了

  • Mybatis 管理数据源
  public void setDataSource(DataSource dataSource) {
    if (dataSource instanceof TransactionAwareDataSourceProxy) {
      // If we got a TransactionAwareDataSourceProxy, we need to perform
      // transactions for its underlying target DataSource, else data
      // access code won't see properly exposed transactions (i.e.
      // transactions for the target DataSource).
      this.dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
    } else {
      this.dataSource = dataSource;
    }
  }
  这里的DataSource 就是DynamicDataSource。
  • 获取相应的数据源: 从事务中获取连接

先大致说一下mybatis的调用顺序
SqlSession -> Executor -> Handler

在Executor 中调用getConnection()

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }
  // transaction.getConnection()  如果是SpringMannagedTransaction, 则会从DataSourceUtils中获取
    private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = isConnectionTransactional(this.connection, this.dataSource);

    if (logger.isDebugEnabled()) {
      logger.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }
  
  // 在DataSourceUtils.getConnection(this.dataSource) 会先从本地线程中根据数据源获取连接, 见后面多数据源源码分析
  若本地线程中没有, 则会调用dataSource.getConnection(); 此时这里是DynamicDataSource, 其getConnection()方法如下
    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }
    // 关键来了, 在determineTargetDataSource中正是调用determineCurrentLookupKey来获取数据源的key
    
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey(); // 当前要切换的数据源的key
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

对于数据源是如何切换的, 我们通过分析已经很清楚, 要注意, key的获取是根据我们的实现获取到的, 最简单的方法就是本地线程, 但是在异步或多线程调用可能就会出现问题。

以上我们创建的数据源是在项目启动时就会创建好, 并且如果再添加新的数据源, 必须修改配置, 并且重新部署应用, 这对分库的应用来说是个大麻烦。

动态切换多数据源组件化

前面我们知道切换数据源之前总是要调用DataSource.setDB("....") 来进行切换, 并且如果该调用只是某个业务逻辑的一部分, 在切换数据源后的调用还需要在切换回来, 并且还要考虑调用发生异常的情况, 为此增大了开发的易错性。 那么有什么好的方法能够避免这样的切换吗?

我初步的设计了以下的组件,不需要考虑切换的步骤, 只需要关心当前我需要切换到什么数据库和执行逻辑:
代码实现如下:

public class DataSourceService {
    /**
     * 跳库执行
     * @param source  跳到某个租户下
     * @param exec  执行的逻辑
     */
    public static <E> void skip(String source, Execution<E> exec) throws Throwable {
        DBContextHolder.setDBType(source, true);
        try{
            exec.execute();
        }finally {
            DBContextHolder.recover();
        }
    }

    /**
     * 跳库执行返回结果
     */
    public static<E> E skipCall(String source, Execution<E> exec) throws Throwable {
        DBContextHolder.setDBType(source, true);
        try {
            E result  = exec.execute();
            return result;
        }finally {
            DBContextHolder.recover();
        }
    }
}


public class DBContextHolder {
    
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    public static final ThreadLocal<Boolean> isPropagation = new ThreadLocal<Boolean>(){
        @Override
        protected Boolean initialValue() {
            return false;
        }
    };
    public static final ThreadLocal<SourceInfo> preSourceHolder = new ThreadLocal<>();
    /**
     * 跳库不传播, 在传播后调用该接口无效
     * @param dbType
     */
    public static void setDBType(String dbType) {
        if (isPropagation.get()!=Boolean.TRUE) {
            updateSourceInfo();
            contextHolder.set(dbType);
        }
    }

    /**
     * 跳库, 接下来都为切换成该库
     * @param dbType
     * @param isProp
     */
    public static void setDBType(String dbType, boolean isProp) {
        updateSourceInfo();
        contextHolder.set(dbType);
        isPropagation.set(isProp);
    }

    public static String getDBType() {
        return contextHolder.get();
    }

    public static void clearDBType() {
        contextHolder.remove();
        isPropagation.remove();
        updateSourceInfo();
    }
    public static void recover(){
        if (preSourceHolder.get()!=null){
            contextHolder.set(preSourceHolder.get().contextHolder);
            isPropagation.set(preSourceHolder.get().isPropagation);
            preSourceHolder.remove();
        }
    }
    public static boolean isPropagation(){
        return isPropagation.get();
    }

    private static void updateSourceInfo(){
        SourceInfo sourceInfo = new SourceInfo();
        sourceInfo.contextHolder = contextHolder.get();
        sourceInfo.isPropagation = isPropagation.get();
        preSourceHolder.set(sourceInfo);
    }
    public static String getCurrentDB(){
        return DBContextHolder.getDBType();
    }
}


调用示例
Entity obj = DataSourceService.skipCall("sourceKey", new Execution<Entity>() {
                    @Override
                    public Entity execute() throws Throwable {
                        return serviceImpl.doSql("....."); // 业务逻辑执行
                    }
                });
如果使用Java8, 使用lamdba 表达式
Entity obj = DataSourceService.skipCall("sourceKey", ()->{
            return serviceImpl.doSql("....."); // 业务逻辑执行
        });

如何使用多数据源(多情况分析)

controller 拦截跳库

往往(大部分情况下)页面的一次操作都在一个数据库中,所以使用拦截器进行跳库是不错的选择, 如果本次操作涉及多个数据库, 可以使用上述组件进行数据源切换。

拦截器大致如下

public class DataSourceIntercetor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        HttpServletRequest request = httpServletRequest;
        String tenant = getDataSouceKeyFromRequest(request);
        return  DBContextHolder.setDB(tenant);
        // 上述getDataSouceKeyFromRequest 根据自己的需求实现,如果信息保存在token中, 则解析token取出, 如果存在cookie中(并不是很安全),
        则查询cookie, 
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { 
        DBContextHolder.clearDBType();// 这里最好清空一下本地线程
    }
}


多数据源问题考虑

在解决跳库这些问题, 对于跳库, 有些问题是我们需要注意的

  1. 在多数据源的情况下, 事务如何保证?
    分布式事务解决方式
    用于对事务性要求不高,允许数据“最终一致”

首先来看一下如果用原来的事务处理会出现什么情况?

在这里主要为==spring transaction== 和 ==mybatis== 一起使用的情况。

假如有以下2个service方法

service1.doMethod1();    // 加入向数据库1插入一条记录
service2.doMethod2();   // 向数据库2插入一条记录
在另一个service3 调用了service1.method1() 和service2.method2()。

大致代码如下:
method3(){
    service1.doMethod1();   
    service2.doMethod2();
}

场景1: 不采用事务切面

在调用method3前开启事务

假如method1调用失败, 由于service1是对A数据库的操作, 开启事务,则service1事务回滚, 没有什么问题。
但是method1调用成功, service1 事务提交, 然而service2调用失败,service2事务回滚, 然而service1无法事务回滚, 导致不一致性。

场景2: 无法切换数据源

即对method3采用@Transactional 注解或进行 tx:advice 切面配置
在调用method3 之前开启事务, 由于事务已经开启, 导致doMethod1和doMethod2的连接不再重新获取而是从事务中的连接池获取
使得2个方法的执行的连接都是在事务开始时获取的数据源连接。导致切换数据源失败

原因: 源码分析

我们在平时通常使用spring 的 TransactionManager, 通过aop 和 tx:advice 来进行事务管理, 很方便。
首先来看一下spring aop 事务切面核心代码:


final PlatformTransactionManager tm = determineTransactionManager(txAttr);   //txAttr是PROPAGATION_REQUIRED 等事务隔离性的实体封装
final String joinpointIdentification = methodIdentification(method, targetClass); // 便于日志记录的识别号

if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
    // Standard transaction demarcation with getTransaction and commit/rollback calls.
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
    Object retVal = null;
    try {
        // This is an around advice: Invoke the next interceptor in the chain.
        // This will normally result in a target object being invoked.
        retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
        // target invocation exception
        completeTransactionAfterThrowing(txInfo, ex); // 会回滚事务
        throw ex;
    }
    finally {
        cleanupTransactionInfo(txInfo);   // 清空事务相关的状态
    }
    commitTransactionAfterReturning(txInfo); // 提交事务
    return retVal;
}

## 在createTransactionIfNecessary中会进行事务的开始。
下面进行核心代码的
public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable {
    
    public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
        Object transaction = doGetTransaction(); //从resources 中获取事务
        boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
    DefaultTransactionStatus status = newTransactionStatus(
                    definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
    doBegin(transaction, definition);  // 开始事务
    prepareSynchronization(status, definition);
    return status;

    }

}

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, InitializingBean {
    if (txObject.getConnectionHolder() == null ||
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
        Connection newCon = this.dataSource.getConnection(); // 从数据源中获取连接,动态数据源会根据此时的contextHolder 来获取到某个连接
        txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
    }

    txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
    con = txObject.getConnectionHolder().getConnection();

    Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
    txObject.setPreviousIsolationLevel(previousIsolationLevel);
    
    if (txObject.isNewConnectionHolder()) {
        TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
        //这一步很关键 也就是说连接池和连接会以map的形式绑定到本地线程中, //将来所有的连接都是通过连接池对应的连接,所以这里动态数据源与此时的一个连接绑定了, 
        //在本次事务内都是对该连接进行操作
    }
}

从中看出, spring在开启事务时, 数据源(DynamicDatasource)会和当前的连接绑定, 导致以后的连接不是通过动态数据源获取,而是通过key找value, 而value永远是之前的数据源。

如果在多数据源上加上了事务, 以上方法通过DynamicDataSource似乎就不可行了。

事务思路

调用service1 开启事务, 但是方法1调用成功不能提交事务,
调用service2 开启事务, 但是方法2调用成功不能提交事务
service3 调用service1和service2 都成功, 同时提交service1和service2。

如何结局

其实多个数据源已经不能用数据库的事务来解决了, 我们其实可以把多数据源理解成分布式的架构, 多余分布式事务控制有很多解决办法, 有心的可以去网上查询相应的办法, 今天就先讲到这里。


参考:
架构师一席谈(一) 为什么要在服务层设计读写分离【转】

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

推荐阅读更多精彩内容

  • 背景 随着业务的发展,数据库压力的增大,如何分割数据库的读写压力是我们需要考虑的问题,而能够动态的切换数据源就是我...
    周艺伟阅读 2,300评论 1 7
  • 背景 最近做一项目,公司数据库用的主从结构,以前做项目都只是用的单数据库,网上扒了两天,终于搞定自认为最优雅的方式...
    小李子Levy阅读 2,893评论 1 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 经年的沧桑,总是在通往青芝山那曲径通幽的石台阶,儿时是那样的淳朴无虞。 ...
    三哥_eb29阅读 267评论 1 1
  • 有人说 金晶,你笑的好像傻子 忽然发现 当我还能笑的这么疯狂,开放 那是一件多么美好的事情 当我还能去做内心感动的...
    金晶花阅读 215评论 0 0