淘東電商項目(66) -聚合支付(基於RabbitMQ解決分佈式事務-積分場景)

引言

本文代碼已提交至Github(版本號:52553aa6fe8b34ff162a1fb33e8f58494b4d2c3f),有興趣的同學可以下載來看看:https://github.com/ylw-github/taodong-shop

閱讀本文前,有興趣的同學可以參考我之前寫的聚合支付的文章:

本文講解聚合支付最後的一個問題 - 分佈式事務。舉個例子,比如要增加一個“積分功能”,當第三方服務器異步返回支付成功結果,請求我們的支付服務器時,同時也要做積分增加的功能,如何能保證,支付結果插入數據庫成功的同時保證積分一定能增加成功呢?這裏涉及到了分佈式事務的問題,本文主要基於Rabbit來解決這個問題。

本文目錄結構:
l____引言
l____ 1.原理圖
l____ 2.積分數據庫建表
l____ 3.核心代碼
l________ 3.1 集成RabbitMQ
l________ 3.2 生產者代碼
l________ 3.3 消費者代碼
l____ 4.測試

1.原理圖

在這裏插入圖片描述
如上圖,如果支付成功,第三方支付服務器會請求項目的支付服務,返回支付結果,這個時候,我們代碼要處理的是如下步驟:

  1. 更新訂單狀態爲“已支付”,即status爲1(注意,這裏的方法使用了@Transactional事務註解修飾)
  2. 更新了支付狀態之後,會使用MQ來生產消息,生產增加積分消息MSG
  3. 如果這個時候程序出錯,會回滾,也就是訂單的狀態在數據庫中沒有修改,而已經增加了積分。

針對以上的問題,做出瞭如下的解決方案:

  • 對於第2個步驟,使用RabbitMQ的消息確認機制,保證消息一定可以投遞到RabbitMQ服務器的增加積分隊列,消費者使用手動簽收的方式,保證消息一定可以消費到,並把增加積分消息更新到數據庫的積分表中。
  • 對於第3個步驟,如果程序出錯了,會回滾,因此數據庫部分的代碼不生效,訂單的支付狀態沒變,所以增加多了一個支付狀態補償隊列,當支付狀態補償消費者接收到消息後,會檢查支付狀態是否已經修改,如果沒有修改,則更新訂單的狀態。

從上面的解決步驟,可以知道,使用RabbitMQ保證了積分一定可以更新本地數據庫,同時訂單狀態一定可以修改,達到最終一致性的效果,同時解決了分佈式事務的問題。

2.積分數據庫建表

講解前,先貼上積分數據庫的建表語句:

CREATE TABLE `integral` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
  `USER_ID` int(11) DEFAULT NULL COMMENT '用戶ID',
  `PAYMENT_ID` varchar(1024) DEFAULT NULL COMMENT '支付ID',
  `INTEGRAL` varchar(32) DEFAULT NULL COMMENT '積分',
  `AVAILABILITY` int(11) DEFAULT NULL COMMENT '是否可用',
  `REVISION` int(11) DEFAULT NULL COMMENT '樂觀鎖',
  `CREATED_BY` varchar(32) DEFAULT NULL COMMENT '創建人',
  `CREATED_TIME` datetime DEFAULT NULL COMMENT '創建時間',
  `UPDATED_BY` varchar(32) DEFAULT NULL COMMENT '更新人',
  `UPDATED_TIME` datetime DEFAULT NULL COMMENT '更新時間',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8 COMMENT=' ';

在這裏插入圖片描述

3.核心代碼

3.1 集成RabbitMQ

RabbitMQ的搭建本文不再詳述,之前有講解過,有興趣的童鞋可以參閱之前寫過的文章: 《消息中間件系列教程(04) -RabbitMQ -簡介&安裝》,下面開始講解項目集成。

①添加maven依賴:

<!-- 添加springboot對amqp的支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

②applicatoin.yml配置:

spring:
  rabbitmq:
    ####連接地址
    host: 127.0.0.1
    ####端口號
    port: 5672
    ####賬號
    username: guest
    ####密碼
    password: guest
    ### 地址
    virtual-host: integral_host
    ###開啓消息確認機制 confirms
    publisher-confirms: true
    publisher-returns: true

③RabbitMQ配置文件:

@Component
public class RabbitmqConfig {

    // 添加積分隊列
    public static final String INTEGRAL_DIC_QUEUE = "integral_queue";
    // 補單隊列,
    public static final String INTEGRAL_CREATE_QUEUE = "integral_create_queue";
    // 積分交換機
    private static final String INTEGRAL_EXCHANGE_NAME = "integral_exchange_name";

    // 1.定義訂單隊列
    @Bean
    public Queue directIntegralDicQueue() {
        return new Queue(INTEGRAL_DIC_QUEUE);
    }

    // 2.定義補訂單隊列
    @Bean
    public Queue directCreateintegralQueue() {
        return new Queue(INTEGRAL_CREATE_QUEUE);
    }

    // 2.定義交換機
    @Bean
    DirectExchange directintegralExchange() {
        return new DirectExchange(INTEGRAL_EXCHANGE_NAME);
    }

    // 3.積分隊列與交換機綁定
    @Bean
    Binding bindingExchangeintegralDicQueue() {
        return BindingBuilder.bind(directIntegralDicQueue()).to(directintegralExchange()).with("integralRoutingKey");
    }

    // 3.補單隊列與交換機綁定
    @Bean
    Binding bindingExchangeCreateintegral() {
        return BindingBuilder.bind(directCreateintegralQueue()).to(directintegralExchange()).with("integralRoutingKey");
    }

}

③在RabbitMQ控制檯增加virtual-host:
在這裏插入圖片描述
④分配guest對新增的virtual-host有用戶權限:
在這裏插入圖片描述
在這裏插入圖片描述

3.2 生產者代碼

①生產者代碼(注意裏面用了消息確認機制,且使用訂單的id作爲全局唯一id來解決冪等性的問題):

/**
 * description: 生產者投遞積分
 * create by: YangLinWei
 * create time: 2020/5/19 11:37 上午
 */
@Component
@Slf4j
public class IntegralProducer implements RabbitTemplate.ConfirmCallback {
	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Transactional
	public void send(JSONObject jsonObject) {

		String jsonString = jsonObject.toJSONString();
		System.out.println("jsonString:" + jsonString);
		String paymentId = jsonObject.getString("paymentId");
		// 封裝消息
		Message message = MessageBuilder.withBody(jsonString.getBytes())
				.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8").setMessageId(paymentId)
				.build();
		// 構建回調返回的數據(消息id)
		this.rabbitTemplate.setMandatory(true);
		this.rabbitTemplate.setConfirmCallback(this);
		CorrelationData correlationData = new CorrelationData(jsonString);
		rabbitTemplate.convertAndSend("integral_exchange_name", "integralRoutingKey", message, correlationData);

	}

	// 生產消息確認機制 生產者往服務器端發送消息的時候,採用應答機制
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		String jsonString = correlationData.getId();
		System.out.println("消息id:" + correlationData.getId());
		if (ack) {
			log.info(">>>使用MQ消息確認機制確保消息一定要投遞到MQ中成功");
			return;
		}
		JSONObject jsonObject = JSONObject.parseObject(jsonString);
		// 生產者消息投遞失敗的話,採用遞歸重試機制
		send(jsonObject);
		log.info(">>>使用MQ消息確認機制投遞到MQ中失敗");
	}
}

②調用生產者處的代碼,在支付結果異步回調處處理(銀聯支付結果異步回調處處理UnionPayCallbackTemplate,注意發送MQ使用了@Async註解,不阻塞當前線程)注意下面模擬拋異常了

@Override
public String asyncService(Map<String, String> verifySignature) {

	String orderId = verifySignature.get("orderId"); // 獲取後臺通知的數據,其他字段也可用類似方式獲取
	String respCode = verifySignature.get("respCode");

	// 判斷respCode=00、A6後,對涉及資金類的交易,請再發起查詢接口查詢,確定交易成功後更新數據庫。
	System.out.println("orderId:" + orderId + ",respCode:" + respCode);
	// 1.判斷respCode是否爲已經支付成功斷respCode=00、A6後,
	if (!(respCode.equals("00") || respCode.equals("A6"))) {
		return failResult();
	}
	// 根據日誌 手動補償 使用支付id調用第三方支付接口查詢
	PaymentTransactionEntity paymentTransaction = paymentTransactionMapper.selectByPaymentId(orderId);
	if (paymentTransaction.getPaymentStatus().equals(PayConstant.PAY_STATUS_SUCCESS)) {
		// 網絡重試中,之前已經支付過
		return successResult();
	}
	// 2.將狀態改爲已經支付成功
	paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "", orderId+"","yinlian_pay");
	// 3.調用積分服務接口增加積分(處理冪等性問題) MQ
	addMQIntegral(paymentTransaction); // 使用MQ
	int i = 1 / 0; // 支付狀態還是爲待支付狀態但是 積分缺增加
	return successResult();
}

/**
 * 基於MQ增加積分
 */
@Async
public void addMQIntegral(PaymentTransactionEntity paymentTransaction) {
	JSONObject jsonObject = new JSONObject();
	jsonObject.put("paymentId", paymentTransaction.getPaymentId());
	jsonObject.put("userId", paymentTransaction.getUserId());
	jsonObject.put("integral", 100);
	integralProducer.send(jsonObject);
}

3.3 消費者代碼

①首先看看支付狀態補償消費者代碼(注意這裏使用了手動簽收):

/**
 * description: 支付回調檢查狀態,是否爲已經支付完成
 * create by: YangLinWei
 * create time: 2020/5/19 1:52 下午
 */
@Component
@Slf4j
public class PayCheckStateConsumer {
    @Autowired
    private PaymentTransactionMapper paymentTransactionMapper;

    // 死信隊列(備胎) 消息被拒絕、隊列長度滿了 定時任務 人工補償

    @RabbitListener(queues = "integral_create_queue")
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
        try {
            String messageId = message.getMessageProperties().getMessageId();
            String msg = new String(message.getBody(), "UTF-8");
            log.info(">>>messageId:{},msg:{}", messageId, msg);
            JSONObject jsonObject = JSONObject.parseObject(msg);
            String paymentId = jsonObject.getString("paymentId");
            if (StringUtils.isEmpty(paymentId)) {
                log.error(">>>>支付id不能爲空 paymentId:{}", paymentId);
                basicNack(message, channel);
                return;
            }
            // 1.使用paymentId查詢之前是否已經支付過
            PaymentTransactionEntity paymentTransactionEntity = paymentTransactionMapper.selectByPaymentId(paymentId);
            if (paymentTransactionEntity == null) {
                log.error(">>>>支付id paymentId:{} 未查詢到", paymentId);
                basicNack(message, channel);
                return;
            }
            Integer paymentStatus = paymentTransactionEntity.getPaymentStatus();
            if (paymentStatus.equals(PayConstant.PAY_STATUS_SUCCESS)) {
                log.error(">>>>支付id paymentId:{} ", paymentId);
                basicNack(message, channel);
                return;
            }
            // 安全期間 主動調用第三方接口查詢
            String paymentChannel = jsonObject.getString("paymentChannel");
            int updatePaymentStatus = paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_STATUS_SUCCESS + "",
                    paymentId, paymentChannel);
            if (updatePaymentStatus > 0) {
                basicNack(message, channel);
                return;
            }
            // 繼續重試

        } catch (Exception e) {
            e.printStackTrace();
            basicNack(message, channel);
        }

    }

    private void basicNack(Message message, Channel channel) throws IOException {
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);

    }

}

②增加積分消費者代碼(注意這裏使用了手動簽收)::

/**
 * description: 積分服務消費者
 * create by: YangLinWei
 * create time: 2020/5/19 2:10 下午
 */
@Component
@Slf4j
public class IntegralConsumer {
	@Autowired
	private IntegralMapper integralMapper;

	@RabbitListener(queues = "integral_queue")
	public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws IOException {
		try {
			String messageId = message.getMessageProperties().getMessageId();
			String msg = new String(message.getBody(), "UTF-8");
			log.info(">>>messageId:{},msg:{}", messageId, msg);
			JSONObject jsonObject = JSONObject.parseObject(msg);
			String paymentId = jsonObject.getString("paymentId");
			if (StringUtils.isEmpty(paymentId)) {
				log.error(">>>>支付id不能爲空 paymentId:{}", paymentId);
				basicNack(message, channel);
				return;
			}
			// 使用paymentId查詢是否已經增加過積分 網絡重試間隔
			IntegralEntity resultIntegralEntity = integralMapper.findIntegral(paymentId);
			if (resultIntegralEntity != null) {
				log.error(">>>>paymentId:{}已經增加過積分", paymentId);
				// 已經增加過積分,通知MQ不要在繼續重試。
				basicNack(message, channel);
				return;
			}
			Integer userId = jsonObject.getInteger("userId");
			if (userId == null) {
				log.error(">>>>paymentId:{},對應的用戶userId參數爲空", paymentId);
				basicNack(message, channel);
				return;
			}
			Long integral = jsonObject.getLong("integral");
			if (integral == null) {
				log.error(">>>>paymentId:{},對應的用戶integral參數爲空", integral);
				return;
			}
			IntegralEntity integralEntity = new IntegralEntity();
			integralEntity.setPaymentId(paymentId);
			integralEntity.setIntegral(integral);
			integralEntity.setUserId(userId);
			integralEntity.setAvailability(1);
			// 插入到數據庫中
			int insertIntegral = integralMapper.insertIntegral(integralEntity);
			if (insertIntegral > 0) {
				// 手動簽收消息,通知mq服務器端刪除該消息
				basicNack(message, channel);
			}
			// 採用重試機制
		} catch (Exception e) {
			log.error(">>>>ERROR MSG:", e.getMessage());
			basicNack(message, channel);
		}

	}

	// 消費者獲取到消息之後 手動簽收 通知MQ刪除該消息
	private void basicNack(Message message, Channel channel) throws IOException {
		channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
	}

}

4.測試

依次啓動Eureka註冊中心、xxlsso單點登錄系統、member會員服務、pay支付服務、pay-web支付門戶服務、還有integral積分服務,啓動後如下圖:
在這裏插入圖片描述

啓動RabbitMQ服務(我的是Mac系統,已經啓動的可以忽略):

cd /usr/local/Cellar/rabbitmq/3.8.2/sbin
./rabbitmq-server -detached

①模擬新增訂單,瀏覽器輸入:http://localhost:8600/cratePayToken?payAmount=999&orderId=20200513141452&userId=27&productName=玉米香腸
在這裏插入圖片描述
②確認提交訂單,瀏覽器輸入:http://localhost:8079/pay?payToken=pay_88c6262f3a494ae98d0873283514abf5
在這裏插入圖片描述
可以看到當前數據庫,訂單狀態爲未支付:
在這裏插入圖片描述
③按照提示,使用銀聯支付,一步一步直至支付完成:
在這裏插入圖片描述

可以看到,訂單支付狀態爲已支付(也就是說訂單支付狀態補償消費者已經接收到消息,並處理訂單爲已支付):
在這裏插入圖片描述
而且積分表也增加了一條數據(也是是說增加積分消費者已收到消息,並增加了一條積分數據):
在這裏插入圖片描述

本文完!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章