mysql读写分离查询从库数据不正确_数据库读写分离的坑,你已经在坑了

本文介绍了在Springboot+Mybatis+HikariCP实现MySQL读写分离时遇到的问题,即查询从库数据不正确。问题源于@Transactional注解在查询方法上导致数据源提前确定,而非按预期的读写分离规则。解决方案包括调整AOP切点表达式以确保事务管理和读写分离策略的一致性。
摘要由CSDN通过智能技术生成

数据库读写分离

本篇文章是在 数据源读写分离 基础上发现的一个小问题。

目录

背景

知识储备

问题现象

解决方案

问题总结

背景

Springboot集成mybatis使用HikariCP连接MySQL,进行读写分离。

知识储备

在上篇文章中,我们实现了数据源的读写分离,也实现了我们想要的效果. 但是存在一个小小的瑕疵。接下来,我需要介绍一下稍微深入的知识。

在我们配置动态数据源的时候,继承了一个类 AbstractRoutingDataSource,我们先看一下这个类有什么作用。

package com.fxb.doraemon.human;

import org.springframework.beans.factory.InitializingBean;

import org.springframework.jdbc.datasource.AbstractDataSource;

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

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

import org.springframework.lang.Nullable;

import org.springframework.util.Assert;

import javax.sql.DataSource;

import java.sql.Connection;

import java.sql.SQLException;

import java.util.HashMap;

import java.util.Map;

/**

* DataSource的实现:根据一个指定的key,调用各种的目标的数据源。 之后,通常根据一些线程的事务上下文来指定数据源。

*/

public abstract class AbstractRoutingDataSourceTest extends AbstractDataSource implements InitializingBean{

/**

* 目标数据源

*/

@Nullable

private Map targetDataSources;

/**

* 默认的数据源

*/

@Nullable

private Object defaultTargetDataSource;

/**

* ??

*/

private boolean lenientFallback = true;

/**

* 数据源查找??

*/

private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

@Nullable

private Map resolvedDataSources;

/**

* 被处理之后的数据源??

*/

@Nullable

private DataSource resolvedDefaultDataSource;

/**

* 根据指定查找的key获取目标数据源的映射,

* 映射的值可以是一个DataSource的实例,也可以是String。如果是String的话,会通过setDataSourceLookup方法进行解析。

* 指定的key可以是任意类型,这个类仅仅实现了通用的查找过程,具体的key标识将由resolveSpecifiedLookupKey(Object)方法和

* determineCurrentLookupKey()进行解析。

* 也就是说:

* #1.如果我们自己定义了Map中的value是字符串的话,就需要重写setDataSourceLookup这个方法进行返回正确的数据源。

* #2.如果我们自己定义了Map中的key的话,我们就需要重写resolveSpecifiedLookupKey方法和determineCurrentLookupKey()。

*/

public void setTargetDataSources(Map targetDataSources){

this.targetDataSources = targetDataSources;

}

/**

* 指定默认的数据源(如果存在的话)

* 如果我们自己定义了Map中的value是字符串的话,就需要重写setDataSourceLookup这个方法进行返回正确的数据源。

* 如果根据指定的key在targetDataSources中找不到Datasource的时候,就用使用这个默认的数据源。

*/

public void setDefaultTargetDataSource(Object defaultTargetDataSource){

this.defaultTargetDataSource = defaultTargetDataSource;

}

/**

* 如果找不到指定的Datsource的时候,可以通知指定lenientFallback来确定是否使用默认数据源

* true: 找不到就会使用默认数据源

* false: 仅在key为null的时候进行回退。即当key为null的时候才使用默认的数据源,否则就会抛出IllegalStateException异常

*/

public void setLenientFallback(boolean lenientFallback){

this.lenientFallback = lenientFallback;

}

/**

* 解析setTargetDataSource中数据源名称是dataSource的情况,默认值是JndiDataSourceLookup。

*/

public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup){

this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());

}

@Override

public void afterPropertiesSet(){

if (this.targetDataSources == null) {

throw new IllegalArgumentException("Property 'targetDataSources' is required");

}

this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());

this.targetDataSources.forEach((key, value) -> {

Object lookupKey = resolveSpecifiedLookupKey(key);

DataSource dataSource = resolveSpecifiedDataSource(value);

this.resolvedDataSources.put(lookupKey, dataSource);

});

if (this.defaultTargetDataSource != null) {

this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);

}

}

/**

* 解析指定的key,对应着setTargetDataSources#2的情况。

* 默认是直接返回

*/

protected Object resolveSpecifiedLookupKey(Object lookupKey){

return lookupKey;

}

/**

* 将指定的数据源对象解析为DataSource实例

* 默认是通过:setDataSourceLookup进行解析。

* 如果是字符串的话,可以用通过setDataSourceLookup设置自定义的dataSourceLookup。

*/

protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException{

if (dataSource instanceof DataSource) {

return (DataSource) dataSource;

} else if (dataSource instanceof String) {

return this.dataSourceLookup.getDataSource((String) dataSource);

} else {

throw new IllegalArgumentException(

"Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);

}

}

@Override

public Connection getConnection() throws SQLException{

return determineTargetDataSource().getConnection();

}

@Override

public Connection getConnection(String username, String password) throws SQLException{

return determineTargetDataSource().getConnection(username, password);

}

@Override

@SuppressWarnings("unchecked")

public T unwrap(Class iface) throws SQLException{

if (iface.isInstance(this)) {

return (T) this;

}

return determineTargetDataSource().unwrap(iface);

}

@Override

public boolean isWrapperFor(Class> iface) throws SQLException{

return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));

}

/**

* 检索当前目标数据源。

* 调用determineCurrentLookupKey获取key,在targetDataSources中进行查找,是否要会回退,使用默认数据源。

* 如果找不到数据源就抛出IllegalStateException异常。

*/

protected DataSource determineTargetDataSource(){

Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");

Object lookupKey = determineCurrentLookupKey();

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需要与targetDatasources这个Map中的key的类型进行匹配,由resolveSpecifiedLookupKey进行解析。

*/

@Nullable

protected abstract Object determineCurrentLookupKey();

}

复制代码

那么这个类的执行过程是怎么样的呢?

当程序要获取数据库的连接的时候,就会调用这个AbstractRoutingDataSource的getConnection(),当然,实际上,是通过DataSource.getConnection()进行调用的。

6a34a9950a8b8e70f39d0f9dd86fcd5e.png

问题现象

问题就出现在下面的代码中

protected DataSource determineTargetDataSource(){

Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");

Object lookupKey = determineCurrentLookupKey();

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;

}

复制代码

接下来,我仔细的演示一遍。

当我在进行保存操作的时候,调用determineCurrentLookupKey()的时候并没有获取到lookupKey.导致在获取数据源的时候,使用的是默认数据源(恰好默认数据源是Master),而我们使用的是Master数据源(巧合)。如下图0466a8708b8939b285c6323ed9111188.png

如果我把默认的数据源改成Slave1呢?-> 虽然我们在日志里打印的是master,但是我们实际上使用的是slave1

其实这个问题看日志的时候,也可以看出来的。d5f8e8b0ed07e30cb9197b025ba26a9b.png

保存用户的时候,使用的master,打印的日志是: 使用的数据源是: master. 但是! 上面还有两句话

HikariPool-1 - Starting...

HikariPool-1 - Start completed.

复制代码

而在查询用户的时候, 使用的数据源是slave2.在下面打印的是:

HikariPool-2 - Starting...

HikariPool-2 - Start completed.

复制代码

好了,现象就描述到这里. 到底什么原因呢?

后来发现,我在业务代码中Service层的save方法上加上了Transactional注解。其他方法上没加。我把注解加到类上发现也会出现这样的问题。这里就涉及到了一个问题: 查询方法上要不要加Transactional注解呢?

我的理解是: @1:如果一个查询方法里,涉及到多次对数据库的查询操作,建议添加注解@Transactional(readOnly=true)

@2:具体为什么加上注解Transactional就会产生这样的问题还没有研究过。

猜测可能是因为开启事务的时候是在进入方法之前操作的,但是我们的读写分离呢,是在进入代理方法之后确定的数据源。所以呢,就导致了会提前调用determineTargetDataSource方法,而我们使用AOP织入的时候,已经确定了数据源,不会再次调用determineTargetDataSource方法了.

@3:新问题: 上图中为什么前两次打印了HikariPool-2 - Starting... HikariPool-2 - Start completed.,之后的查询没有打印这句话呢?

解决方案

解决方案一

修改织入的位置:

@Pointcut("@annotation(com.fxb.doraemon.human.annotation.Master) " +

"|| execution(public * com.fxb.doraemon.human.service..*.save*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.insert*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.update*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.edit*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.delete*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.del*(..)) " +

"|| execution(public * com.fxb.doraemon.human.rest..*.remove*(..)) ")

public void writePointcut(){

}

复制代码

这种解决方案有个问题,就是在一次查询操作涉及两次查询的时候,还想开启事务的时候,就不能get*,select*来命名方法了。 还需要在方法上使用注解指定使用的数据源。

问题总结

不要只看现象,更要看本质。问题解决了,多看一个内部的运行机理。

留下的坑

@1:查询方法要不要添加事务注解。如果一个查询方法里,涉及到多次对数据库的查询操作,建议添加注解@Transactional(readOnly=true)

@2:具体为什么加上注解Transactional就会产生这样的问题还没有研究过。

@3:新问题: 上图中为什么前两次打印了HikariPool-2 - Starting... HikariPool-2 - Start completed.,之后的查询没有打印这句话呢?

最后

如果想了解最新动态,欢迎关注公众号: 方家小白. 欢迎一起交流学习。

341b906e522123e1e493f0804f478805.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值