前言
事務一致性原理回顧
髒讀:事務 A 讀到了事務 B 還沒有提交的數據。
不可重複讀:在一個事務裏面對某個數據讀取了兩次,讀出來的數據不一致。
幻讀:在一個事務對某個數據集用同樣的方式讀取了兩次,數據集的條目數量不一致。
READ_UNCOMMITTED(讀未提交):最低的隔離級別,可以讀到未提交的數據,無法解決髒讀、不可重複讀、幻讀中的任何一種。
READ_COMMITED (讀已提交):能夠防止髒讀,但是無法解決不可重複讀和幻讀的問題。
REPEATABLE_READ (重複讀取):對同一條數據的多次重複讀取能保持一致,解決了髒讀、不可重複讀的問題,但是幻讀的問題還是無法解決。
SERLALIZABLE ( 串行化):最高的事務隔離級別,避免了事務的並行執行,解決了髒讀、不可重複讀和幻讀的問題,但性能最低。
搶購業務中的分佈式事務
分佈式事務的實現方式
傳統分佈式事務
提供了強一致性保證,在業務執行的任何時間點都能確保事務一致性。
使用簡單。常見的關係型數據庫都提供了對XA協議的支持,通過引入事務協調器,業務代碼跟使用單機事務相比基本上沒有差別。
柔性事務
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
事務消息原理分析
搶購業務場景拆解
引入消息異步通知機制
先執行本地事務,還是先發送異步消息?
如何確保遠程事務能執行成功?
消息隊列集羣在將異步消息投遞到遠程事務參與方的時候,由於網絡不穩定,消息沒能投遞成功。
消息投遞成功了,但遠程事務參與方還沒來得及執行遠程事務,就宕機了。
完整流程
事務消息實戰
消息隊列 RocketMQ
開通 RocketMQ 服務
創建資源
本地事務參與方的業務代碼
1、初始化 TransactionProducer
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
<version>1.8.7.2.Final</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.13.1</version>
</dependency>
TransactionProducer
,用於異步消息的發送,需要填入如下信息:
-
Group ID:之前創建的用於本地事務參與方的 Group ID。 -
Access key和Secret Key:RAM 用戶對應的密鑰信息,從 RAM 用戶控制檯獲得。 -
Nameserver Address:RocketMQ 實例的接入點信息,從 RocketMQ 控制檯獲得。
Properties properties = new Properties();
// 您在控制檯創建的Group ID。注意:事務消息的Group ID不能與其他類型消息的Group ID共用。
properties.put(PropertyKeyConst.GROUP_ID, "XXX");
// AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。
properties.put(PropertyKeyConst.AccessKey, "XXX");
// AccessKey Secret阿里雲身份驗證,在阿里雲RAM控制檯創建。
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 設置TCP接入域名,進入消息隊列RocketMQ版控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");
// LocalTransactionCheckerImpl本地事務回查類的實現
TransactionProducer producer = ONSFactory.createTransactionProducer(properties,
new LocalTransactionCheckerImpl());
producer.start();
2、獲取全局唯一的交易流水號
3、實現本地事務回查邏輯
LocalTransactionChecker
接口的LocalTransactionCheckerImpl
類,實現其中的check(Message)
方法,該方法返回本地事務的最終狀態。至於具體的業務邏輯如何實現,不在本文討論的範圍之前,我們將其封裝在BusinessService
類中。
package transaction;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.transaction.LocalTransactionChecker;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LocalTransactionCheckerImpl implements LocalTransactionChecker {
private static Logger LOGGER = LoggerFactory.getLogger(LocalTransactionCheckerImpl.class);
private static BusinessService businessService = new BusinessService();
public TransactionStatus check(Message msg) {
// 從消息體中獲得的交易ID
String transactionKey = msg.getKey();
TransactionStatus transactionStatus = TransactionStatus.Unknow;
try {
boolean isCommit = businessService.checkbusinessService(transactionKey);
if (isCommit) {
transactionStatus = TransactionStatus.CommitTransaction;
} else {
transactionStatus = TransactionStatus.RollbackTransaction;
}
} catch (Exception e) {
LOGGER.error("Transaction Key:{}", transactionKey, e);
}
LOGGER.warn("Transaction Key:{}transactionStatus:{}", transactionKey, transactionStatus.name());
return transactionStatus;
}
}
4、執行本地事務併發送異步消息
LocalTransactionExecuter
接口的匿名類,通過send
方法進行發送,這就是本地事務參與方所需要實現的所有業務代碼了。當然,這個匿名類實現了TransactionStatus execute.execute()
方法,其中包含了對於本地事務的執行。完整代碼如下:
package transaction;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.aliyun.openservices.ons.api.SendResult;
import com.aliyun.openservices.ons.api.transaction.TransactionProducer;
import com.aliyun.openservices.ons.api.transaction.TransactionStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class TransactionProducerClient {
private static Logger LOGGER = LoggerFactory.getLogger(TransactionProducerClient.class);
private static final BusinessService businessService = new BusinessService();
private static final String TOPIC = "create_order";
private static final TransactionProducer producer = null;
static {
Properties properties = new Properties();
// 您在控制檯創建的Group ID。注意:事務消息的Group ID不能與其他類型消息的Group ID共用。
properties.put(PropertyKeyConst.GROUP_ID, "XXX");
// AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。
properties.put(PropertyKeyConst.AccessKey, "XXX");
// AccessKey Secret阿里雲身份驗證,在阿里雲RAM控制檯創建。
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 設置TCP接入域名,進入消息隊列RocketMQ版控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");
// LocalTransactionCheckerImpl本地事務回查類的實現
TransactionProducer producer = ONSFactory.createTransactionProducer(properties,
new LocalTransactionCheckerImpl());
producer.start();
}
public static void main(String[] args) throws InterruptedException {
String transactionKey = getGlobalTransactionKey();
String messageContent = String.format("lock inventory for: %s", transactionKey);
Message message = new Message(TOPIC, null, transactionKey, messageContent.getBytes());
try {
SendResult sendResult = producer.send(message, (msg, arg) -> {
// 此處用Lambda表示,實際是實現TransactionStatus execute(final Message msg, final Object arg)方法
TransactionStatus transactionStatus = TransactionStatus.Unknow;
try {
boolean localTransactionOK = businessService.execbusinessService(transactionKey);
if (localTransactionOK) {
transactionStatus = TransactionStatus.CommitTransaction;
} else {
transactionStatus = TransactionStatus.RollbackTransaction;
}
} catch (Exception e) {
LOGGER.error("Transaction Key:{}", transactionKey, e);
}
LOGGER.warn("Transaction Key:{}", transactionKey);
return transactionStatus;
}, null);
LOGGER.info("send message OK, Transaction Key:{}, result:{}", message.getKey(), sendResult);
} catch (Exception e) {
LOGGER.info("send message failed, Transaction Key:{}", message.getKey());
}
// demo example防止進程退出
TimeUnit.MILLISECONDS.sleep(Integer.MAX_VALUE);
}
private static String getGlobalTransactionKey() {
// TODO
return "";
}
}
遠程事務參與方的業務代碼
package transaction;
import com.aliyun.openservices.ons.api.Action;
import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Properties;
public class TransactionConsumerClient {
private static Logger LOGGER = LoggerFactory.getLogger(TransactionProducerClient.class);
private static final BusinessService businessService = new BusinessService();
private static final String TOPIC = "create_order";
private static final Consumer consumer = null;
static {
Properties properties = new Properties();
// 在控制檯創建的Group ID,不同於本地事務參與方使用的Group ID
properties.put(PropertyKeyConst.GROUP_ID, "XXX");
// AccessKey ID阿里雲身份驗證,在阿里雲RAM控制檯創建。
properties.put(PropertyKeyConst.AccessKey, "XXX");
// Accesskey Secret阿里雲身份驗證,在阿里雲服RAM控制檯創建。
properties.put(PropertyKeyConst.SecretKey, "XXX");
// 設置TCP接入域名,進入控制檯的實例詳情頁面的TCP協議客戶端接入點區域查看。
properties.put(PropertyKeyConst.NAMESRV_ADDR, "XXX");
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.start();
}
public static void main(String[] args) {
consumer.subscribe(TOPIC, "*", (message, context) -> {
LOGGER.info("Receive: " + message);
businessService.doBusiness(message);
// 返回CommitMessage,代表給予消息隊列集羣異步消息已經得到正常處理的回饋
return Action.CommitMessage;
}
);
}
}
事務回滾
技術異常:遠程事務參與方宕機、網絡故障、數據庫故障等。
業務異常:遠程邏輯在業務上無法執行、代碼業務邏輯錯誤等。
多個事務參與方
有可能發生業務異常的:比如鎖定庫存的操作,有可能因爲庫存不足而執行失敗。又比如扣除積分的操作,有可能因爲用戶積分不足而無法扣除。
不太可能發生業務異常的:比如刪除購物車條目的操作,除非是技術類故障,一定可以執行成功,即便對應的條目並不存在,也沒有關係。又比如積分增加的操作,只要對應的用戶沒有註銷,是不可能遇到業務異常的。
其他注意事項
消息冪等
每日對賬
1、消息重試多次後,依然不成功:當消費者完全無法正常工作的時候,RocketMQ 不可能永無止境地重試消息,事實上,如果16次重試後異步消息依然沒有辦法被正常處理,RocketMQ 會停止嘗試,將消息放到一個特殊的隊列中。
2、未處理的業務異常:比如給某個賬號加積分的時候,發現此賬號被註銷了,這是一個非常罕見的業務現象,有可能事先對此並沒有健壯的處理機制。
3、冪等校驗失敗:處理冪等所依賴的系統比如 Redis 發生了故障,導致某些消息被重複處理。
4、其他嚴重的系統故障:比如網絡長時間中斷,留下了大量執行到一半的事務。
5、其他漏網之魚。
總結
乾貨分享
最近將個人學習筆記整理成冊,使用PDF分享。關注我,回覆如下代碼,即可獲得百度盤地址,無套路領取!
•001:《Java併發與高併發解決方案》學習筆記;•002:《深入JVM內核——原理、診斷與優化》學習筆記;•003:《Java面試寶典》•004:《Docker開源書》•005:《Kubernetes開源書》•006:《DDD速成(領域驅動設計速成)》•007:全部•008:加技術羣討論
近期熱文
•LinkedBlockingQueue vs ConcurrentLinkedQueue•解讀Java 8 中爲併發而生的 ConcurrentHashMap•Redis性能監控指標彙總•最全的DevOps工具集合,再也不怕選型了!•微服務架構下,解決數據庫跨庫查詢的一些思路•聊聊大廠面試官必問的 MySQL 鎖機制
關注我
喜歡就點個"在看"唄^_^
本文分享自微信公衆號 - IT牧場(itmuch_com)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。