1. AbstractRoutingDataSource
Spring提供了数据源切换抽线实现--AbstractRoutingDatasource类,通过继承该类可以实现动态数据源切换。
AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,通过spring的InitializingBean接口实现的afterPropertiesSet(),将数据源以key-value形式织入到内存中,然后通过AbstractRoutingDataSource实现的determineCurrentLookupKey()方法动态地获取数据源key,灵活的进行数据源切换。
实现逻辑:
- 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources(所有数据源)和defaultTargetDataSource(默认的数据源)中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
- 定义MultiDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法,动态获取不同数据源的key(一般是通过ThreadLocal处理),为切换数据源做准备。
- 最后AbstractRoutingDataSource会根据内存的数据源信息以及动态key,动态地获取具体数据源,并且通过调用底层datasource.getConnection()方法从数据库连接池中获取某一个连接(具体Connection如何得到不用关心),有了连接和数据源后,datasource底层会自动实现sql的crud。
2.Multi-datasource框架多数据源切换实现逻辑
基于Spring的AbstractRoutingDataSource,我们只需要
- 将默认数据源以及所有需要动态切换的数据源添加到AbstractRoutingDataSource类的
defaultTargetDataSource以及targetDataSources,格式为Map<数据源key,数据源>
- 实现AbstractRoutingDataSource的determineCurrentLookupKey()方法,动态获取数据源key
由于事务切换肯定需要提供注解(定为@MS)做到业务隔离的,故需要借助AOP切面功能,所以底层框架实现伪代码如下:
①:从配置文件中读取所有数据源配置,并封装为具体数据源对象,添加到defaultTargetDataSource以及targetDataSources中.
源码如下:
@PostConstruct
public void init() {
setPrimary(multiDataSourceProperties.getPrimary());
// 添加并分组数据源
Map<String, DataSource> dataSources = new HashMap<>(16);
// 从配置文件中添加数据源(也提供了从其他地方添加数据源的抽象类)
for (MultiDataSourceProvider provider : providers) {
dataSources.putAll(provider.loadDataSources());
}
Map<Object, Object> targetDataSources = new HashMap<>(dataSources);
// 添加到dataSourceMap中
for (Map.Entry<String, DataSource> dsItem : dataSources.entrySet()) {
addDataSource(dsItem.getKey(), dsItem.getValue());
}
// 检测默认数据源是否设置
if (dataSourceMap.containsKey(primary)) {
log.info("dynamic-datasource initial loaded [{}] datasource,primary datasource named [{}]", dataSources.size(), primary);
} else {
log.warn("dynamic-datasource initial loaded [{}] datasource,Please add your primary datasource or check your configuration", dataSources.size());
}
super.setDefaultTargetDataSource(dataSourceMap.get(primary));
super.setTargetDataSources(targetDataSources);
}
由于数据源加载方式有多种,包括但不限于yml、文件等,所以在这里提供了MultiDataSourceProvider接口的loadDataSources()方法,同时将公共创建方法通过模板模式AbstractDataSourceProvider提取出来,最好将所有数据源加载的方式添加进来,以yml加载方式为例:
@Slf4j
@AllArgsConstructor
public class YmlMultiDataSourceProvider extends AbstractDataSourceProvider {
/**
* 所有数据源
*/
private final Map<String, DataSourceProperty> dataSourcePropertiesMap;
/** 创建key为数据源名称,value为具体数据源的map信息
*
* @return
*/
@Override
public Map<String, DataSource> loadDataSources() {
return createDataSourceMap(dataSourcePropertiesMap);
}
}
@Slf4j
public abstract class AbstractDataSourceProvider implements MultiDataSourceProvider {
@Autowired
private DefaultDataSourceCreator defaultDataSourceCreator;
protected Map<String, DataSource> createDataSourceMap(
Map<String, DataSourceProperty> dataSourcePropertiesMap) {
Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
// 获取yml配置的所有数据源,并创建
for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
String dsName = item.getKey();
DataSourceProperty dataSourceProperty = item.getValue();
String poolName = dataSourceProperty.getPoolName();
if (poolName == null || "".equals(poolName)) {
poolName = dsName;
}
dataSourceProperty.setPoolName(poolName);
dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
}
return dataSourceMap;
}
}
同时因为数据源包括但不限于druid、jndi、c3p0,所以需要提供创建数据源的接口,供适配创建不同数据源。
public interface DataSourceCreator {
/**
* 通过属性创建数据源
*
* @param dataSourceProperty 数据源属性
* @return 被创建的数据源
*/
DataSource createDataSource(DataSourceProperty dataSourceProperty);
/**
* 当前创建器是否支持根据此属性创建
*
* @param dataSourceProperty 数据源属性
* @return 是否支持
*/
boolean support(DataSourceProperty dataSourceProperty);
}
public class DruidDataSourceCreator extends AbstractDataSourceCreator implements DataSourceCreator, InitializingBean {
@Override
public boolean support(DataSourceProperty dataSourceProperty) {
Class<? extends DataSource> type = dataSourceProperty.getType();
// 通过yml文件中配置的type决定是使用哪个数据源,没配置默认使用druid数据源
return type == null || "com.alibaba.druid.pool.DruidDataSource".equals(type.getName());
}
@Override
Datasource createDataSource(DataSourceProperty dataSourceProperty) {
// 创建druid数据源
}
}
②:借助AOP切面,编写@MS注解,动态获取数据源
@Aspect
public class MultiDatasourceAspect {
@Autowired
private MultiDataSourceProperties multiDataSourceProperties;
private static final Logger LOG = LogManager.getLogger(MultiDatasourceAspect.class);
@Pointcut("@annotation(com.jenkin.multi.datasource.annotation.MS)")
public void datasourcePointCut() {
}
@Around("datasourcePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String msKey = null;
try {
if (method.isAnnotationPresent(MS.class)) {
MS ms = method.getAnnotation(MS.class);
// 获取当前方法进行的操作
msKey = ms.value();
if (StringUtils.isEmpty(msKey)) {
msKey = multiDataSourceProperties.getPrimary();
}
}
// 放入当前线程中
MultiDataSourceContextHolder.push(msKey);
return joinPoint.proceed();
} catch (Exception e) {
LOG.error("Multi datasource exception for ",e);
throw e;
} finally {
MultiDataSourceContextHolder.poll();
}
}
}
通过ThreadLocal将数据源的key放入线程上线文中,注意当未在方法上使用@MS注解时,是不会进入该切面逻辑的,也就是说程序需要判断ThreadLocal获取到的key为空的情况。
③:实现AbstractRoutingDataSource抽象类的determineCurrentLookupKey()方法,动态获取key
@Slf4j
@Component
public class MultiRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String msKey = MultiDataSourceContextHolder.peek();
// 如果不添加注解时,就使用默认master数据源
if (StringUtils.isEmpty(msKey)) {
msKey = getPrimary();
}
return msKey;
}
……
}
这样就可以借助AbstractRoutingDataSource进行动态数据源的切换了。
3. 实现分布式事务(多数据源)
不同于多系统间的分布式事务,多数据源导致的分布式事务问题就无需引用springcloud的这种seata重量级框架来实现了。
对于传统单库事务的回滚操作,一般是通过try{ sql} catch {记录状态} finally {如果status异常了则获取Connection回滚,如果status正常则获取Connection执行sql}。
对于多库事务,也可以类似处理,所以重点在于如何获取所有需要执行sql库的所有connection
①:记录\获取所有Connection
进行数据库crud操作时,比如orderMapper.insert(order),就会执行AbstractRoutingDataSource类中的getConnection(),它是实现与Datasource接口,所以可以重写该方法,用以记录所有数据源的连接。
可以使用代理模式,创建Connection代理,用以处理Connection的所有操作
@Slf4j
public class ConnectionProxy implements Connection {
private Connection connection;
private String ds;
public ConnectionProxy(Connection connection, String ds) {
this.connection = connection;
this.ds = ds;
}
public void notify(Boolean commit) throws SQLException {
try {
if (commit) {
connection.commit();
} else {
connection.rollback();
}
} catch (SQLException e) {
log.error(e.getLocalizedMessage(), e);
throw e;
} finally {
try {
connection.close();
} catch (SQLException e2) {
log.error("db connection close failed", e2);
}
}
}
@Override
public void commit() throws SQLException {
//connection.commit();
}
@Override
public void rollback() throws SQLException {
//connection.rollback();
}
@Override
public void close() throws SQLException {
//connection.close();
}
……
}
有这个代理就可以通过notify方法对数据库crud操作进行commit或者rollback,从而到达事务管理。
对于Connection的创建和获取,可以通过工厂模式构建,通过putConnection()方法,将连接存放到 CONNECTION_HOLDER中;通过getConnection(String ds)方法获取具体数据源的连接。
public class ConnectionFactory {
private static final ThreadLocal<Map<String, ConnectionProxy>> CONNECTION_HOLDER =
new ThreadLocal<Map<String, ConnectionProxy>>() {
@Override
protected Map<String, ConnectionProxy> initialValue() {
return new ConcurrentHashMap<>(8);
}
};
public static void putConnection(String ds, ConnectionProxy connection) {
Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
if (!concurrentHashMap.containsKey(ds)) {
try {
connection.setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
concurrentHashMap.put(ds, connection);
}
}
public static ConnectionProxy getConnection(String ds) {
return CONNECTION_HOLDER.get().get(ds);
}
public static void notify(Boolean state) throws Exception {
Exception exception = null;
try {
Map<String, ConnectionProxy> concurrentHashMap = CONNECTION_HOLDER.get();
for (ConnectionProxy connectionProxy : concurrentHashMap.values()) {
try {
connectionProxy.notify(state);
} catch (SQLException e) {
exception = e;
}
}
} finally {
CONNECTION_HOLDER.remove();
if (exception != null) {
throw exception;
}
}
}
}
有了以上准备,就可以实现AbstractRoutingDataSource类的getConnection方法,将Connection记录
@Slf4j
@Component
public class MultiRoutingDataSource extends AbstractRoutingDataSource {
@Override
public Connection getConnection() throws SQLException {
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
// datasource.getConnection()底层通过从数据库连接池中获取某个Connection,具体哪一个不需要关关心
return determineDataSource().getConnection();
} else {
String ds = MultiDataSourceContextHolder.peek();
ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
}
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
String xid = TransactionContext.getXID();
if (StringUtils.isEmpty(xid)) {
return determineDataSource().getConnection(username, password);
} else {
String ds = MultiDataSourceContextHolder.peek();
ds = StringUtils.isEmpty(ds) ? getPrimary() : ds;
ConnectionProxy connection = ConnectionFactory.getConnection(ds);
return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection(username, password))
: connection;
}
}
…...
}
这样在多数据源切换时,就会将所有Connection加入到CONNECTION_HOLDER中。
②:创建@MSTransaction注解,添加AOP切面
@Aspect
public class MultiDatasourceTransactionAspect {
@Autowired
private MultiDataSourceProperties multiDataSourceProperties;
private static final Logger LOG = LogManager.getLogger(MultiDatasourceTransactionAspect.class);
@Pointcut("@annotation(com.jenkin.multi.datasource.annotation.MSTransaction)")
public void multiDatasourceTransactionPointCut() {
}
@Around("multiDatasourceTransactionPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 在分布式事务或多数据源事务的场景中,可能存在已经启动的外部事务,
// 比如由某个全局事务管理器(如分布式事务框架或者 Spring 的事务管理器)创建的事务。此时,就不需要再次启动新的本地事务。
if (!StringUtils.isEmpty(TransactionContext.getXID())) {
return joinPoint.proceed();
}
boolean state = true;
Object o;
LocalTxUtil.startTransaction();
try {
o = joinPoint.proceed();
} catch (Exception e) {
state = false;
throw e;
} finally {
if (state) {
LocalTxUtil.commit();
} else {
LocalTxUtil.rollback();
}
}
return o;
}
}
@Slf4j
public final class LocalTxUtil {
/**
* 手动开启事务
*/
public static void startTransaction() {
if (!StringUtils.isEmpty(TransactionContext.getXID())) {
log.debug("dynamic-datasource exist local tx [{}]", TransactionContext.getXID());
} else {
String xid = UUID.randomUUID().toString();
TransactionContext.bind(xid);
log.debug("dynamic-datasource start local tx [{}]", xid);
}
}
/**
* 手动提交事务
*/
public static void commit() throws Exception {
try {
ConnectionFactory.notify(true);
} finally {
log.debug("dynamic-datasource commit local tx [{}]", TransactionContext.getXID());
TransactionContext.remove();
}
}
/**
* 手动回滚事务
*/
public static void rollback() throws Exception {
try {
ConnectionFactory.notify(false);
} finally {
log.debug("dynamic-datasource rollback local tx [{}]", TransactionContext.getXID());
TransactionContext.remove();
}
}
}