隨着公司業務的不斷擴大,核心業務的數據量也是爆炸性增長。因爲數據庫選用和大多數據互聯網公司一樣使用的是 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
這個類,它的作用就是持有Advisor
與Pointcut
類
@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。