实现读写分离(springboot+mybatis)(二)

在(一)中直接写了代码, 能力有限 , 只能对(一)中的代码和遇到的坑做一些简单的阐释

1. 之所以没有使用aop是因为读写分离不是在一开始就在在我们的规划里面 , 方法名没有按照一定的规则写,所以不好拦截,最后选择了mybatis的插件拦截. 关于插件拦截的学习可以参考以下博客 : 

https://www.jianshu.com/p/14bf6a4ca7ef

2. 最开始的配置中并没有使用到自定义DataSourceTransactionManager, 然后发现 , 不带有事务的增删改查 , 完全按照读走从库 , 增删改走主库的形式进行, 没有问题. 但是一旦涉及到事务 ,对于主从的选择就会出现问题 , 按照之前的设计, 带有事务的情况应该完全走主库才对. 可是实际情况遇到事务完全随机的来.

进行调试发现, 一个请求中 , 在调用事务方法之前 , 进行过查询(走过从库的), 在事务开启之后依然走的是从库; 而在事务开启之前没有调用查询(走从库的)使用的是默认的数据源(主库)

进一步查明原因:

 在走过mybatis的插件之后, 会调用DataSourceUtils.class的getConnection方法, 部分代码如下 ,主要功能是获取链接的时候 , 根据上线文中存储的数据源的key(在插件拦截中已经选好了应该使用的数据源的key放在上线文中), 拿到相应的数据源; 而当事务存在并且事务中已经存在ConnectionHolder的情况下(开启事务的时候也会走一遍getConnection, 这时候会拿到上下文中已经存在的key去获取数据源, 并存在ConnectionHolder中), 就不会重新进行这些操作, 直接使用ConnectionHolder中的链接. 所以就算插件拦截中重新设置了上下文中数据源的key, 也没有通过key去重新获取数据源这一步了. 所以事务的时候, 设置数据源就失效了. 

org.springframework.jdbc.datasource.DataSourceUtils
    
public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException {
        try {
            return doGetConnection(dataSource);
        } catch (SQLException var2) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection", var2);
        } catch (IllegalStateException var3) {
            throw new CannotGetJdbcConnectionException("Failed to obtain JDBC Connection: " + var3.getMessage());
        }
    }

    public static Connection doGetConnection(DataSource dataSource) throws SQLException {
        Assert.notNull(dataSource, "No DataSource specified");
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(dataSource);
        //在没有事务的情况下,conHolder是null,会进行fetchConnection方法,这个方法也贴在了下面,会调用dataSource的getConnection方法.- 通过上下文中的key拿到对应的数据源.
        //在有事务的情况下,如果之前存在数据源(conHolder不是null)不会重新动态获取数据源了
        if (conHolder == null || !conHolder.hasConnection() && !conHolder.isSynchronizedWithTransaction()) {
            logger.debug("Fetching JDBC Connection from DataSource");
            Connection con = fetchConnection(dataSource);
            if (TransactionSynchronizationManager.isSynchronizationActive()) {
                logger.debug("Registering transaction synchronization for JDBC Connection");
                ConnectionHolder holderToUse = conHolder;
                if (conHolder == null) {
                    holderToUse = new ConnectionHolder(con);
                } else {
                    conHolder.setConnection(con);
                }

                holderToUse.requested();
                TransactionSynchronizationManager.registerSynchronization(new DataSourceUtils.ConnectionSynchronization(holderToUse, dataSource));
                holderToUse.setSynchronizedWithTransaction(true);
                if (holderToUse != conHolder) {
                    TransactionSynchronizationManager.bindResource(dataSource, holderToUse);
                }
            }

            return con;
        } else {
            conHolder.requested();
            if (!conHolder.hasConnection()) {
                logger.debug("Fetching resumed JDBC Connection from DataSource");
                conHolder.setConnection(fetchConnection(dataSource));
            }

            return conHolder.getConnection();
        }
    }

    private static Connection fetchConnection(DataSource dataSource) throws SQLException {
//这里的dataSource就是我们继承的AbstractRoutingDataSource类,并且重写了determineCurrentLookupKey方法的DataSourceSelector类
        Connection con = dataSource.getConnection();
        if (con == null) {
            throw new IllegalStateException("DataSource returned null from getConnection(): " + dataSource);
        } else {
            return con;
        }
    }
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

public Connection getConnection() throws SQLException {
        return this.determineTargetDataSource().getConnection();
    }


protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //这里调用的是我们重写的方法,从上下文中获取key值
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource 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 + "]");
        } else {
            return dataSource;
        }
    }

@Nullable
protected abstract Object determineCurrentLookupKey();

所以在开启事务的时候, 解决问题的关键点, 在于开启事务的时候, 为上下文中的数据源key值重新赋值. 我们在(一)中的步骤5就是在处理这种情况


package com.hlz.dao.config;
 
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
 
import javax.sql.DataSource;
 
public class HlzDataSourceTransactionManager extends DataSourceTransactionManager {
 
    public HlzDataSourceTransactionManager(DataSource dataSource) {
        super(dataSource);
    }
 
    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        DynamicDataSourceHolder.setDataSourceType(DynamicDataSourceHolder.DB_MASTER);
        super.doBegin(transaction, definition);
    }

}

3. 我们做的主库和从库, 使用的方式是: 主从复制binlog复制的方式, 像我们做的这种读写分离, 还是会有一些小小的问题, 设想一个场景, [在没有事务的情况下, 先插入, 再查询], 插入时候会走主库, 而查询走的是从库. 从插入到查询程序走的时间很短, 快到从库还没来得及从主库复制数据, 所以在写这些场景的时候要格外注意, 加上事务, 或者用其他的方式实现默认走主库. 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值