模仿 Spring 註解事務寫出優雅多數據源切換代碼

隨着公司業務的不斷擴大,核心業務的數據量也是爆炸性增長。因爲數據庫選用和大多數據互聯網公司一樣使用的是 Mysql 很多表的數據量都超過了 1 kw,所以決定對大表進行數據擴容。並且在容量擴容的時候決定使用雙寫方案。在調研的時候,有三個方案可以選擇:

  • Sharding-jdbc:模仿分片處理,繼承 AbstractShardingPreparedStatementAdapter 重寫 jdbc 原生 PreparedStatement,但是由於老表與分片表需要表一致,這個和 sharding-jdbc 的表路由衝突,排除。
  • Mybatsi 擴展:繼承 MapperFactoryBean添加雙寫規則,然後在註解 org.mybatis.spring.annotation.MapperScan 指定 factoryBean 。這種方式對業務方傾入太多,並且實現比較複雜,排除。
  • Spring AOP 動態數據源:使用 Spring AOP 動態數據源,由業務方在業務操作的時候指定數據庫,對舊數據庫使用原來的數據源(普通數據源)。需要把數據添加到分片數據源的時候就指定操作數據源爲新數據源(sharding-jdbc 數據源)。

網絡上大多是通過 @Aspect 切面來完成數據源,我之前的博客也是這種實現 – Spring AOP 動態多數據源。但是業務方使用起來不夠簡潔,所以我就模仿 Spring 事務註解處理優化了一下。Spring 註解處理的核心其實就是:

  • @Transactional:Spring 事務處理註解,其實也就是 Spring 對事務屬性的定義。主要包含:事務的傳播特性與隔離級別及能夠回滾的異常等。。可以標註在方法上,也可以標註在類上。以標註在方法上優先處理。
  • @EnableTransactionManagement: 這個註解引用 TransactionManagementConfigurationSelector 通過實現ImportSelector 引入 Class 配置類 ProxyTransactionManagementConfiguration 添加 @Transactional 註解事務處理能力。 @EnableXxxx 在 Spring framework 裏面是使得具有什麼的能力,比如 @EnableWebMvc 是具有配置 Spring MVC 擴展的能力
  • ProxyTransactionManagementConfiguration:裏面有三個 bean 配置,一個是 TransactionInterceptor,它實現了 ··MethodInterceptor··,實現方法增強,其實是對事務的具體處理;一個是 TransactionAttributeSource,在 Spring 處理的時候抽象了 Spring 事務的動作處理 PlatformTransactionManager 包括獲取事務,提交事務,回滾事務,具體的調用其實是在 TransactionInterceptor,同樣的對於事務的屬性也有具體的抽象,就是 TransactionAttribute,而 TransactionAttributeSource 就是用來解析事務屬性的抽象接口它的作用類似於 pointcut,如果能夠獲取到事務屬性就進行事務增強,反之則不進行事務增強;最後一個就是 BeanFactoryTransactionAttributeSourceAdvisor它其實就是一個 Advisor。Spring 在進行 AOP 處理的時候就是一個一個的Advisor,這個對象裏面包含 2 個對象。一個是 Pointcut也就是哪些地方需要被增強,另外一個是 Advice 也就是方法需要如何增強。

下面我們就對應 Spring 事務註解,我們來寫一個多數據源切換:

1、@DataSource

@DataSource 其實是多數據源指定數據源註解。它在方法或者類上指定需要操作的數據源,其中方法標註的數據源優先於類上標註的數據源

@Documented
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

	String value() default DatasourceContextHolder.DATASOURCE_NO_SHARDING;

}

2、@EnableDataSource

@EnableDataSource激活多數據源註解。通過引用實現了 ImportSelector 接口的 ShardingConfigurationSelector 引入 ProxyShardingConfiguration 這個 Spring Bean Java 配置類。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ShardingConfigurationSelector.class)
public @interface EnableDataSource {

}

public class ShardingConfigurationSelector implements ImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		return new String[] {ProxyShardingConfiguration.class.getName()};
	}

}

3、ProxyShardingConfiguration

ProxyShardingConfiguration 就是一個 spring java bean 配置類,裏面包括了 spring aop 增強的三大元素。

  • Advise: 就是 ShardingInterceptor這個通知類,它主要是用於對方法的增強
  • Pointcut:就是DataSourcePointcut 這個切面類,它的作用就是哪些方法需要被增強
  • Advisor:就是DefaultBeanFactoryPointcutAdvisor 這個類,它的作用就是持有 AdvisorPointcut
@Configuration
public class ProxyShardingConfiguration {

	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@Bean
	public DefaultBeanFactoryPointcutAdvisor shardingAdvisor() {
		DefaultBeanFactoryPointcutAdvisor advisor = new DefaultBeanFactoryPointcutAdvisor();
		advisor.setPointcut(dataSourcePointcut());
		advisor.setAdvice(shardingInterceptor());
		return advisor;
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public DataSourcePointcut dataSourcePointcut(){
		return new DataSourcePointcut();
	}

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public ShardingInterceptor shardingInterceptor() {
		ShardingInterceptor interceptor = new ShardingInterceptor();
		return interceptor;
	}

}

4、ShardingInterceptor

方法增強類,指定當前業務方需要操作的數據源。因爲數據源註解只有一個 String 這個數據源 key 。所以就不需要數據源註解解析類了。它的作用是在方法執行前獲取到 @Datasource 裏面定義的數據源 key 添加到 ThreadLocal 當中,然後在 finally 塊裏面清除數據源 key。

public class ShardingInterceptor implements MethodInterceptor {

	private final static String NULL_DATASOURCE_ATTRIBUTE = "null";

	private final Map<Object, String> attributeCache = new ConcurrentHashMap<>(1024);

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();
		Class<?> declaringClass = invocation.getMethod().getDeclaringClass();
		String dataSourceAttribute = getDataSourceAttribute(method, declaringClass);
		if(StringUtils.hasText(dataSourceAttribute)){
			DatasourceContextHolder.setDataSourceKey(dataSourceAttribute);
		}
		Object result;
		try {
			result = invocation.proceed();
		} finally {
			DatasourceContextHolder.clearDataSourceKey();
		}
		return result;
	}

	public String getDataSourceAttribute(Method method, Class<?> targetClass) {
		if (method.getDeclaringClass() == Object.class) {
			return null;
		}

		// First, see if we have a cached value.
		Object cacheKey = getCacheKey(method, targetClass);
		String cached = this.attributeCache.get(cacheKey);
		if (cached != null) {
			// Value will either be canonical value indicating there is no transaction attribute,
			// or an actual transaction attribute.
			if (cached == NULL_DATASOURCE_ATTRIBUTE) {
				return null;
			}
			else {
				return cached;
			}
		} else {
			// We need to work it out.
			String txAttr = computeDataSourceAttribute(method, targetClass);
			// Put it in the cache.
			if (StringUtils.hasText(txAttr)) {
				this.attributeCache.put(cacheKey, txAttr);
			} else {
				this.attributeCache.put(cacheKey, NULL_DATASOURCE_ATTRIBUTE);
			}
			return txAttr;
		}
	}

	/**
	 * Determine a cache key for the given method and target class.
	 * <p>Must not produce same key for overloaded methods.
	 * Must produce same key for different instances of the same method.
	 * @param method the method (never {@code null})
	 * @param targetClass the target class (may be {@code null})
	 * @return the cache key (never {@code null})
	 */
	protected Object getCacheKey(Method method, Class<?> targetClass) {
		return new MethodClassKey(method, targetClass);
	}

	private String computeDataSourceAttribute(Method method, Class<?> targetClass){
		// Ignore CGLIB subclasses - introspect the actual user class.
		Class<?> userClass = ClassUtils.getUserClass(targetClass);
		// The method may be on an interface, but we need attributes from the target class.
		// If the target class is null, the method will be unchanged.
		Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass);
		// If we are dealing with method with generic parameters, find the original method.
		specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
		// First try is the method in the target class.
		String txAttr = findDataSourceAttribute(specificMethod);
		if (txAttr != null) {
			return txAttr;
		}
		// Second try is the transaction attribute on the target class.
		txAttr = findDataSourceAttribute(specificMethod.getDeclaringClass());
		if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
			return txAttr;
		}
		if (specificMethod != method) {
			// Fallback is to look at the original method.
			txAttr = findDataSourceAttribute(method);
			if (txAttr != null) {
				return txAttr;
			}
			// Last fallback is the class of the original method.
			txAttr = findDataSourceAttribute(method.getDeclaringClass());
			if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
				return txAttr;
			}
		}

		return null;
	}

	private String findDataSourceAttribute(AnnotatedElement annotatedElement){
		DataSource dataSourceAnnotation = annotatedElement.getAnnotation(DataSource.class);
		if(dataSourceAnnotation != null) {
			return dataSourceAnnotation.value();
		}
		return null;
	}

}

5、DataSourcePointcut

動態數據源 Pointcut,方法或者類上標註@DataSource 的 spring bean 都會被增強。

public class DataSourcePointcut extends StaticMethodMatcherPointcut {

	@Override
	public boolean matches(Method method, Class<?> aClass) {
		return matchesInternal(method) || matchesInternal(aClass);
	}

	private boolean matchesInternal(AnnotatedElement annotatedElement) {
		return annotatedElement.getAnnotation(DataSource.class) != null;
	}

}

6、DatasourceContextHolder

通過 ThreadLocal 保存並傳遞 數據源的 key 值。

public class DatasourceContextHolder {

	public static final String DATASOURCE_SHARDING = "shardingDataSource";

	public static final String DATASOURCE_NO_SHARDING = "noShardingDataSource";

	public static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

	public static void setDataSourceKey(String dataSourceKey) {
		contextHolder.set(dataSourceKey);
	}

	public static String getDataSourceKey() {
		return contextHolder.get();
	}

	public static void clearDataSourceKey() {
		contextHolder.remove();
	}

}

7、SmartShardingDatasource

繼承 AbstractRoutingDataSource 這個 spring 動態數據源。通過業務方定義的多數據源,然後從DatasourceContextHolder 這個 ThreadLocal 對象獲取到需要操作的數據源。

public class SmartShardingDatasource extends AbstractRoutingDataSource {

	@Override
	protected Object determineCurrentLookupKey() {
		return DatasourceContextHolder.getDataSourceKey();
	}

}

使用方式與 Spring 註解事務類似。that is all。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章