lms框架的分散式事務解決方案採用的TCC事務模型。在開發過程中參考和借鑑了hmily。使用AOP的程式設計思想,在rpc通訊過程中通過攔截器的方式對全域性事務或是分支事務進行管理和協調。
本文通過lms.samples 訂單介面給大家介紹lms框架分散式事務的基本使用。
lms分散式事務的使用
在lms框架中,在應用服務介面通過[Transaction]
特性標識該介面是一個分散式事務介面(應用介面層需要安裝包Silky.Lms.Transaction
)。應用服務介面的實現必須需要通過 [TccTransaction(ConfirmMethod = "ConfirmMethod", CancelMethod = "CancelMethod")]
特性指定Confirm階段和Cancel階段的方法(需要再應用層安裝包Silky.Lms.Transaction.Tcc
)。
warning 注意
一個應用介面被分散式事務[Transaction]
特性標識,那麼這個應用介面的實現也必須要使用TccTransaction
特性來標識。否則,應用在啟動時會丟擲異常。
在一個分散式事務處理過程中,會存在如下兩種角色的事務。
事務角色
- 全域性事務
在Lms框架中,第一個執行的事務被認為是全域性事務(事務角色為TransactionRole.Start
)。換句話說,在一個業務處理過程中,執行的第一個被標識為TccTransaction
(應用介面需要被標識為Transaction
)的方法為全域性事務。
當然,全域性事務也作為事務的一特殊的事務參與者,在全域性事務開始後,作為事務參與者註冊到事務上下文中。
- 分支事務
在開始的一個分散式事務中,參與rpc通訊,且被特性[Transaction]
標識的應用服務,被認為是分支事務(事務角色為:TransactionRole.Participant
)。
事務的執行
-
在開啟一個全域性事務之後,在全域性事務的
try
過程中,首先將全域性事務作為一個事務參與者新增到事務上下文中。如果遇到一個分支事務,那麼首先會呼叫分支事務的try
方法。如果try
方法執行成功,那麼分支事務作為一個事務參與者被註冊到事務上下文中,並且分支的事務狀態為變更為trying
。 -
如果在全域性事務的try方法執行過程中發生異常,那麼全域性事務的
Cancel
方法和被加入事務上下文且狀態為trying
的分支事務參與者的Cancel
方法將會被呼叫,在Cancel
方法中實現資料回滾。也就是說,全域性事務的Cancel
不管try
方法是否執行成功,全域性事務的Cancel
方法都會被執行。分支事務只有被加入到事務上下文,且狀態為trying
(分支事務已經執行過try
方法),那麼分支事務的Cancel
方法才會被執行。 -
全域性事務的try方法執行成功,那麼全域性事務的
Confirm
和各個分支事務的Confirm
方法將會得到執行。 -
換句話說,所有全域性事務(事務主分支)以及分支事務的try方法都執行成功,才會依次執行所有事務參與者的
Confirm
方法,如果分散式事務的try
階段執行失敗,那麼主分支事務的Cancel
方法一定會被呼叫;而分支事務看是否有被新增到事務上下文中且已經執行成功try
階段的方法,只有這樣的分支事務才會呼叫Cancel
方法。 -
如果分支事務存在分支事務的情況下,這種業務場景會相對特殊,這個時候的分支事務相對於它的分支事務就是一個特殊的全域性事務。它會在特殊的
try
階段執行孫子輩的分支事務的try
和confirm
(成功)或是try
和cancel
(失敗)。並且會將執行成功與否返回給父分支事務(全域性事務)。
warning 注意
無論是全域性事務還是分支事務的各個階段,如果涉及到多個表的操作,那麼,對應的資料庫操作的都需要放到本地事務進行操作。
分散式事務案例-- lms.samples訂單介面
下面,我們通過lms.samples的訂單介面來熟悉通過lms框架如何實現分散式事務。
lms.samples 訂單介面的業務流程介紹
在上一篇博文通過lms.samples熟悉lms微服務框架的使用,給大家介紹了lms.samples樣例專案的基本情況。本文通過大家熟悉的一個訂單介面,熟悉lms的分散式事務是如何使用。
下面,給大家梳理一下訂單介面的業務流程。
-
判斷和鎖定訂單產品庫存: 在下訂單之前需要判斷是否存在相應的產品,產品的剩餘數量是否足夠,如果產品數量足夠的話,扣減產品庫存,鎖定訂單的庫存數量(分支事務)
-
建立一個訂單記錄,訂單狀態為NoPay(全域性事務)
-
判斷使用者的賬號是否存在,賬戶餘額是否充足,如果賬戶餘額充足的話,則需要鎖定訂單金額,建立一個賬戶流水記錄。
-
如果1,2,3都成功,釋放產品鎖定的訂單庫存
-
如果1,2,3都成功,釋放賬號鎖定的金額,修改賬號流水記錄相關狀態
-
如果1,2,3都成功,修改訂單狀態為Payed
-
如果在步驟1就出現異常(例如:產品的庫存不足或是rpc通訊失敗,或是訪問資料庫出現異常等),庫存分支事務(
DeductStockCancel
)和賬號分支事務(DeductBalanceCancel
)指定的Cancel
方法都不會被執行。但是全域性事務指定的Cancel
方法(OrderCreateCancel
)會被呼叫 -
如果在步驟2就出現異常(下訂單訪問資料庫出現異常),庫存分支事務指定的
Cancel
方法(DeductStockCancel
)以及全域性事務指定的Cancel
方法(OrderCreateCancel
)會被呼叫,賬號分支事務指定(DeductBalanceCancel
)的Cancel
方法都不會被執行。 -
如果在步驟3就出現異常(使用者的賬號餘額不足,訪問資料庫出現異常等),那麼庫存分支事務(
DeductStockCancel
)和賬號分支事務指定(DeductBalanceCancel
)全域性事務指定的Cancel
方法(OrderCreateCancel
)都會被呼叫。
tip 提示
- 如果在一個分散式事務處理失敗,全域性事務的
Cancel
方法一定會被呼叫。分支事務的Try
方法得到執行(分支事務的狀態為trying
),那麼將會執行分支事務指定的Cancel
方法。如果分支事務的分支事務的Try
方法沒有得到執行(分支事務的狀態為pretry
),那麼不會執行分支事務指定的Cancel
方法。- 上述的業務流程過程中,步驟1,2,3為
try
階段,步驟4,5,6為confirm
階段,步驟7,8,9為concel
階段。
全域性事務--訂單介面
通過lms分散式事務的使用節點的介紹,我們知道在服務之間的rpc通訊呼叫中,執行的第一個被標識為Transaction
的應用方法即為全域性事務(即:事務的開始)。
首先, 我們需要在訂單應用介面中通過[Transaction]
來標識這是一個分散式事務的應用介面。
[Transaction]
Task<GetOrderOutput> Create(CreateOrderInput input);
其次,在應用介面的實現通過[TccTransaction]
特性指定ConfirmMethod
方法和CancelMethod
。
- 指定的
ConfirmMethod
和CancelMethod
必須為public
型別,但是不需要在應用介面中宣告。 - 全域性事務的
ConfirmMethod
和CancelMethod
必定有一個會被執行,如果try方法(Create
)執行成功,那麼執行ConfirmMethod
方法,執行失敗,那麼則會執行CancelMethod
。 - 可以將
try
、confirm
、cancel
階段的方法放到領域服務中實現。 - 全域性事務可以通過
RpcContext
的Attachments
向分支事務或是confirm
、cancel
階段的方法傳遞Attachment引數。但是分支事務不能夠通過RpcContext
的Attachments
向全域性事務傳遞Attachment引數。
/// <summary>
/// try階段的方法
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[TccTransaction(ConfirmMethod = "OrderCreateConfirm", CancelMethod = "OrderCreateCancel")]
public async Task<GetOrderOutput> Create(CreateOrderInput input)
{
return await _orderDomainService.Create(input); //具體的業務放到領域層實現
}
// confirm階段的方法
public async Task<GetOrderOutput> OrderCreateConfirm(CreateOrderInput input)
{
var orderId = RpcContext.GetContext().GetAttachment("orderId");
var order = await _orderDomainService.GetById(orderId.To<long>());
order.Status = OrderStatus.Payed;
order = await _orderDomainService.Update(order);
return order.MapTo<GetOrderOutput>();
}
// cancel階段的方法
public async Task OrderCreateCancel(CreateOrderInput input)
{
var orderId = RpcContext.GetContext().GetAttachment("orderId");
// 如果不為空證明已經建立了訂單
if (orderId != null)
{
// 是否保留訂單可以根據具體的業務來確定。
// await _orderDomainService.Delete(orderId.To<long>());
var order = await _orderDomainService.GetById(orderId.To<long>());
order.Status = OrderStatus.UnPay;
await _orderDomainService.Update(order);
}
}
下訂單的具體業務(訂單try階段的實現)
public async Task<GetOrderOutput> Create(CreateOrderInput input)
{
// 扣減庫存
var product = await _productAppService.DeductStock(new DeductStockInput()
{
Quantity = input.Quantity,
ProductId = input.ProductId
}); // rpc呼叫,DeductStock被特性[Transaction]標記,是一個分支事務
// 建立訂單
var order = input.MapTo<Domain.Orders.Order>();
order.Amount = product.UnitPrice * input.Quantity;
order = await Create(order);
RpcContext.GetContext().SetAttachment("orderId", order.Id); //分支事務或是主分支事務的confirm或是cancel階段可以從RpcContext獲取到Attachment引數。
//扣減賬戶餘額
var deductBalanceInput = new DeductBalanceInput()
{OrderId = order.Id, AccountId = input.AccountId, OrderBalance = order.Amount};
var orderBalanceId = await _accountAppService.DeductBalance(deductBalanceInput); // rpc呼叫,DeductStock被特性[Transaction]標記,是一個分支事務
if (orderBalanceId.HasValue)
{
RpcContext.GetContext().SetAttachment("orderBalanceId", orderBalanceId.Value);//分支事務或是主分支事務的confirm或是cancel階段可以從RpcContext獲取到Attachment引數。
}
return order.MapTo<GetOrderOutput>();
}
分支事務--扣減庫存
首先,需要在應用介面層標識這個是一個分散式事務介面。
// 標識這個是一個分散式事務介面
[Transaction]
// 執行成功,清除快取資料
[RemoveCachingIntercept("GetProductOutput","Product:Id:{0}")]
// 該介面不對叢集外部發布
[Governance(ProhibitExtranet = true)]
Task<GetProductOutput> DeductStock(DeductStockInput input);
其次,應用介面的實現指定Confirm
階段和Cancel
階段的方法。
[TccTransaction(ConfirmMethod = "DeductStockConfirm", CancelMethod = "DeductStockCancel")]
public async Task<GetProductOutput> DeductStock(DeductStockInput input)
{
var product = await _productDomainService.GetById(input.ProductId);
if (input.Quantity > product.Stock)
{
throw new BusinessException("訂單數量超過庫存數量,無法完成訂單");
}
product.LockStock += input.Quantity;
product.Stock -= input.Quantity;
product = await _productDomainService.Update(product);
return product.MapTo<GetProductOutput>();
}
public async Task<GetProductOutput> DeductStockConfirm(DeductStockInput input)
{
//Confirm階段的具體業務放在領域層實現
var product = await _productDomainService.DeductStockConfirm(input);
return product.MapTo<GetProductOutput>();
}
public Task DeductStockCancel(DeductStockInput input)
{
//Cancel階段的具體業務放在領域層實現
return _productDomainService.DeductStockCancel(input);
}
分支事務--扣減賬戶餘額
首先,需要在應用介面層標識這個是一個分散式事務介面。
[Governance(ProhibitExtranet = true)]
[RemoveCachingIntercept("GetAccountOutput","Account:Id:{0}")]
[Transaction]
Task<long?> DeductBalance(DeductBalanceInput input);
其次,應用介面的實現指定Confirm
階段和Cancel
階段的方法。
[TccTransaction(ConfirmMethod = "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")]
public async Task<long?> DeductBalance(DeductBalanceInput input)
{
var account = await _accountDomainService.GetAccountById(input.AccountId);
if (input.OrderBalance > account.Balance)
{
throw new BusinessException("賬號餘額不足");
}
return await _accountDomainService.DeductBalance(input, TccMethodType.Try);
}
public Task DeductBalanceConfirm(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Confirm);
}
public Task DeductBalanceCancel(DeductBalanceInput input)
{
return _accountDomainService.DeductBalance(input, TccMethodType.Cancel);
}
第三, 領域層的業務實現
public async Task<long?> DeductBalance(DeductBalanceInput input, TccMethodType tccMethodType)
{
var account = await GetAccountById(input.AccountId);
//涉及多張表,所有每一個階段的都放到一個本地事務中執行
var trans = await _repository.BeginTransactionAsync();
BalanceRecord balanceRecord = null;
switch (tccMethodType)
{
case TccMethodType.Try:
account.Balance -= input.OrderBalance;
account.LockBalance += input.OrderBalance;
balanceRecord = new BalanceRecord()
{
OrderBalance = input.OrderBalance,
OrderId = input.OrderId,
PayStatus = PayStatus.NoPay
};
await _repository.InsertAsync(balanceRecord);
RpcContext.GetContext().SetAttachment("balanceRecordId",balanceRecord.Id);
break;
case TccMethodType.Confirm:
account.LockBalance -= input.OrderBalance;
var balanceRecordId1 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId1.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId1.Value);
balanceRecord.PayStatus = PayStatus.Payed;
await _repository.UpdateAsync(balanceRecord);
}
break;
case TccMethodType.Cancel:
account.Balance += input.OrderBalance;
account.LockBalance -= input.OrderBalance;
var balanceRecordId2 = RpcContext.GetContext().GetAttachment("orderBalanceId")?.To<long>();
if (balanceRecordId2.HasValue)
{
balanceRecord = await _repository.GetByIdAsync<BalanceRecord>(balanceRecordId2.Value);
balanceRecord.PayStatus = PayStatus.Cancel;
await _repository.UpdateAsync(balanceRecord);
}
break;
}
await _repository.UpdateAsync(account);
await trans.CommitAsync();
// 將受影響的快取資料移除。
await _accountCache.RemoveAsync($"Account:Name:{account.Name}");
return balanceRecord?.Id;
}
訂單介面測試
前提
存在如下賬號和產品:
模擬庫存不足
請求引數:
{
"accountId": 1,
"productId": 1,
"quantity": 11
}
響應:
{
"data": null,
"status": 1000,
"statusCode": "BusinessError",
"errorMessage": "訂單數量超過庫存數量,無法完成訂單",
"validErrors": null
}
資料庫變化
檢視資料庫,並沒有生成訂單資訊,賬戶餘額和產品庫存也沒有修改:
測試結果:
庫存和賬戶餘額均為變化,也未建立訂單資訊
達到期望
模擬賬號餘額不足
請求引數:
{
"accountId": 1,
"productId": 1,
"quantity": 9
}
響應:
{
"data": null,
"status": 1000,
"statusCode": "BusinessError",
"errorMessage": "賬號餘額不足",
"validErrors": null
}
資料庫變化
-
新增了一個產品訂單,訂單狀態為未支付狀態
-
產品庫存和賬戶餘額並未變更
測試結果:
建立了一個新的訂單,狀態為未支付,使用者賬號餘額,產品訂單均未變化。
達到測試期望
正常下訂單
{
"accountId": 1,
"productId": 1,
"quantity": 2
}
響應:
{
"data": {
"id": 2,
"accountId": 1,
"productId": 1,
"quantity": 2,
"amount": 20,
"status": 1
},
"status": 200,
"statusCode": "Success",
"errorMessage": null,
"validErrors": null
}
資料庫變化
- 建立了一個訂單,該訂單狀態為已支付
- 庫存扣減成功
- 賬戶金額扣減成功,並且建立了一個流水記錄
測試結果:
建立了一個新的訂單,狀態為支付,使用者賬號餘額,產品訂單均被扣減,且也建立了交易流水記錄。
達到期望結果。
開源地址
github: https://github.com/liuhll/lms