2020年4月26日 星期日

使用交易的傳播機制

@Transactional的屬性propagation定義了交易的邊界(boundary)或稱為傳播的方式。因為@Transactional標註的方法可能再呼叫其他方法,Spring會呼叫TransactionManagergetTransaction()方法來取得或建立與資料庫的交易,並根據屬性propagation的設定決定要啟動和暫停交易;但並非TransactionManager的所有實作都直接支援所有交易傳播方式。後續介紹幾種常用的傳播方式以及它們如何作用。
REQUIRED的機制傳播交易
Propagation.REQUIRED」是屬性propagation預設值,使用方式如下:
@Transactional (propagation = Propagation.REQUIRED)
public void requiredCase() { 
    // ... 
}
因為是預設,也可以省略:
@Transactional
public void requiredCase() { 
    // ... 
}
若方法上標註@Transactional (propagation = Propagation.REQUIRED)Spring將:
1.     檢查是否有使用中的交易,如果不存在就「建立一個新的交易」。
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return createNewTransaction();
SUPPORTS的機制傳播交易
若屬性propagation值使用「Propagation.SUPPORTS」:
@Transactional (propagation = Propagation.SUPPORTS)
public void supportsCase() { 
    // ... 
}
Spring將:
1.     檢查是否有使用中的交易,如果不存在就「不使用交易」。
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
return emptyTransaction();
MANDATORY的機制傳播交易
若屬性propagation值使用「Propagation.MANDATORY」:
@Transactional(propagation = Propagation.MANDATORY)
public void mandatoryCase() { 
    // ... 
}
Spring將:
1.     檢查是否有使用中的交易,如果不存在就「拋出例外」
2.     若存在交易但驗證為無效,則拋出例外。
3.     若存在交易且驗證為有效,則使用該交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    if (isValidateExistingTransaction()) {
        validateExisitingAndThrowExceptionIfNotValid();
    }
    return existing;
}
throw IllegalTransactionStateException;
NEVER的機制傳播交易
若屬性propagation值使用「Propagation.NEVER」:
@Transactional(propagation = Propagation.NEVER)
public void neverCase() { 
    // ... 
}
Spring預期不存在交易;執行時期將檢查是否有使用中的交易,若存在就拋出例外
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    throw IllegalTransactionStateException;
}
return emptyTransaction;
NOT_SUPPORTED的機制傳播交易
若屬性propagation值使用「Propagation.NOT_SUPPORTED」:
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void notSupportedCase() { 
    // ... 
}
Spring將檢查是否有使用中的交易,如果存在就「暫停」;因此商業邏輯將在交易暫停的情況下被執行。
使用JTATransactionManager可以直接支援這類需求,其他情況則可能是以模擬的方式進行。
REQUIRES_NEW的機制傳播交易
若屬性propagation值使用「Propagation.NEVER」:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void requiresNewCase() { 
    // ... 
}
Spring將:
1.     檢查是否有使用中的交易,如果不存在就「建立新交易」
2.     若存在交易則暫停原交易,並建立新交易。
這樣的概念可以使用以下的偽代碼(pseudo-code)來示意:
if (isExistingTransaction()) {
    suspend(existing);
    try {
        return createNewTransaction();
    } catch (exception) {
        resumeAfterBeginException();
        throw exception;
    }
}
return createNewTransaction();
NOT_SUPPORTED相似,使用JTATransactionManager可以直接支援這類需求,其他情況則可能是以模擬的方式進行。

使用標註類別@Transactional進行交易

標註類別@org.springframework.transaction.annotation.Transactional用來將方法內的資料庫存取包含於資料庫交易中,可以設定交易的傳播(propagation),隔離(isolation),超時(timeout),唯讀(read-only)和回復(rollback)條件等,也可以設定交易管理器(transaction manager)
@Transactional的運行方式
假設我們以@Transactional標註callMethod()方法,Spring會在方法周圍(around)AOP機制處理交易需求:
createTransactionIfNecessary();  //若必要將產生交易
try {
    callMethod();
    commitTransactionAfterReturning();  //若正常結束則commit
} catch (exception) {
    rollbackTransactionAfterThrowing();  //若異常結束則rollback
    throw exception;
}
如何使用@Transactional
我們可以將@Transactional標註在介面(interfaces)、類別(classes)的宣告上,或是直接標註在方法(method)上。若彼此間衝突或不一致時,將以覆寫(override)的概念決定有效順位,原則是:
1.            標註在「類別方法」時高於「父類別方法」,又高於「介面方法」。
2.            標註在「類別宣告」時高於「父類別宣告」,又高於「介面宣告」。
3.            標註在「方法」時高於「類別宣告」或「介面宣告」。
@Transactional標註在類別的宣告上時,效果將自動擴充到類別內每一個沒有使用@Transactional標註的public方法;若privateprotected的方法上被開發者以@Transactional標註,也將自動被Spring框架忽略。
以下示範標註在介面宣告上:
@Transactional
public interface TransferService {
    void transfer(String user1, String user2, double val);
}
一般而言,不會建議在介面上直接使用類似@Transactional的功能性標註類別,因為介面的設計要考慮一般性,不見得每個實作都有需要使用資料庫交易;但如@Repository這種分類型標註類別就比較沒有這樣的限制,因為每個實作都還會是Repository類別。
一旦有介面或父類別被標註@Transactional,就可以在子類別上標註@Transactional以覆寫設定:
@Service
@Transactional
public class TransferServiceImpl implements TransferService {
    @Override
    public void transfer(String user1, String user2, double val) {
        // ...
    }
}
所以也可以在方法上直接標註@Transactional來覆寫類別上的標註:
@Transactional
public void transfer(String user1, String user2, double val) {
    // ...
}

2020年4月12日 星期日

SpringBoot的日誌(logging)機制


Java常見的幾種日誌(logging) API關係如下:




因為日誌API的實作不止一種,常見程式手法為依賴抽象層「slf4j」,再以設定的方式抽換實作。Spring Boot專案預設實作為「logback」,將在執行時期自動注入。
使用方式如:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller
public class HomeController {
    Logger logger = LoggerFactory.getLogger(HomeController.class);
    @RequestMapping(value = { "/", "/homepage" }, method = GET)
    public String home() {
        testLogLevel();
        return "home";
    }
    private void testLogLevel() {
        logger.trace("A TRACE Message");
        logger.debug("A DEBUG Message");
        logger.info("An INFO Message");
        logger.warn("A WARN Message");
        logger.error("An ERROR Message");
    }
}
相關日誌紀錄的設定(application.properties)為:
# Core properties(logging)
logging.level.com.rwd=debug
logging.file=D:/springBoot4War.log
#logging.pattern.console= %d{yyyy-MM-dd HH:mm:ss} - %logger{36} - %msg%n
#logging.pattern.file= %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%

2020年4月2日 星期四

微服務架構下的常見6種資料庫存取設計模式


微服務架構下的常見6種資料庫存取設計模式
「資料庫存取」在一個單體(monolithic)系統裡常因為系統規模逐漸擴大,表格逐漸增多,而變得愈來愈棘手:
1.      SQL在程式裡是字串不好追蹤也不好維護。
2.      查詢時連接(join)愈來愈多的表格,效能愈來愈差。
3.      交易時包含愈來愈多表格,效能不好,也容易出現死結(dead lock)
軟體的成敗常決定於資料管理,必須在正確時間提供正確數字給利益攸關的使用者;接下來分享微服務架構下的6種資料管理設計模式(pattern),讀者可以選擇適合自己專案的設計模式,模式間也會有互相結合的情況。

1.      服務擁有自己的資料庫(Database Per Service)設計模式
在這種模式下,每一個微服務都管理自己的資料庫。這也表示沒有其他微服務可以存取自己的資料庫,只能透過定義良好的微服務進行資料的通信或交換。
聽起來不難,但實際上要並不容易,因為通常商業邏輯必須依賴服務之間的資料來互相支援,這導致牽扯不清,像一棵大樹下的盤根錯節的樹根。
這個模式的成功取決於設計時商業邏輯邊界(context boundary)的定義;建構新專案或新服務比較簡單,重構龐大的舊單體系統則比較麻煩。其他常見的挑戰還包含跨服務的資料庫交易如何維持一致性等。
如果設計成功,好處是服務間的鬆耦合(loose coupling),因此可以讓服務重複使用,也可以將硬體資源如CPURAM等賦予真正需要的服務,甚至依據服務類別選擇資料庫種類。

2.      服務共享資料庫(Shared Database)設計模式
若前述「Database Per Service」有窒礙難性的地方,自然就只能採行「Shared Database」的設計模式。
這種模式的優點是使用方式對開發人員而言比較熟悉,等同於開發較小規模的單體式系統;對資料庫交易的ACID都與過去習慣做法一致,也相對保守、安全。缺點是比較用不到微服務的好處,多個微服務同時存取同一個資料庫,容易有資源競爭或資料鎖定的問題。
總體而言,這種方法弊大於利。

3.      API複合(API Composition)設計模式
這種模式常見於在微服務架構中「合併」多服務的查詢結果。
在這種模式下,API組件者(composer)會依照既定順序呼叫相關微服務,並在提供資料給使用者之前先在記憶體中串接(in-memory join)。缺點是相較於SQL join通常會取回多餘的資料,而且大型資料集合在記憶體中串接效率不彰因此影響效能。

4.      指令與查詢分離(Command Query Responsibility Segregation, CQRS)設計模式
對資料庫而言,寫入(write)與讀取(read)有很大的不同:
l  對系統的負載不同。
l  寫入比讀取要考慮的事務複雜,如交易一致性。
l  資訊安全考量也不同。
l  寫入時對象是「正規化」的資料表,讀取時卻經常是「反正規化」的資料,例如會將成用的查詢建立View物件。
微軟資料庫的快照(snapshot)解決方案就是基於COW (Copy On Write)的原則建置讀寫分離的架構。
















x

-資料庫的讀寫分離
CQRS也主張程式對資料庫的存取應該「讀寫分離」:
l   (write):執行後會改變物件狀態,在這裡使用「Command」描述。
l   (read):查看物件結果,而且不會改變物件的狀態、對物件本身沒有副作用,在這裡使用「Query」描述。
這個概念來自於物件導向的「命令與查詢分離(CQS, Command Query Separation)」,出自於1987Betrand MeyerObject-Oriented Software Construction(物件導向軟體建構)一書,其原始概念是我們可以把物件操作分為命令(Command)和查詢(Query)兩種形式。
CQRS可以看成解決「API複合」模式的問題的一種嘗試。因為在將資料寫入資料庫的同時,記憶體或其他地方中可以保留一份,查詢時就可以直接使用這份資料而不需要和經常被更新的資料競爭資源。概念為:
-服務層級的CQRS

5.      事件溯源(Event Sourcing)設計模式
這種模式經常和CQRS合併應用。負責寫入的微服務在改變資料狀態時將發布事件(event)並儲存資料於事件中,關聯的讀取微服務即可取得資料而不需要再查詢資料庫。這也是觀察者(Observer)設計模式的實現。
必須注意的是,這種模式必須確認只有預期的寫入微服務可以異動資料,否則將造成讀取服務取得的資料不一致。
這種模式的缺點是讀、寫服務需要的資料內容通常不同,因此服務寫入資料庫時保留的資料不見得直接適用於讀取服務,通常還需要整理。

6.      SAGA設計模式
SAGA模式用來解決跨服務的交易一致性需求。
SAGA本質上代表一連串的微服務區域(local)交易。在SAGA內進行交易的服務會發布交易事件,每一個微服務只在收到前一個服務的事件通知後才進行自己的交易內容;若有交易內容失敗SAGA會發動rollback以回復到交易前狀態,若全部成功則commit
舉一個送餐應用程序為例。當客戶嘗試訂購食物時,可能會發生以下步驟:
l  「訂單微服務」建立訂單物件。此時訂單處於「暫停(PENDING)」狀態,SAGA將管理後續一連串的事件。
l  SAGA藉由「餐廳微服務」聯絡被挑選的餐廳,餐廳藉由「餐廳微服務」回應是否接單。
l  SAGA取得「餐廳微服務」的回應,依據內容決定訂單物件狀態為「受理(APPROVED)」或「拒絕(REJECTED)」,並藉由「訂單微服務」回應客戶訂單明細或抱歉訊息。
如本例,當以微服務串接流程時會比一般點對點(point-to-point)的程式呼叫來的複雜,而SAGA就扮演居中協調的角色。

在接下來的文章中,我們將更進一步探討這些模式以及實現方式。