借助ThreadLocal实现数据库读写分离,诡异的问题

一、背景说明

        准备将项目中读取数据库操作切换到从数据库上,调研方案后最终决定借助ThreadLocal实现,在测试过程中遇到间歇性异常情况,经排查是由于相同的insert、update语句会间歇性的命中到从库数据库,最终导致异常。

二、读写分离配置

       原理 : 在spring-jdbc.jar包中提供了可动态切换数据源的实现类(AbstractRoutingDataSource),首先在项目中配置双数据库数据源(主数据源、从数据源)。使用AOP对dao层方法进行切割,根据方法名称判断,当方法名称为只读方法时将ThreadLocal中存储的数据源标签设置为read数据源( 从数据源名称)。重写AbstractRoutingDataSource中的determineCurrentLookupKey钩子方法,读取ThreadLocal中存储的数据源标签名称返回,最终实现数据库的读写分离。

       ThreadLocal工具类代码如下所示

public class DbTagUtil {

    private static ThreadLocal<String> dbTagLocal = new ThreadLocal<String>();

    public static final String READ = "read";

    public static final String WRITE = "write";

    public static String getDbTag() {
        String db = dbTagLocal.get();
        if (db == null) {
            /**默认写库**/
            db = WRITE;
        }
        return db;
    }
    /**
     * 赋值
     * @param tag
     */
    public static void setTag(String tag) {
        dbTagLocal.set(tag);
    }
    /**
     * 清空
     */
    public static void clearTag() {
        dbTagLocal.remove();
    }
}

借助Spring的AbstractRoutingDataSource 实现动态数据源切换,配置如下所示:

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DataSourceProxy extends AbstractRoutingDataSource {
    /**模版模式中 钩子实现**/
    @Override
    protected Object determineCurrentLookupKey() {
        return DbTagUtil.getDbTag();
    }
}

Dao层方法切面实现方式

import org.springframework.aop.MethodBeforeAdvice;
public class DbAopCutting implements MethodBeforeAdvice {
    private List<String> methodKeys;
    @Override
    public void before(Method method, Object[] args, Object target)
            throws Throwable {
         
        String methodName = method.getName().toLowerCase();
        if (null != methodKeys ) {
            for (String key : methodKeys) {
                if (methodName.startsWith(key.toLowerCase())) {
                    DbTagUtil.setTag(DbTagUtil.READ);
                    return;
                }
            }
        }
        DbTagUtil.setTag(DbTagUtil.WRITE);
    }
    public void setMethodKeys(List<String> methodKeys) {
        this.methodKeys = methodKeys;
    }
}

mybatis 配置文件,配置方式如下所示:

<bean id="writeDataSource" class="org.apache.commons.dbcp.BasicDataSource" abstract="true" destroy-method="close" lazy-init="true">
	......
</bean>
<bean id="readDataSource" class="org.apache.commons.dbcp.BasicDataSource" abstract="true" destroy-method="close" lazy-init="true">
	......
</bean>
<bean id="dataSource" class="com.timer.bin.DataSourceProxy">
	<property name="targetDataSources">
		<util:map key-type="java.lang.String">
			<entry key="read" value-ref="readDataSource" />
			<entry key="write" value-ref="writeDataSource"/>
		</util:map>
	</property>
	<property name="defaultTargetDataSource" ref="writeDataSource" />
</bean>

<bean id="dbAopCutting" class="com.timer.bin.DbAopCutting">
	<property name="methodKeys">
		<list>
			<value>select</value>
			<value>query</value>
		</list>
	</property>
</bean>
<aop:aspectj-autoproxy/>
<aop:config>
	<aop:pointcut expression="execution(* com.timer.bin.dao..*.*(..))" id="dbCutPoint"/>
	<aop:advisor advice-ref="dbAopCutting" pointcut-ref="dbCutPoint"/>
</aop:config>

借助以上配置实现读写分离

三、问题说明

错误信息如下所示:

Caused by: java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement

四、问题排查

借助输出调试日志方式,在相应的determineCurrentLookupKey和DbAopCutting.before方法中添加调试日志,最终发现出现异常时,在ThreadLocal获取到的数据源标签为read从数据源,并且获取数据源日志先于DbAopCutting.before方法中日志输出,结合其他输出日志最终排查出,出现异常的方法由于在Service层方法上加了事务。

事务在开启时就会获取dataSource,而DbAopCutting切面还未执行,所以此时在ThreadLocal中获取到的数据源标签为上一次SQL执行时存储的数据源标签,具体是主或从不定,最终导致程序出现间歇性异常问题。

五、修改方案

对DbAopCutting切面进行修改,增加afterReturning 增强,在方法正常执行完成后,立即将ThreadLocal中存储的数据源标签清除,保证ThreadLocal用完即删。

import org.springframework.aop.MethodBeforeAdvice;
public class DbAopCutting implements MethodBeforeAdvice {
    private List<String> methodKeys;
	......
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        DbTagUtil.clearTag();
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值