根據(jù)微服務(wù)架構(gòu)的鼻祖 Martin Fowler 的忠告撤缴,微服務(wù)架構(gòu)中應(yīng)當(dāng)盡量避免分布式事務(wù)。
分布式事務(wù)的討論主要聚焦于強(qiáng)一致性和最終一致性的解決方案叽唱。
微服務(wù)的發(fā)展
微服務(wù)倡導(dǎo)將復(fù)雜的單體應(yīng)用拆分為若干個(gè)功能簡(jiǎn)單腹泌、松耦合的服務(wù),這樣可以降低開(kāi)發(fā)難度尔觉、增強(qiáng)擴(kuò)展性、便于敏捷開(kāi)發(fā)芥吟。當(dāng)前被越來(lái)越多的開(kāi)發(fā)者推崇侦铜,很多互聯(lián)網(wǎng)行業(yè)巨頭、開(kāi)源社區(qū)等都開(kāi)始了微服務(wù)的討論和實(shí)踐钟鸵。
微服務(wù)落地存在的問(wèn)題
雖然微服務(wù)現(xiàn)在如火如荼钉稍,但對(duì)其實(shí)踐其實(shí)仍處于探索階段。很多中小型互聯(lián)網(wǎng)公司棺耍,鑒于經(jīng)驗(yàn)贡未、技術(shù)實(shí)力等問(wèn)題,微服務(wù)落地比較困難。
如著名架構(gòu)師Chris Richardson所言俊卤,目前存在的主要困難有如下幾方面:
- 單體應(yīng)用拆分為分布式系統(tǒng)后嫩挤,進(jìn)程間的通訊機(jī)制和故障處理措施變的更加復(fù)雜。
- 系統(tǒng)微服務(wù)化后消恍,一個(gè)看似簡(jiǎn)單的功能岂昭,內(nèi)部可能需要調(diào)用多個(gè)服務(wù)并操作多個(gè)數(shù)據(jù)庫(kù)實(shí)現(xiàn),服務(wù)調(diào)用的分布式事務(wù)問(wèn)題變的非常突出狠怨。
- 微服務(wù)數(shù)量眾多约啊,其測(cè)試、部署佣赖、監(jiān)控等都變的更加困難恰矩。
隨著RPC框架的成熟,第一個(gè)問(wèn)題已經(jīng)逐漸得到解決憎蛤。例如springcloud可以非常好的支持restful調(diào)用外傅,dubbo可以支持多種通訊協(xié)議。
對(duì)于第三個(gè)問(wèn)題蹂午,隨著docker栏豺、devops技術(shù)的發(fā)展以及各公有云paas平臺(tái)自動(dòng)化運(yùn)維工具的推出,微服務(wù)的測(cè)試豆胸、部署與運(yùn)維會(huì)變得越來(lái)越容易奥洼。
而對(duì)于第二個(gè)問(wèn)題,現(xiàn)在還沒(méi)有通用方案很好的解決微服務(wù)產(chǎn)生的事務(wù)問(wèn)題晚胡。分布式事務(wù)已經(jīng)成為微服務(wù)落地最大的阻礙灵奖,也是最具挑戰(zhàn)性的一個(gè)技術(shù)難題。
ACID
原子性(Atomicity): 一個(gè)事務(wù)的所有系列操作步驟被看成是一個(gè)動(dòng)作估盘,所有的步驟要么全部完成要么一個(gè)也不會(huì)完成瓷患,如果事務(wù)過(guò)程中任何一點(diǎn)失敗,將要被改變的數(shù)據(jù)庫(kù)記錄就不會(huì)被真正被改變遣妥。
一致性(Consistency): 數(shù)據(jù)庫(kù)的約束 級(jí)聯(lián)和觸發(fā)機(jī)制Trigger都必須滿(mǎn)足事務(wù)的一致性擅编。也就是說(shuō),通過(guò)各種途徑包括外鍵約束等任何寫(xiě)入數(shù)據(jù)庫(kù)的數(shù)據(jù)都是有效的箫踩,不能發(fā)生表與表之間存在外鍵約束爱态,但是有數(shù)據(jù)卻違背這種約束性。所有改變數(shù)據(jù)庫(kù)數(shù)據(jù)的動(dòng)作事務(wù)必須完成境钟,沒(méi)有事務(wù)會(huì)創(chuàng)建一個(gè)無(wú)效數(shù)據(jù)狀態(tài)锦担,這是不同于CAP理論的一致性"consistency".
隔離性(Isolation): 主要用于實(shí)現(xiàn)并發(fā)控制, 隔離能夠確保并發(fā)執(zhí)行的事務(wù)能夠順序一個(gè)接一個(gè)執(zhí)行,通過(guò)隔離慨削,一個(gè)未完成事務(wù)不會(huì)影響另外一個(gè)未完成事務(wù)洞渔。
持久性(Durability): 一旦一個(gè)事務(wù)被提交套媚,它應(yīng)該持久保存,不會(huì)因?yàn)楹推渌僮鳑_突而取消這個(gè)事務(wù)磁椒。很多人認(rèn)為這意味著事務(wù)是持久在磁盤(pán)上堤瘤,但是規(guī)范沒(méi)有特別定義這點(diǎn)。
一致性理論
分布式事務(wù)的目的是保障分庫(kù)數(shù)據(jù)一致性衷快,而跨庫(kù)事務(wù)會(huì)遇到各種不可控制的問(wèn)題宙橱,如個(gè)別節(jié)點(diǎn)永久性宕機(jī),像單機(jī)事務(wù)一樣的 ACID 是無(wú)法奢望的蘸拔。
另外师郑,業(yè)界著名的 CAP 理論也告訴我們,對(duì)分布式系統(tǒng)调窍,需要將數(shù)據(jù)一致性和系統(tǒng)可用性宝冕、分區(qū)容忍性放在天平上一起考慮。
兩階段提交協(xié)議(簡(jiǎn)稱(chēng)2PC)是實(shí)現(xiàn)分布式事務(wù)較為經(jīng)典的方案邓萨,但 2PC 的可擴(kuò)展性很差地梨,在分布式架構(gòu)下應(yīng)用代價(jià)較大,eBay 架構(gòu)師 Dan Pritchett 提出了 BASE 理論缔恳,用于解決大規(guī)模分布式系統(tǒng)下的數(shù)據(jù)一致性問(wèn)題宝剖。
BASE 理論告訴我們:可以通過(guò)放棄系統(tǒng)在每個(gè)時(shí)刻的強(qiáng)一致性來(lái)?yè)Q取系統(tǒng)的可擴(kuò)展性。
CAP 理論
在分布式系統(tǒng)中歉甚,一致性(Consistency)万细、可用性(Availability)和分區(qū)容忍性(Partition Tolerance)3 個(gè)要素最多只能同時(shí)滿(mǎn)足兩個(gè),不可兼得纸泄。其中赖钞,分區(qū)容忍性又是不可或缺的。
- 一致性:分布式環(huán)境下聘裁,多個(gè)節(jié)點(diǎn)的數(shù)據(jù)是否強(qiáng)一致雪营。
- 可用性:分布式服務(wù)能一直保證可用狀態(tài)。當(dāng)用戶(hù)發(fā)出一個(gè)請(qǐng)求后衡便,服務(wù)能在有限時(shí)間內(nèi)返回結(jié)果献起。
- 分區(qū)容忍性:特指對(duì)網(wǎng)絡(luò)分區(qū)的容忍性。
舉例:Cassandra镣陕、Dynamo 等征唬,默認(rèn)優(yōu)先選擇 AP,弱化 C;HBase茁彭、MongoDB 等,默認(rèn)優(yōu)先選擇 CP扶歪,弱化 A理肺。
BASE 理論
核心思想:
- 基本可用(Basically Available):指分布式系統(tǒng)在出現(xiàn)故障時(shí)摄闸,允許損失部分的可用性來(lái)保證核心可用;
- 軟狀態(tài)(Soft state):指允許分布式系統(tǒng)存在中間狀態(tài)妹萨,該中間狀態(tài)不會(huì)影響到系統(tǒng)的整體可用性年枕;
- 最終一致性(Eventual consistency):指分布式系統(tǒng)中的所有副本數(shù)據(jù)經(jīng)過(guò)一定時(shí)間后,最終能夠達(dá)到一致的狀態(tài)乎完;
- 原子性(A)與持久性(D)必須根本保障;
- 為了可用性、性能與降級(jí)服務(wù)的需要肯夏,只有降低一致性( C ) 與 隔離性( I ) 的要求闺兢;
- 酸堿平衡(ACID-BASE Balance);
BASE 是對(duì) CAP 中 AP 的一個(gè)擴(kuò)展
一致性模型
數(shù)據(jù)的一致性模型可以分成以下三類(lèi):
- 強(qiáng)一致性:數(shù)據(jù)更新成功后帽揪,任意時(shí)刻所有副本中的數(shù)據(jù)都是一致的硝清,一般采用同步的方式實(shí)現(xiàn)。
- 弱一致性:數(shù)據(jù)更新成功后转晰,系統(tǒng)不承諾立即可以讀到最新寫(xiě)入的值芦拿,也不承諾具體多久之后可以讀到。
- 最終一致性:弱一致性的一種形式查邢,數(shù)據(jù)更新成功后蔗崎,系統(tǒng)不承諾立即可以返回最新寫(xiě)入的值,但是保證最終會(huì)返回上一次更新操作的值扰藕。
分布式系統(tǒng)數(shù)據(jù)的強(qiáng)一致性缓苛、弱一致性和最終一致性可以通過(guò) Quorum NRW 算法分析。
本地事務(wù)
- 在單個(gè)數(shù)據(jù)庫(kù)的本地并且限制在單個(gè)進(jìn)程內(nèi)的事務(wù)
- 本地事務(wù)不涉及多個(gè)數(shù)據(jù)來(lái)源
分布式事務(wù)典型方案
- 兩階段提交(2PC, Two Phase Commit)方案实胸;
- 本地消息表 (eBay 事件隊(duì)列方案)他嫡;
- TCC 補(bǔ)償模式;
分類(lèi):
- 兩階段型
- 補(bǔ)償型
- 異步確保型
- 最大努力通知型
服務(wù)模式:
- 可查詢(xún)操作
- 冪等操作
- TCC操作
- 可補(bǔ)償操作
兩階段提交2PC(強(qiáng)一致性)
基于XA協(xié)議的兩階段提交:
- 第一階段是表決階段庐完,所有參與者都將本事務(wù)能否成功的信息反饋發(fā)給協(xié)調(diào)者钢属;
- 第二階段是執(zhí)行階段,協(xié)調(diào)者根據(jù)所有參與者的反饋门躯,通知所有參與者淆党,步調(diào)一致地在所有分支上提交或者回滾;
缺點(diǎn):
- 單點(diǎn)問(wèn)題:事務(wù)管理器在整個(gè)流程中扮演的角色很關(guān)鍵,如果其宕機(jī)讶凉,比如在第一階段已經(jīng)完成染乌,在第二階段正準(zhǔn)備提交的時(shí)候事務(wù)管理器宕機(jī),資源管理器就會(huì)一直阻塞懂讯,導(dǎo)致數(shù)據(jù)庫(kù)無(wú)法使用荷憋。
- 同步阻塞:在準(zhǔn)備就緒之后,資源管理器中的資源一直處于阻塞褐望,直到提交完成勒庄,釋放資源串前。
- 數(shù)據(jù)不一致:兩階段提交協(xié)議雖然為分布式數(shù)據(jù)強(qiáng)一致性所設(shè)計(jì),但仍然存在數(shù)據(jù)不一致性的可能实蔽。比如:在第二階段中荡碾,假設(shè)協(xié)調(diào)者發(fā)出了事務(wù) Commit 的通知,但是因?yàn)榫W(wǎng)絡(luò)問(wèn)題該通知僅被一部分參與者所收到并執(zhí)行了 Commit 操作局装,其余的參與者則因?yàn)闆](méi)有收到通知一直處于阻塞狀態(tài)坛吁,這時(shí)候就產(chǎn)生了數(shù)據(jù)的不一致性。
總的來(lái)說(shuō)铐尚,XA 協(xié)議比較簡(jiǎn)單拨脉,成本較低,但是其單點(diǎn)問(wèn)題塑径,以及不能支持高并發(fā)(由于同步阻塞)依然是其最大的弱點(diǎn)女坑。
本地消息表(最終一致性)
eBay 的架構(gòu)師 Dan Pritchett,曾在一篇解釋 BASE 原理的論文《Base:An Acid Alternative》中提到一個(gè) eBay 分布式系統(tǒng)一致性問(wèn)題的解決方案统舀。
它的核心思想是將需要分布式處理的任務(wù)通過(guò)消息或者日志的方式來(lái)異步執(zhí)行匆骗,消息或日志可以存到本地文件、數(shù)據(jù)庫(kù)或消息隊(duì)列誉简,再通過(guò)業(yè)務(wù)規(guī)則進(jìn)行失敗重試碉就,它要求各服務(wù)的接口是冪等的。
本地消息表與業(yè)務(wù)數(shù)據(jù)表處于同一個(gè)數(shù)據(jù)庫(kù)中闷串,這樣就能利用本地事務(wù)來(lái)保證在對(duì)這兩個(gè)表的操作滿(mǎn)足事務(wù)特性瓮钥,并且使用了消息隊(duì)列來(lái)保證最終一致性。
- 在分布式事務(wù)操作的一方完成寫(xiě)業(yè)務(wù)數(shù)據(jù)的操作之后向本地消息表發(fā)送一個(gè)消息烹吵,本地事務(wù)能保證這個(gè)消息一定會(huì)被寫(xiě)入本地消息表中碉熄;
- 之后將本地消息表中的消息轉(zhuǎn)發(fā)到 Kafka 等消息隊(duì)列中,如果轉(zhuǎn)發(fā)成功則將消息從本地消息表中刪除肋拔,否則繼續(xù)重新轉(zhuǎn)發(fā)锈津;
- 消息消費(fèi)方處理這個(gè)消息,并完成自己的業(yè)務(wù)邏輯凉蜂。此時(shí)如果本地事務(wù)處理成功琼梆,表明已經(jīng)處理成功了,如果處理失敗窿吩,那么就會(huì)重試執(zhí)行茎杂。如果是業(yè)務(wù)上面的失敗,可以給生產(chǎn)方發(fā)送一個(gè)業(yè)務(wù)補(bǔ)償消息纫雁,通知生產(chǎn)方進(jìn)行回滾等操作煌往;
優(yōu)點(diǎn): 一種非常經(jīng)典的實(shí)現(xiàn),避免了分布式事務(wù)轧邪,實(shí)現(xiàn)了最終一致性刽脖。
缺點(diǎn): 消息表會(huì)耦合到業(yè)務(wù)系統(tǒng)中悼粮,如果沒(méi)有封裝好的解決方案,會(huì)有很多雜活需要處理曾棕。
這個(gè)方案的核心在于第二階段的重試和冪等執(zhí)行。失敗后重試菜循,這是一種補(bǔ)償機(jī)制翘地,它是能保證系統(tǒng)最終一致的關(guān)鍵流程。
可靠消息的最終一致性代碼示例
表結(jié)構(gòu)
DROP TABLE IF EXISTS `rp_transaction_message`;
CREATE TABLE `rp_transaction_message` (
`id` VARCHAR (50) NOT NULL DEFAULT '' COMMENT '主鍵ID',
`version` INT (11) NOT NULL DEFAULT '0' COMMENT '版本號(hào)',
`editor` VARCHAR (100) DEFAULT NULL COMMENT '修改者',
`creater` VARCHAR (100) DEFAULT NULL COMMENT '創(chuàng)建者',
`edit_time` datetime DEFAULT NULL COMMENT '最后修改時(shí)間',
`create_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '創(chuàng)建時(shí)間',
`message_id` VARCHAR (50) NOT NULL DEFAULT '' COMMENT '消息ID',
`message_body` LONGTEXT NOT NULL COMMENT '消息內(nèi)容',
`message_data_type` VARCHAR (50) DEFAULT NULL COMMENT '消息數(shù)據(jù)類(lèi)型',
`consumer_queue` VARCHAR (100) NOT NULL DEFAULT '' COMMENT '消費(fèi)隊(duì)列',
`message_send_times` SMALLINT (6) NOT NULL DEFAULT '0' COMMENT '消息重發(fā)次數(shù)',
`areadly_dead` VARCHAR (20) NOT NULL DEFAULT '' COMMENT '是否死亡',
`status` VARCHAR (20) NOT NULL DEFAULT '' COMMENT '狀態(tài)',
`remark` VARCHAR (200) DEFAULT NULL COMMENT '備注',
`field1` VARCHAR (200) DEFAULT NULL COMMENT '擴(kuò)展字段1',
`field2` VARCHAR (200) DEFAULT NULL COMMENT '擴(kuò)展字段2',
`field3` VARCHAR (200) DEFAULT NULL COMMENT '擴(kuò)展字段3',
PRIMARY KEY (`id`),
KEY `AK_Key_2` (`message_id`)
) ENGINE = INNODB DEFAULT CHARSET = utf8;
public interface RpTransactionMessageService {
/**
* 預(yù)存儲(chǔ)消息.
*/
public int saveMessageWaitingConfirm(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 確認(rèn)并發(fā)送消息.
*/
public void confirmAndSendMessage(String messageId) throws MessageBizException;
/**
* 存儲(chǔ)并發(fā)送消息.
*/
public int saveAndSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 直接發(fā)送消息.
*/
public void directSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 重發(fā)消息.
*/
public void reSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 根據(jù)messageId重發(fā)某條消息.
*/
public void reSendMessageByMessageId(String messageId) throws MessageBizException;
/**
* 將消息標(biāo)記為死亡消息.
*/
public void setMessageToAreadlyDead(String messageId) throws MessageBizException;
/**
* 根據(jù)消息ID獲取消息
*/
public RpTransactionMessage getMessageByMessageId(String messageId) throws MessageBizException;
/**
* 根據(jù)消息ID刪除消息
*/
public void deleteMessageByMessageId(String messageId) throws MessageBizException;
/**
* 重發(fā)某個(gè)消息隊(duì)列中的全部已死亡的消息.
*/
public void reSendAllDeadMessageByQueueName(String queueName, int batchSize) throws MessageBizException;
/**
* 獲取分頁(yè)數(shù)據(jù)
*/
PageBean listPage(PageParam pageParam, Map<String, Object> paramMap) throws MessageBizException;
}
@Service("rpTransactionMessageService")
public class RpTransactionMessageServiceImpl implements RpTransactionMessageService {
private static final Log log = LogFactory.getLog(RpTransactionMessageServiceImpl.class);
@Autowired
private RpTransactionMessageDao rpTransactionMessageDao;
@Autowired
private JmsTemplate notifyJmsTemplate;
public int saveMessageWaitingConfirm(RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息為空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消費(fèi)隊(duì)列不能為空 ");
}
message.setEditTime(new Date());
message.setStatus(MessageStatusEnum.WAITING_CONFIRM.name());
message.setAreadlyDead(PublicEnum.NO.name());
message.setMessageSendTimes(0);
return rpTransactionMessageDao.insert(message);
}
public void confirmAndSendMessage(String messageId) {
final RpTransactionMessage message = getMessageByMessageId(messageId);
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "根據(jù)消息id查找的消息為空");
}
message.setStatus(MessageStatusEnum.SENDING.name());
message.setEditTime(new Date());
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
public int saveAndSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息為空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消費(fèi)隊(duì)列不能為空 ");
}
message.setStatus(MessageStatusEnum.SENDING.name());
message.setAreadlyDead(PublicEnum.NO.name());
message.setMessageSendTimes(0);
message.setEditTime(new Date());
int result = rpTransactionMessageDao.insert(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
return result;
}
public void directSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息為空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消費(fèi)隊(duì)列不能為空 ");
}
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
public void reSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息為空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消費(fèi)隊(duì)列不能為空 ");
}
message.addSendTimes();
message.setEditTime(new Date());
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
public void reSendMessageByMessageId(String messageId) {
final RpTransactionMessage message = getMessageByMessageId(messageId);
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "根據(jù)消息id查找的消息為空");
}
int maxTimes = Integer.valueOf(PublicConfigUtil.readConfig("message.max.send.times"));
if (message.getMessageSendTimes() >= maxTimes) {
message.setAreadlyDead(PublicEnum.YES.name());
}
message.setEditTime(new Date());
message.setMessageSendTimes(message.getMessageSendTimes() + 1);
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
public void setMessageToAreadlyDead(String messageId) {
RpTransactionMessage message = getMessageByMessageId(messageId);
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "根據(jù)消息id查找的消息為空");
}
message.setAreadlyDead(PublicEnum.YES.name());
message.setEditTime(new Date());
rpTransactionMessageDao.update(message);
}
public RpTransactionMessage getMessageByMessageId(String messageId) {
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("messageId", messageId);
return rpTransactionMessageDao.getBy(paramMap);
}
public void deleteMessageByMessageId(String messageId) {
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("messageId", messageId);
rpTransactionMessageDao.delete(paramMap);
}
@SuppressWarnings("unchecked")
public void reSendAllDeadMessageByQueueName(String queueName, int batchSize) {
log.info("==>reSendAllDeadMessageByQueueName");
int numPerPage = 1000;
if (batchSize > 0 && batchSize < 100) {
numPerPage = 100;
} else if (batchSize > 100 && batchSize < 5000) {
numPerPage = batchSize;
} else if (batchSize > 5000) {
numPerPage = 5000;
} else {
numPerPage = 1000;
}
int pageNum = 1;
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("consumerQueue", queueName);
paramMap.put("areadlyDead", PublicEnum.YES.name());
paramMap.put("listPageSortType", "ASC");
Map<String, RpTransactionMessage> messageMap = new HashMap<String, RpTransactionMessage>();
List<Object> recordList = new ArrayList<Object>();
int pageCount = 1;
PageBean pageBean = rpTransactionMessageDao.listPage(new PageParam(pageNum, numPerPage), paramMap);
recordList = pageBean.getRecordList();
if (recordList == null || recordList.isEmpty()) {
log.info("==>recordList is empty");
return;
}
pageCount = pageBean.getTotalPage();
for (final Object obj : recordList) {
final RpTransactionMessage message = (RpTransactionMessage) obj;
messageMap.put(message.getMessageId(), message);
}
for (pageNum = 2; pageNum <= pageCount; pageNum++) {
pageBean = rpTransactionMessageDao.listPage(new PageParam(pageNum, numPerPage), paramMap);
recordList = pageBean.getRecordList();
if (recordList == null || recordList.isEmpty()) {
break;
}
for (final Object obj : recordList) {
final RpTransactionMessage message = (RpTransactionMessage) obj;
messageMap.put(message.getMessageId(), message);
}
}
recordList = null;
pageBean = null;
for (Map.Entry<String, RpTransactionMessage> entry : messageMap.entrySet()) {
final RpTransactionMessage message = entry.getValue();
message.setEditTime(new Date());
message.setMessageSendTimes(message.getMessageSendTimes() + 1);
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
}
@SuppressWarnings("unchecked")
public PageBean<RpTransactionMessage> listPage(PageParam pageParam, Map<String, Object> paramMap) {
return rpTransactionMessageDao.listPage(pageParam, paramMap);
}
}
@Component("messageBiz")
public class MessageBiz {
private static final Log log = LogFactory.getLog(MessageBiz.class);
@Autowired
private RpTradePaymentQueryService rpTradePaymentQueryService;
@Autowired
private RpTransactionMessageService rpTransactionMessageService;
/**
* 處理[waiting_confirm]狀態(tài)的消息
* @param messages
*/
public void handleWaitingConfirmTimeOutMessages(Map<String, RpTransactionMessage> messageMap) {
log.debug("開(kāi)始處理[waiting_confirm]狀態(tài)的消息,總條數(shù)[" + messageMap.size() + "]");
// 單條消息處理(目前該狀態(tài)的消息癌幕,消費(fèi)隊(duì)列全部是accounting衙耕,如果后期有業(yè)務(wù)擴(kuò)充,需做隊(duì)列判斷勺远,做對(duì)應(yīng)的業(yè)務(wù)處理橙喘。)
for (Map.Entry<String, RpTransactionMessage> entry : messageMap.entrySet()) {
RpTransactionMessage message = entry.getValue();
try {
log.debug("開(kāi)始處理[waiting_confirm]消息ID為[" + message.getMessageId() + "]的消息");
String bankOrderNo = message.getField1();
RpTradePaymentRecord record = rpTradePaymentQueryService.getRecordByBankOrderNo(bankOrderNo);
// 如果訂單成功,把消息改為待處理胶逢,并發(fā)送消息
if (TradeStatusEnum.SUCCESS.name().equals(record.getStatus())) {
// 確認(rèn)并發(fā)送消息
rpTransactionMessageService.confirmAndSendMessage(message.getMessageId());
} else if (TradeStatusEnum.WAITING_PAYMENT.name().equals(record.getStatus())) {
// 訂單狀態(tài)是等到支付厅瞎,可以直接刪除數(shù)據(jù)
log.debug("訂單沒(méi)有支付成功,刪除[waiting_confirm]消息id[" + message.getMessageId() + "]的消息");
rpTransactionMessageService.deleteMessageByMessageId(message.getMessageId());
}
log.debug("結(jié)束處理[waiting_confirm]消息ID為[" + message.getMessageId() + "]的消息");
} catch (Exception e) {
log.error("處理[waiting_confirm]消息ID為[" + message.getMessageId() + "]的消息異常:", e);
}
}
}
/**
* 處理[SENDING]狀態(tài)的消息
* @param messages
*/
public void handleSendingTimeOutMessage(Map<String, RpTransactionMessage> messageMap) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.debug("開(kāi)始處理[SENDING]狀態(tài)的消息,總條數(shù)[" + messageMap.size() + "]");
// 根據(jù)配置獲取通知間隔時(shí)間
Map<Integer, Integer> notifyParam = getSendTime();
// 單條消息處理
for (Map.Entry<String, RpTransactionMessage> entry : messageMap.entrySet()) {
RpTransactionMessage message = entry.getValue();
try {
log.debug("開(kāi)始處理[SENDING]消息ID為[" + message.getMessageId() + "]的消息");
// 判斷發(fā)送次數(shù)
int maxTimes = Integer.valueOf(PublicConfigUtil.readConfig("message.max.send.times"));
log.debug("[SENDING]消息ID為[" + message.getMessageId() + "]的消息,已經(jīng)重新發(fā)送的次數(shù)["
+ message.getMessageSendTimes() + "]");
// 如果超過(guò)最大發(fā)送次數(shù)直接退出
if (maxTimes < message.getMessageSendTimes()) {
// 標(biāo)記為死亡
rpTransactionMessageService.setMessageToAreadlyDead(message.getMessageId());
continue;
}
// 判斷是否達(dá)到發(fā)送消息的時(shí)間間隔條件
int reSendTimes = message.getMessageSendTimes();
int times = notifyParam.get(reSendTimes == 0 ? 1 : reSendTimes);
long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();
long needTime = currentTimeInMillis - times * 60 * 1000;
long hasTime = message.getEditTime().getTime();
// 判斷是否達(dá)到了可以再次發(fā)送的時(shí)間條件
if (hasTime > needTime) {
log.debug("currentTime[" + sdf.format(new Date()) + "],[SENDING]消息上次發(fā)送時(shí)間["
+ sdf.format(message.getEditTime()) + "],必須過(guò)了[" + times + "]分鐘才可以再發(fā)送。");
continue;
}
// 重新發(fā)送消息
rpTransactionMessageService.reSendMessage(message);
log.debug("結(jié)束處理[SENDING]消息ID為[" + message.getMessageId() + "]的消息");
} catch (Exception e) {
log.error("處理[SENDING]消息ID為[" + message.getMessageId() + "]的消息異常:", e);
}
}
}
/**
* 根據(jù)配置獲取通知間隔時(shí)間
* @return
*/
private Map<Integer, Integer> getSendTime() {
Map<Integer, Integer> notifyParam = new HashMap<Integer, Integer>();
notifyParam.put(1, Integer.valueOf(PublicConfigUtil.readConfig("message.send.1.time")));
notifyParam.put(2, Integer.valueOf(PublicConfigUtil.readConfig("message.send.2.time")));
notifyParam.put(3, Integer.valueOf(PublicConfigUtil.readConfig("message.send.3.time")));
notifyParam.put(4, Integer.valueOf(PublicConfigUtil.readConfig("message.send.4.time")));
notifyParam.put(5, Integer.valueOf(PublicConfigUtil.readConfig("message.send.5.time")));
return notifyParam;
}
}
public class AccountingMessageListener implements SessionAwareMessageListener<Message> {
private static final Log LOG = LogFactory.getLog(AccountingMessageListener.class);
/**
* 會(huì)計(jì)隊(duì)列模板(由Spring創(chuàng)建并注入進(jìn)來(lái))
*/
@Autowired
private JmsTemplate notifyJmsTemplate;
@Autowired
private RpAccountingVoucherService rpAccountingVoucherService;
@Autowired
private RpTransactionMessageService rpTransactionMessageService;
public synchronized void onMessage(Message message, Session session) {
RpAccountingVoucher param = null;
String strMessage = null;
try {
ActiveMQTextMessage objectMessage = (ActiveMQTextMessage) message;
strMessage = objectMessage.getText();
LOG.info("strMessage1 accounting:" + strMessage);
param = JSONObject.parseObject(strMessage, RpAccountingVoucher.class);
// 這里轉(zhuǎn)換成相應(yīng)的對(duì)象還有問(wèn)題
if (param == null) {
LOG.info("param參數(shù)為空");
return;
}
int entryType = param.getEntryType();
double payerChangeAmount = param.getPayerChangeAmount();
String voucherNo = param.getVoucherNo();
String payerAccountNo = param.getPayerAccountNo();
int fromSystem = param.getFromSystem();
int payerAccountType = 0;
if (param.getPayerAccountType() != null && !param.getPayerAccountType().equals("")) {
payerAccountType = param.getPayerAccountType();
}
double payerFee = param.getPayerFee();
String requestNo = param.getRequestNo();
double bankChangeAmount = param.getBankChangeAmount();
double receiverChangeAmount = param.getReceiverChangeAmount();
String receiverAccountNo = param.getReceiverAccountNo();
String bankAccount = param.getBankAccount();
String bankChannelCode = param.getBankChannelCode();
double profit = param.getProfit();
double income = param.getIncome();
double cost = param.getCost();
String bankOrderNo = param.getBankOrderNo();
int receiverAccountType = 0;
double payAmount = param.getPayAmount();
if (param.getReceiverAccountType() != null && !param.getReceiverAccountType().equals("")) {
receiverAccountType = param.getReceiverAccountType();
}
double receiverFee = param.getReceiverFee();
String remark = param.getRemark();
rpAccountingVoucherService.createAccountingVoucher(entryType, voucherNo, payerAccountNo, receiverAccountNo,
payerChangeAmount, receiverChangeAmount, income, cost, profit, bankChangeAmount, requestNo,
bankChannelCode, bankAccount, fromSystem, remark, bankOrderNo, payerAccountType, payAmount,
receiverAccountType, payerFee, receiverFee);
//刪除消息
rpTransactionMessageService.deleteMessageByMessageId(param.getMessageId());
} catch (BizException e) {
// 業(yè)務(wù)異常初坠,不再寫(xiě)會(huì)隊(duì)列
LOG.error("==>BizException", e);
} catch (Exception e) {
// 不明異常不再寫(xiě)會(huì)隊(duì)列
LOG.error("==>Exception", e);
}
}
public JmsTemplate getNotifyJmsTemplate() {
return notifyJmsTemplate;
}
public void setNotifyJmsTemplate(JmsTemplate notifyJmsTemplate) {
this.notifyJmsTemplate = notifyJmsTemplate;
}
public RpAccountingVoucherService getRpAccountingVoucherService() {
return rpAccountingVoucherService;
}
public void setRpAccountingVoucherService(RpAccountingVoucherService rpAccountingVoucherService) {
this.rpAccountingVoucherService = rpAccountingVoucherService;
}
}
與常規(guī)MQ的ACK機(jī)制對(duì)比
常規(guī)MQ確認(rèn)機(jī)制:
- Producer生成消息并發(fā)送給MQ(同步和簸、異步);
- MQ接收消息并將消息數(shù)據(jù)持久化到消息存儲(chǔ)(持久化操作為可選配置)碟刺;
- MQ向Producer返回消息的接收結(jié)果(返回值锁保、異常);
- Consumer監(jiān)聽(tīng)并消費(fèi)MQ中的消息半沽;
- Consumer獲取到消息后執(zhí)行業(yè)務(wù)處理爽柒;
- Consumer對(duì)已成功消費(fèi)的消息向MQ進(jìn)行ACK確認(rèn)(確認(rèn)后的消息將從MQ中刪除);
常規(guī)MQ隊(duì)列消息的處理流程無(wú)法實(shí)現(xiàn)消息發(fā)送一致性者填,因此直接使用現(xiàn)成的MQ中間件產(chǎn)品無(wú)法實(shí)現(xiàn)可靠消息最終一致性的分布式事務(wù)解決方案
消息發(fā)送一致性:是指產(chǎn)生消息的業(yè)務(wù)動(dòng)作與消息發(fā)送的一致浩村。也就是說(shuō),如果業(yè)務(wù)操作成功幔托,那么由這個(gè)業(yè)務(wù)操作所產(chǎn)生的消息一定要成功投遞出去(一般是發(fā)送到kafka穴亏、rocketmq、rabbitmq等消息中間件中)重挑,否則就丟消息嗓化。
下面用偽代碼進(jìn)行演示消息發(fā)送和投遞的不可靠性:
先進(jìn)行數(shù)據(jù)庫(kù)操作,再發(fā)送消息:
public void test1(){
//1 數(shù)據(jù)庫(kù)操作
//2 發(fā)送MQ消息
}
這種情況下無(wú)法保證數(shù)據(jù)庫(kù)操作與發(fā)送消息的一致性谬哀,因?yàn)榭赡軘?shù)據(jù)庫(kù)操作成功刺覆,發(fā)送消息失敗。
先發(fā)送消息史煎,再操作數(shù)據(jù)庫(kù):
public void test1(){
//1 發(fā)送MQ消息
//2 數(shù)據(jù)庫(kù)操作
}
這種情況下無(wú)法保證數(shù)據(jù)庫(kù)操作與發(fā)送消息的一致性谦屑,因?yàn)榭赡馨l(fā)送消息成功驳糯,數(shù)據(jù)庫(kù)操作失敗。
在數(shù)據(jù)庫(kù)事務(wù)中氢橙,先發(fā)送消息酝枢,后操作數(shù)據(jù)庫(kù):
@Transactional
public void test1(){
//1 發(fā)送MQ消息
//2 數(shù)據(jù)庫(kù)操作
}
這里使用spring 的@Transactional注解,方法里面的操作都在一個(gè)事務(wù)中悍手。同樣無(wú)法保證一致性帘睦,因?yàn)榘l(fā)送消息成功了,數(shù)據(jù)庫(kù)操作失敗的情況下坦康,數(shù)據(jù)庫(kù)操作是回滾了竣付,但是MQ消息沒(méi)法進(jìn)行回滾。
在數(shù)據(jù)庫(kù)事務(wù)中滞欠,先操作數(shù)據(jù)庫(kù)古胆,后發(fā)送消息:
@Transactional
public void test1(){
//1 數(shù)據(jù)庫(kù)操作
//2 發(fā)送MQ消息
}
這種情況下,貌似沒(méi)有問(wèn)題筛璧,如果發(fā)送MQ消息失敗逸绎,拋出異常,事務(wù)一定會(huì)回滾(加上了@Transactional注解后隧哮,spring方法拋出異常后桶良,會(huì)自動(dòng)進(jìn)行回滾)。
這只是一個(gè)假象沮翔,因?yàn)榘l(fā)送MQ消息可能事實(shí)上已經(jīng)成功陨帆,如果是響應(yīng)超時(shí)導(dǎo)致的異常。這個(gè)時(shí)候采蚀,數(shù)據(jù)庫(kù)操作依然回滾疲牵,但是MQ消息實(shí)際上已經(jīng)發(fā)送成功,導(dǎo)致不一致榆鼠。
與消息發(fā)送一致性流程的對(duì)比:
- 常規(guī)MQ隊(duì)列消息的處理流程無(wú)法實(shí)現(xiàn)消息發(fā)送一致性纲爸;
- 投遞消息的流程其實(shí)就是消息的消費(fèi)流程,可細(xì)化妆够;
TCC (Try-Confirm-Cancel)補(bǔ)償模式(最終一致性)
TCC 其實(shí)就是采用的補(bǔ)償機(jī)制识啦,其核心思想是:針對(duì)每個(gè)操作,都要注冊(cè)一個(gè)與其對(duì)應(yīng)的確認(rèn)和補(bǔ)償(撤銷(xiāo))操作神妹。
它分為三個(gè)階段:
- Try 階段主要是對(duì)業(yè)務(wù)系統(tǒng)做檢測(cè)及資源預(yù)留
- Confirm 階段主要是對(duì)業(yè)務(wù)系統(tǒng)做確認(rèn)提交颓哮,Try階段執(zhí)行成功并開(kāi)始執(zhí)行 Confirm階段時(shí),默認(rèn) Confirm階段是不會(huì)出錯(cuò)的鸵荠。即:只要Try成功冕茅,Confirm一定成功。
- Cancel 階段主要是在業(yè)務(wù)執(zhí)行錯(cuò)誤,需要回滾的狀態(tài)下執(zhí)行的業(yè)務(wù)取消姨伤,預(yù)留資源釋放哨坪。
舉例(Bob 要向 Smith 轉(zhuǎn)賬):
- 首先在 Try 階段,要先調(diào)用遠(yuǎn)程接口把 Smith 和 Bob 的錢(qián)給凍結(jié)起來(lái)乍楚。
- 在 Confirm 階段当编,執(zhí)行遠(yuǎn)程調(diào)用的轉(zhuǎn)賬的操作,轉(zhuǎn)賬成功進(jìn)行解凍徒溪。
- 如果第2步執(zhí)行成功凌箕,那么轉(zhuǎn)賬成功,如果第二步執(zhí)行失敗词渤,則調(diào)用遠(yuǎn)程凍結(jié)接口對(duì)應(yīng)的解凍方法 (Cancel)。
優(yōu)點(diǎn):
跟2PC比起來(lái)串绩,實(shí)現(xiàn)以及流程相對(duì)簡(jiǎn)單了一些缺虐,但數(shù)據(jù)的一致性比2PC也要差一些
缺點(diǎn):
缺點(diǎn)還是比較明顯的,在2,3步中都有可能失敗礁凡。TCC屬于應(yīng)用層的一種補(bǔ)償方式高氮,所以需要程序員在實(shí)現(xiàn)的時(shí)候多寫(xiě)很多補(bǔ)償?shù)拇a,在一些場(chǎng)景中顷牌,一些業(yè)務(wù)流程可能用TCC不太好定義及處理剪芍。
可靠消息最終一致(常用)
不要用本地的消息表了,直接基于MQ來(lái)實(shí)現(xiàn)事務(wù)窟蓝。比如阿里的RocketMQ就支持消息事務(wù)罪裹。
大概流程:
- A系統(tǒng)先發(fā)送一個(gè)prepared消息到mq,如果這個(gè)prepared消息發(fā)送失敗那么就直接取消操作別執(zhí)行了
- 如果這個(gè)消息發(fā)送成功過(guò)了运挫,那么接著執(zhí)行本地事務(wù)状共,如果成功就告訴mq發(fā)送確認(rèn)消息,如果失敗就告訴mq回滾消息
- 如果發(fā)送了確認(rèn)消息谁帕,那么此時(shí)B系統(tǒng)會(huì)接收到確認(rèn)消息峡继,然后執(zhí)行本地的事務(wù)
- mq會(huì)自動(dòng)定時(shí)輪詢(xún)所有prepared消息回調(diào)你的接口,問(wèn)你匈挖,這個(gè)消息是不是本地事務(wù)處理失敗了碾牌,所有沒(méi)發(fā)送確認(rèn)消息?那是繼續(xù)重試還是回滾儡循?一般來(lái)說(shuō)這里你就可以查下數(shù)據(jù)庫(kù)看之前本地事務(wù)是否執(zhí)行舶吗,如果回滾了,那么這里也回滾吧贮折。這個(gè)就是避免可能本地事務(wù)執(zhí)行成功了裤翩,別確認(rèn)消息發(fā)送失敗了。
這個(gè)方案里,要是系統(tǒng)B的事務(wù)失敗了咋辦踊赠?重試咯呵扛,自動(dòng)不斷重試直到成功,如果實(shí)在是不行筐带,要么就是針對(duì)重要的資金類(lèi)業(yè)務(wù)進(jìn)行回滾今穿,比如B系統(tǒng)本地回滾后,想辦法通知系統(tǒng)A也回滾伦籍;或者是發(fā)送報(bào)警由人工來(lái)手工回滾和補(bǔ)償
目前國(guó)內(nèi)互聯(lián)網(wǎng)公司大都是這么玩兒的蓝晒,要不你使用RocketMQ支持的,要不你就基于其他MQ中間件自己封裝一套類(lèi)似的邏輯帖鸦,總之思路就是這樣的芝薇。
最大努力通知
業(yè)務(wù)發(fā)起方將協(xié)調(diào)服務(wù)的消息發(fā)送到MQ,下游服務(wù)接收此消息作儿,如果處理失敗洛二,將進(jìn)行重試,重試N次后依然失敗攻锰,將不進(jìn)行重試晾嘶,放棄處理,這個(gè)應(yīng)用場(chǎng)景要求對(duì)事物性要求不高的地方娶吞。