1. 程式人生 > 實用技巧 >ShardingSphere 如何實現系統的擴充套件性

ShardingSphere 如何實現系統的擴充套件性

什麼是微核心架構?

微核心是一種典型的架構模式 ,區別於普通的設計模式,架構模式是一種高層模式,用於描述系統級的結構組成、相互關係及相關約束。微核心架構在開源框架中的應用也比較廣泛,除了 ShardingSphere 之外,在主流的 PRC 框架 Dubbo 中也實現了自己的微核心架構。那麼,在介紹什麼是微核心架構之前,我們有必要先闡述這些開源框架會使用微核心架構的原因。

為什麼要使用微核心架構?

微核心架構本質上是為了提高系統的擴充套件性 。所謂擴充套件性,是指系統在經歷不可避免的變更時所具有的靈活性,以及針對提供這樣的靈活性所需要付出的成本間的平衡能力。也就是說,當在往系統中新增新業務時,不需要改變原有的各個元件,只需把新業務封閉在一個新的元件中就能完成整體業務的升級,我們認為這樣的系統具有較好的可擴充套件性。

就架構設計而言,擴充套件性是軟體設計的永恆話題。而要實現系統擴充套件性,一種思路是提供可插拔式的機制來應對所發生的變化。當系統中現有的某個元件不滿足要求時,我們可以實現一個新的元件來替換它,而整個過程對於系統的執行而言應該是無感知的,我們也可以根據需要隨時完成這種新舊元件的替換。

比如在下個課時中我們將要介紹的 ShardingSphere 中提供的分散式主鍵功能,分散式主鍵的實現可能有很多種,而擴充套件性在這個點上的體現就是, 我們可以使用任意一種新的分散式主鍵實現來替換原有的實現,而不需要依賴分散式主鍵的業務程式碼做任何的改變

微核心架構模式為這種實現擴充套件性的思路提供了架構設計上的支援,ShardingSphere 基於微核心架構實現了高度的擴充套件性。在介紹如何實現微核心架構之前,我們先對微核心架構的具體組成結構和基本原理做簡要的闡述。

什麼是微核心架構?

從組成結構上講, 微核心架構包含兩部分元件:核心系統和外掛 。這裡的核心系統通常提供系統執行所需的最小功能集,而外掛是獨立的元件,包含自定義的各種業務程式碼,用來向核心系統增強或擴充套件額外的業務能力。在 ShardingSphere 中,前面提到的分散式主鍵就是外掛,而 ShardingSphere 的執行時環境構成了核心系統。

那麼這裡的外掛具體指的是什麼呢?這就需要我們明確兩個概念,一個概念就是經常在說的 API ,這是系統對外暴露的介面。而另一個概念就是 SPI(Service Provider Interface,服務提供介面),這是外掛自身所具備的擴充套件點。就兩者的關係而言,API 面向業務開發人員,而 SPI 面向框架開發人員,兩者共同構成了 ShardingSphere 本身。

可插拔式的實現機制說起來簡單,做起來卻不容易,我們需要考慮兩方面內容。一方面,我們需要梳理系統的變化並把它們抽象成多個 SPI 擴充套件點。另一方面, 當我們實現了這些 SPI 擴充套件點之後,就需要構建一個能夠支援這種可插拔機制的具體實現,從而提供一種 SPI 執行時環境

那麼,ShardingSphere 是如何實現微核心架構的呢?讓我們來一起看一下。

如何實現微核心架構?

事實上,JDK 已經為我們提供了一種微核心架構的實現方式,這種實現方式針對如何設計和實現 SPI 提出了一些開發和配置上的規範,ShardingSphere 使用的就是這種規範。首先,我們需要設計一個服務介面,並根據需要提供不同的實現類。接下來,我們將模擬實現分散式主鍵的應用場景。

基於 SPI 的約定,建立一個單獨的工程來存放服務介面,並給出介面定義。請注意 這個服務介面的完整類路徑為 com.tianyilan.KeyGenerator ,介面中只包含一個獲取目標主鍵的簡單示例方法。

public interface KeyGenerator{ 
    String getKey(); 
}

針對該介面,提供兩個簡單的實現類,分別是基於 UUID 的 UUIDKeyGenerator 和基於雪花演算法的 SnowflakeKeyGenerator。

/**
 * @author WGR
 * @create 2020/11/19 -- 18:54
 */
public class UUIDKeyGenerator implements KeyGenerator {
    @Override
    public String getKey() {
        return "UUIDKey";
    }
}
/**
 * @author WGR
 * @create 2020/11/19 -- 18:55
 */
public class SnowflakeKeyGenerator implements KeyGenerator {
    @Override
    public String getKey() {
        return "SnowflakeKey";
    }
}

接下來的這個步驟很關鍵, 在這個程式碼工程的 META-INF/services/ 目錄下,需要建立一個以服務介面完整類路徑 com.dalianpai.KeyGenerator 命名的檔案 ,檔案的內容是指向該介面所對應的兩個實現類的完整類路徑 com.dalianpai.UUIDKeyGenerator 和 com.dalianpai. SnowflakeKeyGenerator。

我們把這個程式碼工程打成一個 jar 包,然後新建另一個程式碼工程,該程式碼工程需要這個 jar 包,並完成如下所示的 Main 函式。

/**
 * @author WGR
 * @create 2020/11/19 -- 18:56
 */
public class Test {
    public static void main(String[] args) {
        ServiceLoader<KeyGenerator> generators = ServiceLoader.load(KeyGenerator.class);
        for (KeyGenerator generator : generators) {
            System.out.println(generator.getClass());
            String key = generator.getKey();
            System.out.println(key);
        }
    }
}

現在,該工程的角色是 SPI 服務的使用者,這裡使用了 JDK 提供的 ServiceLoader 工具類來獲取所有 KeyGenerator 的實現類。現在在 jar 包的 META-INF/services/com.dalianpai.KeyGenerator 檔案中有兩個 KeyGenerator 實現類的定義。執行這段 Main 函式,我們將得到的輸出結果如下:

class com.dalianpai.UUIDKeyGenerator
UUIDKey
class com.dalianpai.SnowflakeKeyGenerator
SnowflakeKey

如果我們調整 META-INF/services/com.dalianpai.KeyGenerator 檔案中的內容,去掉 com.dalianpai.UUIDKeyGenerator 的定義,並重新打成 jar 包供 SPI 服務的使用者進行引用。再次執行 Main 函式,則只會得到基於 SnowflakeKeyGenerator 的輸出結果。

至此, 完整 的 SPI 提供者和使用者的實現過程演示完畢。我們通過一張圖,總結基於 JDK 的 SPI 機制實現微核心架構的開發流程:

這個示例非常簡單,但卻是 ShardingSphere 中實現微核心架構的基礎。接下來,就讓我們把話題轉到 ShardingSphere,看看 ShardingSphere 中應用 SPI 機制的具體方法。

ShardingSphere 如何基於微核心架構實現擴充套件性?

ShardingSphere 中微核心架構的實現過程並不複雜,基本就是對 JDK 中 SPI 機制的封裝。讓我們一起來看一下。

ShardingSphere 中的微核心架構基礎實現機制

我們發現,在 ShardingSphere 原始碼的根目錄下,存在一個獨立的工程 shardingsphere-spi。顯然,從命名上看,這個工程中應該包含了 ShardingSphere 實現 SPI 的相關程式碼。我們快速瀏覽該工程,發現裡面只有一個介面定義和兩個工具類。我們先來看這個介面定義 TypeBasedSPI:

public interface TypeBasedSPI { 
    //獲取SPI對應的型別 
    String getType(); 
    //獲取屬性 
    Properties getProperties(); 
    //設定屬性 
    void setProperties(Properties properties); 
}

從定位上看,這個介面在 ShardingSphere 中應該是一個頂層介面,我們已經在上一課時給出了這一介面的實現類類層結構。接下來再看一下 NewInstanceServiceLoader 類,從命名上看,不難想象該類的作用類似於一種 ServiceLoader,用於載入新的目標物件例項:

public final class NewInstanceServiceLoader { 
    private static final Map<Class, Collection<Class<?>>> SERVICE_MAP = new HashMap<>(); 
    //通過ServiceLoader獲取新的SPI服務例項並註冊到SERVICE_MAP中
    public static <T> void register(final Class<T> service) { 
        for (T each : ServiceLoader.load(service)) { 
            registerServiceClass(service, each); 
        } 
    } 
    @SuppressWarnings("unchecked") 
    private static <T> void registerServiceClass(final Class<T> service, final T instance) { 
        Collection<Class<?>> serviceClasses = SERVICE_MAP.get(service); 
        if (null == serviceClasses) { 
            serviceClasses = new LinkedHashSet<>(); 
        } 
        serviceClasses.add(instance.getClass()); 
        SERVICE_MAP.put(service, serviceClasses); 
    } 
    @SneakyThrows 
    @SuppressWarnings("unchecked") 
    public static <T> Collection<T> newServiceInstances(final Class<T> service) { 
        Collection<T> result = new LinkedList<>(); 
        if (null == SERVICE_MAP.get(service)) { 
            return result; 
        } 
        for (Class<?> each : SERVICE_MAP.get(service)) { 
            result.add((T) each.newInstance()); 
        } 
        return result; 
    } 
}

在上面這段程式碼中, 首先看到了熟悉的 ServiceLoader.load(service) 方法,這是 JDK 中 ServiceLoader 工具類的具體應用。同時,注意到 ShardingSphere 使用了一個 HashMap 來儲存類的定義以及類的例項之 間 的一對多關係,可以認為,這是一種用於提高訪問效率的快取機制。

最後,我們來看一下 TypeBasedSPIServiceLoader 的實現,該類依賴於前面介紹的 NewInstanceServiceLoader 類。 下面這段程式碼演示了 基於 NewInstanceServiceLoader 獲取例項類列表,並根據所傳入的型別做過濾:

 //使用NewInstanceServiceLoader獲取例項類列表,並根據型別做過濾 
    private Collection<T> loadTypeBasedServices(final String type) { 
        return Collections2.filter(NewInstanceServiceLoader.newServiceInstances(classType), new Predicate<T>() { 
            @Override 
            public boolean apply(final T input) { 
                return type.equalsIgnoreCase(input.getType()); 
            } 
        }); 
    }

TypeBasedSPIServiceLoader 對外暴露了服務的介面,對通過 loadTypeBasedServices 方法獲取的服務例項設定對應的屬性然後返回:

	//基於型別通過SPI建立例項 
    public final T newService(final String type, final Properties props) { 
        Collection<T> typeBasedServices = loadTypeBasedServices(type); 
        if (typeBasedServices.isEmpty()) { 
            throw new RuntimeException(String.format("Invalid `%s` SPI type `%s`.", classType.getName(), type)); 
        } 
        T result = typeBasedServices.iterator().next(); 
        result.setProperties(props); 
        return result; 
	}

同時,TypeBasedSPIServiceLoader 也對外暴露了不需要傳入型別的 newService 方法,該方法使用了 loadFirstTypeBasedService 工具方法來獲取第一個服務例項:

	//基於預設型別通過SPI建立例項 
    public final T newService() { 
        T result = loadFirstTypeBasedService(); 
        result.setProperties(new Properties()); 
        return result; 
	} 
    private T loadFirstTypeBasedService() { 
        Collection<T> instances = NewInstanceServiceLoader.newServiceInstances(classType); 
        if (instances.isEmpty()) { 
            throw new RuntimeException(String.format("Invalid `%s` SPI, no implementation class load from SPI.", classType.getName())); 
        } 
        return instances.iterator().next(); 
	}

這樣,shardingsphere-spi 程式碼工程中的內容就介紹完畢。 這部分內容相當於是 ShardingSphere 中所提供的外掛執行時環境 。下面我們基於 ShardingSphere 中提供的幾個典型應用場景來討論這個執行時環境的具體使用方法。

微核心架構在 ShardingSphere 中的應用

  • SQL 解析器 SQLParser

SQLParser 類,該類負責將具體某一條 SQL 解析成一個抽象語法樹的整個過程。而這個 SQLParser 的生成由 SQLParserFactory 負責:

	public final class SQLParserFactory { 
	    public static SQLParser newInstance(final String databaseTypeName, final String sql) { 
	     //通過SPI機制載入所有擴充套件 
	     for (SQLParserEntry each : NewInstanceServiceLoader.newServiceInstances(SQLParserEntry.class)) { 
	        … 
	    } 
	}

可以看到,這裡並沒有使用前面介紹的 TypeBasedSPIServiceLoader 來載入例項,而是直接使用更為底層的 NewInstanceServiceLoader。

這裡引入的 SQLParserEntry 介面就位於 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.spi 包中。顯然,從包的命名上看,該介面是一個 SPI 介面。在 SQLParserEntry 類層結構介面中包含一批實現類,分別對應各個具體的資料庫:

我們先來看針對 MySQL 的程式碼工程 shardingsphere-sql-parser-mysql,在 META-INF/services 目錄下,我們找到了一個org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 檔案:

可以看到這裡指向了 org.apache.shardingsphere.sql.parser.MySQLParserEntry 類。再來到 Oracle 的程式碼工程 shardingsphere-sql-parser-oracle,在 META-INF/services 目錄下,同樣找到了一個 org.apache.shardingsphere.sql.parser.spi.SQLParserEntry 檔案:

顯然,這裡應該指向 org.apache.shardingsphere.sql.parser.OracleParserEntry 類,通過這種方式,系統在執行時就會根據類路徑動態載入 SPI。

可以注意到,在 SQLParserEntry 介面的類層結構中,實際並沒有使用到 TypeBasedSPI 介面 ,而是完全採用了 JDK 原生的 SPI 機制。

  • 配置中心 ConfigCenter

接下來,我們來找一個使用 TypeBasedSPI 的示例,比如代表配置中心的 ConfigCenter:

public interface ConfigCenter extends TypeBasedSPI

顯然,ConfigCenter 介面繼承了 TypeBasedSPI 介面,而在 ShardingSphere 中也存在兩個 ConfigCenter 介面的實現類,一個是 ApolloConfigCenter,一個是 CuratorZookeeperConfigCenter。

在 sharding-orchestration-core 工程的 org.apache.shardingsphere.orchestration.internal.configcenter 中,我們找到了 ConfigCenterServiceLoader 類,該類擴充套件了前面提到的 TypeBasedSPIServiceLoader 類:

public final class ConfigCenterServiceLoader extends TypeBasedSPIServiceLoader<ConfigCenter> { 
    static { 
        NewInstanceServiceLoader.register(ConfigCenter.class); 
    } 
    public ConfigCenterServiceLoader() { 
        super(ConfigCenter.class); 
    } 
    //基於SPI載入ConfigCenter 
    public ConfigCenter load(final ConfigCenterConfiguration configCenterConfig) { 
        Preconditions.checkNotNull(configCenterConfig, "Config center configuration cannot be null."); 
        ConfigCenter result = newService(configCenterConfig.getType(), configCenterConfig.getProperties()); 
        result.init(configCenterConfig); 
        return result; 
    } 
}

那麼它是如何實現的呢? 首先,ConfigCenterServiceLoader 類通過 NewInstanceServiceLoader.register(ConfigCenter.class) 語句將所有 ConfigCenter 註冊到系統中,這一步會通過 JDK 的 ServiceLoader 工具類載入類路徑中的所有 ConfigCenter 例項。

我們可以看到在上面的 load 方法中,通過父類 TypeBasedSPIServiceLoader 的 newService 方法,基於型別建立了 SPI 例項。

以 ApolloConfigCenter 為例,我們來看它的使用方法。在 sharding-orchestration-config-apollo 工程的 META-INF/services 目錄下,應該存在一個名為 org.apache.shardingsphere.orchestration.config.api.ConfigCenter 的配置檔案,指向 ApolloConfigCenter 類:

其他的 ConfigCenter 實現也是一樣,你可以自行查閱 sharding-orchestration-config-zookeeper-curator 等工程中的 SPI 配置檔案。

至此,我們全面瞭解了 ShardingSphere 中的微核心架構,也就可以基於 ShardingSphere 所提供的各種 SPI 擴充套件點提供滿足自身需求的具體實現。

從原始碼解析到日常開發

在日常開發過程中,我們一般可以直接使用 JDK 的 ServiceLoader 類來實現 SPI 機制。當然,我們也可以採用像 ShardingSphere 的方式對 ServiceLoader 類進行一層簡單的封裝,並新增屬性設定等自定義功能。

同時,我們也應該注意到,ServiceLoader 這種實現方案也有一定缺點:

  • 一方面,META/services 這個配置檔案的載入地址是寫死在程式碼中,缺乏靈活性。
  • 另一方面,ServiceLoader 內部採用了基於迭代器的載入方法,會把配置檔案中的所有 SPI 實現類都載入到記憶體中,效率不高。

所以如果需要提供更高的靈活性和效能,我們也可以基於 ServiceLoader 的實現方法自己開發適合自身需求的 SPI 載入 機制。