数据库读写分离本篇文章是在 数据源读写分离 基础上发现的一个小问题。目录背景知识储备问题现象解决方案问题总结背景Springboot集成mybatis使用HikariCP连接MySQL,进行读写分离。
数据库读写分离
本篇文章是在 数据源读写分离 基础上发现的一个小问题。
目录
- 背景
- 知识储备
- 问题现象
- 解决方案
- 问题总结
背景
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<Object, Object> targetDataSources; /** * 默认的数据源 */ @Nullable private Object defaultTargetDataSource; /** * ?? */ private boolean lenientFallback = true; /** * 数据源查找?? */ private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); @Nullable private Map<Object, DataSource> 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<Object, Object> 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> T unwrap(Class<T> 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()
进行调用的。
问题现象
问题就出现在下面的代码中
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
数据源(巧合)。如下图

Slave1
呢?-> 虽然我们在日志里打印的是master
,但是我们实际上使用的是slave1
其实这个问题看日志的时候,也可以看出来的。

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.
,之后的查询没有打印这句话呢?
最后
如果想了解最新动态,欢迎关注公众号: 方家小白. 欢迎一起交流学习。
.

- 0