RabbitMQ解決分佈式事務實例

1.產生事務條件如下:

確認生產者將信息投遞到MQ服務器中(採用MQ確認機制)
    生產者向MQ發送消息失敗,採用重試機制
確認消費者能正確的消費消息,採用手動ACK模式(注意冪等性問題)
    消費者消費消息失敗,生產者無需回滾 
生產者和消費者都成功,但是生產者後續步驟出現異常,數據庫事務回滾
    生產者同時投遞到兩個隊列,第二個隊列判斷生產者數據是否插入數據庫,未插入則執行數據庫插入邏輯


項目背景:umz-merchant-platform 商戶平臺項目  --用於創建訂單 生成訂單表
        umz-pay                支付項目     --跟據訂單表中的訂單號生成對應的流水錶

 2.實例項目構建

 3.部分代碼

umz-merchant-platform 商戶平臺項目

#########發起訂單請求#######################

package com.microservice.soa.controller;

import java.util.Date;

import javax.annotation.Resource;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.microservice.soa.dao.order.EquipOrderDao;
import com.microservice.soa.model.EquipOrder;
import com.microservice.soa.service.order.EquipOrderService;
import com.microservice.soa.service.order.impl.EquipOrderServiceImpl;
import com.microservice.soa.util.Utils;
import com.microservice.soa.vo.EquipOrderRequest;

@RestController
public class EquipOrderController {
	
	@Resource
	private EquipOrderServiceImpl equipOrderService;

	@RequestMapping("createOrder")
	@ResponseBody
	public String createOrder(@RequestBody EquipOrderRequest equipOrder) {
		String result = equipOrderService.createOrder_tx(equipOrder);
		return result;
		
	}
}
##########################生成訂單 發送端 消息提供者 ###########################

package com.microservice.soa.service.order.impl;

import java.math.BigDecimal;
import java.util.Date;
import java.util.UUID;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.microservice.soa.dao.order.EquipOrderDao;
import com.microservice.soa.model.EquipOrder;
import com.microservice.soa.util.JsonToBean;
import com.microservice.soa.util.Utils;
import com.microservice.soa.vo.EquipOrderRequest;

import net.sf.json.JSONObject;

/**
 *  訂單業務實現
 *  分佈式事務思路解決方案
 *  	1.如果消費者消費消息失敗,生產者是不需要回滾事務
 *  		解決方法:消費採用手動ack應答方式,採用MQ進行補償重試機制,注意MQ補償冪等問題。
 *  	2.如何確認生產者一定要將數據投遞到MQ服務器中Confirm機制(確認應答機制)
 *  		如查生產者發送消息到MQ服務器失敗
 *  		解決辦法:使用生產者重試機制進行發消息。
 * @author jiajie
 *
 */
@Service("equipOrderService")
public class EquipOrderServiceImpl implements RabbitTemplate.ConfirmCallback {
	private static Logger log = LoggerFactory.getLogger(EquipOrderServiceImpl.class);
	@Resource
	private EquipOrderDao equipOrderDao;
	
	@Resource
	RabbitTemplate template;

//	@Override
	@Transactional
	public int insert(EquipOrderRequest equipOrderRequest) {
		// TODO Auto-generated method stub
		EquipOrder equipOrder = new EquipOrder();
		equipOrder.setOrderId(Utils.createUtil());   	 //生成uuid
		equipOrder.setOrderNo(equipOrderRequest.getOrderNo()); 				//生成訂單號
		equipOrder.setCreateTime(new Date());
		equipOrder.setDeliveryTime(new Date());
		equipOrder.setOrderName(equipOrderRequest.getOrderName());
		equipOrder.setConsigneeProvince(equipOrderRequest.getConsigneeProvince());
		equipOrder.setConsigneeCity(equipOrderRequest.getConsigneeCity());
		equipOrder.setConsigneeArea(equipOrderRequest.getConsigneeArea());
		equipOrder.setConsigneeSite(equipOrderRequest.getConsigneeSite());
		equipOrder.setConsigneeTel(equipOrderRequest.getConsigneeTel());
		equipOrder.setOrderAmt(Long.parseLong(equipOrderRequest.getOrderAmt()));
		equipOrder.setOrderStatus("1");
		equipOrder.setFreightPrice(new BigDecimal(equipOrderRequest.getFreightPrice()));
		equipOrder.setGoodsPrice(new BigDecimal(equipOrderRequest.getGoodsPrice()));
		equipOrder.setGoodsNum(Integer.parseInt(equipOrderRequest.getGoodsNum()));
		equipOrder.setOrderName(equipOrderRequest.getOrderName());
		//插入訂單表
//		equipOrderDao.insert(equipOrder);
		//模擬補嘗 
		 int result = 1/0;
		//插入訂單表 
		return equipOrderDao.insert(equipOrder);
	}
	
	/**
	 * 創建訂單
	 * @param equipOrder
	 * @return
	 */
	@Transactional
	public String createOrder_tx(EquipOrderRequest equipOrderRequest) {
		try {
			String orderNo = Utils.getOrderIdByTime();
			equipOrderRequest.setOrderNo(orderNo);
			
			JSONObject json = JSONObject.fromObject(equipOrderRequest);
			String jsonString = json.toString();
			System.out.println("jsonString:" + jsonString);
			
			EquipOrder equipOrder = new EquipOrder();
			equipOrder.setOrderId(Utils.createUtil());   	 //生成uuid
			equipOrder.setOrderNo(equipOrderRequest.getOrderNo()); 				//生成訂單號
			equipOrder.setCreateTime(new Date());
			equipOrder.setDeliveryTime(new Date());
			equipOrder.setOrderName(equipOrderRequest.getOrderName());
			equipOrder.setConsigneeProvince(equipOrderRequest.getConsigneeProvince());
			equipOrder.setConsigneeCity(equipOrderRequest.getConsigneeCity());
			equipOrder.setConsigneeArea(equipOrderRequest.getConsigneeArea());
			equipOrder.setConsigneeSite(equipOrderRequest.getConsigneeSite());
			equipOrder.setConsigneeTel(equipOrderRequest.getConsigneeTel());
			equipOrder.setOrderAmt(Long.parseLong(equipOrderRequest.getOrderAmt()));
			equipOrder.setOrderStatus("1");
			equipOrder.setFreightPrice(new BigDecimal(equipOrderRequest.getFreightPrice()));
			equipOrder.setGoodsPrice(new BigDecimal(equipOrderRequest.getGoodsPrice()));
			equipOrder.setGoodsNum(Integer.parseInt(equipOrderRequest.getGoodsNum()));
			equipOrder.setOrderName(equipOrderRequest.getOrderName());
			//插入訂單表
//			equipOrderDao.insert(equipOrder);
			
			//插入訂單表 
			equipOrderDao.insert(equipOrder);
			
			
			// 生產者發送消息的時候需要設置消息id
			Message message = MessageBuilder.withBody(jsonString.getBytes())
					.setContentType(MessageProperties.CONTENT_TYPE_JSON).setContentEncoding("utf-8")
					.setMessageId(UUID.randomUUID() + "").build();
			//回調
			this.template.setMandatory(true);
			this.template.setConfirmCallback(this);
			//構建回調返回的數據(消息ID)
			CorrelationData correlationData = new CorrelationData(orderNo);
			correlationData.setReturnedMessage(message);
			template.convertAndSend("orderChange","order.bar.test",message,correlationData);
			//模擬補嘗 
			 int result = 1/0;
		} catch (AmqpException e) {
			// TODO Auto-generated catch block
//			e.printStackTrace();
			log.info("訂單創建失敗:原因【"+e.toString()+"】");
			return  "error";
		}
		
		return "success";
	}

	/**
	 * confirm 生產者確認機制 生產者往服務器發送消息的時候,採用應答機制
	 */
	@Override
	public void confirm(CorrelationData correlationData, boolean ack, String cause) {
		// TODO Auto-generated method stub
		log.info("消息ID:"+correlationData.getId()+"消息內容:"+correlationData.getReturnedMessage());
		
		try {
			String msg = new String(correlationData.getReturnedMessage().getBody());
			JSONObject json = JSONObject.fromObject(msg);
			EquipOrderRequest equipOrderRequest  = JsonToBean.json2Bean(json, EquipOrderRequest.class);
			
			if (ack) {
				log.info("消息發送成功!");
//				insert(equipOrderRequest);
			}else {
				createOrder_tx(equipOrderRequest);
				log.info("消息發送失敗!"+cause);
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}
#############################事物補嘗 隊列監聽 ################################

package com.microservice.soa.listener;

import java.io.IOException;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;

import com.alibaba.druid.util.DaemonThreadFactory;
import com.alibaba.druid.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservice.soa.dao.order.EquipOrderDao;
import com.microservice.soa.model.EquipOrder;
import com.microservice.soa.util.JsonToBean;
import com.microservice.soa.util.Utils;
import com.microservice.soa.vo.EquipOrderRequest;
import com.netflix.ribbon.proxy.annotation.Http.Header;
import com.rabbitmq.client.Channel;

import net.sf.json.JSONObject;



/**
 *  @RabbitListener 底層 使用Aop進行攔截,如果程序沒有拋出異常,自動提交事務,
 *  				如果Aop使用異常通知攔截  獲取異常信息的話,自動實現補償機制,該消息會緩存到rabbitmq服務器存放,
 *  				會一直重試到不拋出異常爲主。解決方式 修改重試機制策略 默認間隔5秒重試一次
 * 
 * 
 * 
* @ClassName: EmailMessageListener  
* @Description: TODO(消息處理監聽類 補嘗隊列訂單)  
* @author MAOJIAJIE  
* @date 2019年4月12日  
*
 */
@Component
public class OderMessageCompensationListener{
	private static Logger log = LoggerFactory.getLogger(OderMessageCompensationListener.class);
	
	@Resource
	private EquipOrderDao equipOrderDao;
	
	@RabbitListener(queues = {"orderCompensationQueue"})
	public void onMessage(Message message,@Headers Map<String, Object> header,Channel channel) throws Exception {
		// TODO Auto-generated method stub
		log.info("*************************開始訂單補嘗監聽消息*************************");
		String messageBody = new String(message.getBody());
		ObjectMapper mapper = new ObjectMapper();
		log.info("message:"+messageBody);
		
		/**
		 * rabbitmq 默認情況下,如果消費者程序出現異常的情況下,會自動實現補償機制 (重試機制)
		 * 隊列服務器發送補嘗請求
		 */
		try {
//			int i = 1/0;
			JSONObject json = JSONObject.fromObject(messageBody);
			EquipOrderRequest equipOrderRequest  = JsonToBean.json2Bean(json, EquipOrderRequest.class);
			log.info("接收到郵件消息:【" + equipOrderRequest.toString() + "】");
			if (StringUtils.isEmpty(equipOrderRequest.getOrderNo())) {
				// 手動簽收消息,通知mq服務器端刪除該消息
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
				return;
			}
			EquipOrder order = equipOrderDao.queryByOrderNoInfo(equipOrderRequest.getOrderNo());
			if (order!=null) {
				//訂單已經存在 手動簽收消息,通知mq服務器端刪除該消息
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
				return;
			}
			log.info("訂單號:"+equipOrderRequest.getOrderNo()+"開始補嘗操作!");
			EquipOrder equipOrder = new EquipOrder();
			equipOrder.setOrderId(Utils.createUtil());   	 //生成uuid
			equipOrder.setOrderNo(equipOrderRequest.getOrderNo()); 				//生成訂單號
			equipOrder.setCreateTime(new Date());
			equipOrder.setDeliveryTime(new Date());
			equipOrder.setOrderName(equipOrderRequest.getOrderName());
			equipOrder.setConsigneeProvince(equipOrderRequest.getConsigneeProvince());
			equipOrder.setConsigneeCity(equipOrderRequest.getConsigneeCity());
			equipOrder.setConsigneeArea(equipOrderRequest.getConsigneeArea());
			equipOrder.setConsigneeSite(equipOrderRequest.getConsigneeSite());
			equipOrder.setConsigneeTel(equipOrderRequest.getConsigneeTel());
			equipOrder.setOrderAmt(Long.parseLong(equipOrderRequest.getOrderAmt()));
			equipOrder.setOrderStatus("1");
			equipOrder.setFreightPrice(new BigDecimal(equipOrderRequest.getFreightPrice()));
			equipOrder.setGoodsPrice(new BigDecimal(equipOrderRequest.getGoodsPrice()));
			equipOrder.setGoodsNum(Integer.parseInt(equipOrderRequest.getGoodsNum()));
			equipOrder.setOrderName(equipOrderRequest.getOrderName());
			//插入訂單表
//			equipOrderDao.insert(equipOrder);
			//插入訂單表
			int result = equipOrderDao.insert(equipOrder);
			if (result>=0) { //插入成功
				//訂單插入成功,手動向通道發送簽收消息,通知mq服務器端刪除該消息
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
				return;
			}
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
//			log.error("發送郵件失敗!:【" + e.toString() + "】");
			//拒絕消費消息(丟失消息) 給死信隊列
			channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
		}
	}

}
###########################相關工具類##############################
package com.microservice.soa.util;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import net.sf.json.JSONArray;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;

public class JsonToBean {
	public static <T> T json2Bean(JSONObject json, Class<T> clazz) throws Exception {
        T t = clazz.newInstance();
        Method[] methods = clazz.getDeclaredMethods();
        if (methods == null || methods.length == 0)
            return null;

        for (Method method : methods) {
            String methodName = method.getName();
            System.out.println(methodName);
            if (methodName.startsWith("set")) {
                StringBuffer fieldName = new StringBuffer();

                // 形參類型:String long Product boolean double short int String List
                Class<?> ParamClazz = method.getParameterTypes()[0];

                // 通過set的method拿到字段名稱name time delFlag birth price price2 age
                fieldName.append(methodName.substring(3, 4).toLowerCase())//字段首字母小寫
                        .append(methodName.substring(4));

                if (!json.has(fieldName.toString())) {
                    continue;
                }

                // 常用基本類型 不含short,Date(支持String類型的 日期)
                if (ParamClazz == boolean.class || ParamClazz == Boolean.class) {
                    method.invoke(t, new Object[] { json.optBoolean(fieldName.toString(), false) });
                } else if (ParamClazz == int.class || ParamClazz == Integer.class) {
                    method.invoke(t, new Object[] { json.optInt(fieldName.toString(), 0) });
                } else if (ParamClazz == long.class || ParamClazz == Long.class) {
                    method.invoke(t, new Object[] { json.optLong(fieldName.toString(), 0) });
                } else if (ParamClazz == double.class || ParamClazz == Double.class) {
                    method.invoke(t, new Object[] { json.optDouble(fieldName.toString(), 0) });
                } else if (ParamClazz == String.class) {
                		method.invoke(t, new Object[] { json.optString(fieldName.toString(), "") });
                }else if(ParamClazz == Date.class){
                    	method.invoke(t, new Object[] { json.optString(fieldName.toString(),"") });
                }else if (List.class.isAssignableFrom(ParamClazz)) {//解析jsonArray爲list
                    JSONArray array = json.getJSONArray(fieldName.toString());
                    List list = new ArrayList<>();
                    for(int i =0;array!=null && i<array.size();i++){
                    	
                        JSONObject jsonobj = array.getJSONObject(i); 

                        Class<?> listType = (Class<?>) ((ParameterizedType) clazz   // 獲取List的泛型類型
                        	    .getDeclaredField(fieldName.toString()).getGenericType())
                                .getActualTypeArguments()[0];

                        Object obj = json2Bean(jsonobj, listType);
                        list.add(obj);
                    }
                    method.invoke(t,list);

                } else {//jsonObject和無法解析字段的判斷
                    try{
                        JSONObject getjsonObj = json.getJSONObject(fieldName.toString());
                        if (getjsonObj != null) {
                        	Object obj = json2Bean(getjsonObj, ParamClazz);
                            method.invoke(t, obj);
						}
                        
                    }catch(JSONException e){
                        e.printStackTrace();
                        continue;
                    }
                }
            }
        }
        return t;
    }
}

4.rabbitmq配置文件

package com.microservice.soa.conf;


import java.util.HashMap;
import java.util.Map;

import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitConfig {
    //爲了方便演示我都使用的public
    //隊列
    public static final String ORDER_QUEUE = "orderQueue";
    //死信隊列
    public static final String ORDER_DEAD_QUEUE = "orderDeadQueue";
    //交換機
    public static final String EXCHANGE_TOPIC_NAME = "orderChange";
    //死信交換機
    public static final String EXCHANGE_DEAD_TOPIC_NAME = "orderDeadChange";
    //#路由規則 匹配 email後面 所有的鍵
    public static final String QUEUE_BING_ROUTINGKEY="order.#";
    //綁定死信路由(死信隊列與死信交換機之前綁定關係)
    public static final String QUEUE_BING_EMAIL_DEAD_ROUTINGKEY="order.queue.dead.#";
    //綁定死信路由(消費隊列與死儲交換機綁定)
    public static final String QUEUE_BING_DEAD_ROUTING_ROUTINGKEY="order.deal.*";
    
    //補償隊列
    public static final String ORDER_COMPENSATION_QUEUE = "orderCompensationQueue";
    
   
    
    //# 提供者發送消息指定的 路由鍵(郵件)
    public static final String PRODUCER_ROUTINGKEY="order.bar.test";
    
    
    /**
     * 死信隊列 交換機標識符
     */
    private static final String DEAD_LETTER_QUEUE_KEY = "x-dead-letter-exchange";
    /**
     * 死信隊列交換機綁定鍵標識符
     */
    private static final String DEAD_LETTER_ROUTING_KEY = "x-dead-letter-routing-key";
    

    //聲明郵件隊列
    @Bean(ORDER_QUEUE)
    public Queue QUEUE_NEWS(){    //郵件隊列
    	Map<String, Object> args = new HashMap<String, Object>(2);
//    	//消費隊列---------------綁定死信交換機 (x-dead-letter-exchange    聲明  死信交換機)
//    	args.put(DEAD_LETTER_QUEUE_KEY,EXCHANGE_DEAD_TOPIC_NAME);
//    	//消費隊列---------------綁定死信路由  x-dead-letter-routing-key    聲明 死信路由鍵
//    	args.put(DEAD_LETTER_ROUTING_KEY, QUEUE_BING_DEAD_ROUTING_ROUTINGKEY);
//    
//        return new Queue(ORDER_QUEUE, true, false, false,args);
    	return new Queue(ORDER_QUEUE);
    }
    
    //補嘗隊列
    @Bean(ORDER_COMPENSATION_QUEUE)
    public Queue ORDER_COMPENSATION_QUEUE() {
    	return new Queue(ORDER_COMPENSATION_QUEUE);
    }

    //聲明交換機
    @Bean(EXCHANGE_TOPIC_NAME)
    public Exchange EXCHANGE_TOPIC_INFORM(){
        //聲明瞭一個Topic類型的交換機,durable是持久化(重啓rabbitmq這個交換機不會被自動刪除)
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPIC_NAME).durable(true).build();
    }

    //聲明ORDER_QUEUE隊列和交換機綁定關係,並且指定RoutingKey
    @Bean
    public Binding NEWS_BINDING_TOPIC(@Qualifier(ORDER_QUEUE) Queue queue,
                                      @Qualifier(EXCHANGE_TOPIC_NAME) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(QUEUE_BING_ROUTINGKEY).noargs();
    }
    
    //聲明ORDER_COMPENSATION_QUEUE補嘗隊列和交換機綁定關係,並且指定與ORDER_QUEUE隊列同一個RoutingKey由路key
    @Bean
    public Binding NEWS_BINDING_COMPENSATION_TOPIC(@Qualifier(ORDER_COMPENSATION_QUEUE) Queue queue,
                                      @Qualifier(EXCHANGE_TOPIC_NAME) Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(QUEUE_BING_ROUTINGKEY).noargs();
    }
    
    
    
//    /**
//     * 創建死信隊列
//     * @return
//     */
//    @Bean
//	public Queue deadQueue() {
//		Queue queue = new Queue(ORDER_DEAD_QUEUE, true);
//		return queue;
//	}
//
//    /**
//     * 創建死信交換機
//     * @return
//     */
//	@Bean
//	public DirectExchange deadExchange() {
//		return new DirectExchange(EXCHANGE_DEAD_TOPIC_NAME);
//	}
//
//	@Bean
//	public Binding bindingDeadExchange(Queue deadQueue, DirectExchange deadExchange) {
//		return BindingBuilder.bind(deadQueue).to(deadExchange).with(QUEUE_BING_DEAD_ROUTING_ROUTINGKEY);
//	} 
    
    /**
     * rabbit 數據傳輸方式JSON
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
   
}

5.application.yml文件

# native
#應用端口及應用名稱 商戶平臺項目
server:
  port: 8010
  servlet:
    context-path: /umz-merchant-platform


#數據庫配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/umz_db?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 12345678
    type: com.alibaba.druid.pool.DruidDataSource
    tomcat:
      initial-size: 1
      max-wait: 60000
      min-idle: 3
      max-age: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
  mvc:
    view:
      prefix: /WEB-INF/
      suffix: .jsp
  http:
    encoding:
      charset: UTF-8
      enabled: true
      force: true 
  #郵箱服務器
  mail:
    host: smtp.qq.com
    #發送郵箱用戶名
    username: [email protected]
    #此處爲客戶端授權密碼
    password: kmwptnlpeznjebaj
  #編碼集
    default-encoding: UTF-8
  #必須有!郵箱授權開啓,不然報錯
  properties:
    mail:
      smtp:
        auth: true
          #服務器地址校正
        localhost: smtp.qq.com

  
  application:
    name: umz-merchant-platform   #客戶端服務名

  #消息隊列
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    ### 地址
    virtual-host: /
    ##開啓消息確認機制 confirms
    publisher-confirms: true
    publisher-returns: true



#註冊信息
eureka:
  instance:
    hostname: localhost  #eureka客戶端主機實例名稱
    instance-id: umz-merchant-platform:8763 #客戶端實例名稱
    prefer-ip-address: true #顯示IP
    lease-renewal-interval-in-seconds: 1 #eureka 客戶端向服務端發送心跳間隔時間  單位秒
    lease-expiration-duration-in-seconds: 2 #eureka服務端收到最後一次等筆
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
      # 單機 defaultZone: http://localhost:2001/eureka   #把服務註冊到eureka註冊中心
      #defaultZone: http://eureka2001.java1234.com:2001/eureka/,http://eureka2002.java1234.com:2002/eureka/,http://eureka2003.java1234.com:2003/eureka/ # 集羣

mybatis:
  mapper-locations:
  - classpath:com/microservice/soa/dao/*.xml
  config-location: classpath:com/microservice/mybatis-config.xml
  type-aliases-package: com.microservice.soa.model

6.pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.microservice</groupId>
	<artifactId>umz-merchant-platform</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>umz-merchant-platform</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<zkclient.version>0.10</zkclient.version>
		<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		
		<dependency>
		    <groupId>com.rabbitmq</groupId>
		    <artifactId>amqp-client</artifactId>
		    <version>5.5.1</version>
		</dependency>
		
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        
         <dependency>
		    <groupId>com.alibaba</groupId>
		    <artifactId>druid</artifactId>
		    <version>1.1.22</version>
		</dependency>  
		
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.2</version>
		</dependency>
		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>net.sf.json-lib</groupId>
			<artifactId>json-lib</artifactId>
			<version>2.4</version>
			<classifier>jdk15</classifier>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		
		<!--日誌相關依賴-->                                                          
	   <dependency>                                                           
	       <groupId>org.springframework.boot</groupId>                        
	       <artifactId>spring-boot-starter-log4j2</artifactId>                
	   </dependency>                

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<dependency>
         <groupId>com.101tec</groupId>
         <artifactId>zkclient</artifactId>
         <version>${zkclient.version}</version>
     </dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

7.umz-pay 支付項目

##################消費者 跟據訂單表中的訂單號 生成 支付流水錶##########################
package com.microservice.soa.listener;

import java.io.IOException;
import java.util.Map;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;

import com.alibaba.druid.util.DaemonThreadFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservice.soa.dto.EquipOrderDto;
import com.microservice.soa.model.TradeWater;
import com.microservice.soa.service.trade.TradeWaterService;
import com.microservice.soa.util.JsonToBean;
import com.microservice.soa.util.Utils;
import com.netflix.ribbon.proxy.annotation.Http.Header;
import com.rabbitmq.client.Channel;

import net.sf.json.JSONObject;



/**
 *  @RabbitListener 底層 使用Aop進行攔截,如果程序沒有拋出異常,自動提交事務,
 *  				如果Aop使用異常通知攔截  獲取異常信息的話,自動實現補償機制,該消息會緩存到rabbitmq服務器存放,
 *  				會一直重試到不拋出異常爲主。解決方式 修改重試機制策略 默認間隔5秒重試一次
 * 
 * 
 * 
* @ClassName: EmailMessageListener  
* @Description: TODO(消息處理監聽類 訂單)  
* @author MAOJIAJIE  
* @date 2019年4月12日  
*
 */
@Component
public class OderToTradeMessageListener{
	private static Logger log = LoggerFactory.getLogger(OderToTradeMessageListener.class);
	
	@Resource
	private TradeWaterService tradeWaterService;
	
	@RabbitListener(queues = {"orderQueue"})
	public void onMessage(Message message,@Headers Map<String, Object> header,Channel channel) throws Exception {
		// TODO Auto-generated method stub
		log.info("*************************開始訂單監聽消息*************************");
		String messageBody = new String(message.getBody());
		ObjectMapper mapper = new ObjectMapper();
		System.out.println(messageBody);
		
		/**
		 * rabbitmq 默認情況下,如果消費者程序出現異常的情況下,會自動實現補償機制 (重試機制)
		 * 隊列服務器發送補嘗請求
		 */
		try {
//			int i = 1/0;
			JSONObject json = JSONObject.fromObject(messageBody);
			EquipOrderDto orderInfoDto  = JsonToBean.json2Bean(json, EquipOrderDto.class);
//			 Jackson2JsonMessageConverter jackson2JsonMessageConverter =new Jackson2JsonMessageConverter();
//			 EquipOrderDto equipOrderDto = (EquipOrderDto)jackson2JsonMessageConverter.fromMessage(message);
//			Email email = mapper.readValue(messageBody, Email.class);
			log.info("接收到訂單消息:【" + orderInfoDto.toString() + "】");
			//調用支付sdk
			
			//插入交易流水錶
			TradeWater tradeWater = new TradeWater();
			tradeWater.setMtwId(Utils.createUtil());		   //uuid
			tradeWater.setOrdNo(orderInfoDto.getOrderNo());   //訂單號
			tradeWater.setTradeAmt(tradeWater.getTradeAmt());  //交易金額
			tradeWater.setCreateTime(Utils.formatDateStr());   //創建時間 
			tradeWater.setTradeTime(Utils.formatDateStr());    //交易時間
			tradeWater.setTransType("2101");                   //消費類型
			tradeWater.setPayChannel("ALIPAY");                //交易碼類型
			int result = tradeWaterService.insertTradeWater(tradeWater);
			if (result>=0) {
				// 手動簽收消息,通知mq服務器端刪除該消息
				channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
//				return;
			}
			
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
//			log.error("發送郵件失敗!:【" + e.toString() + "】");
			//拒絕消費消息(丟失消息) 給死信隊列
			channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
		}
	}

}

8.application.yml配置文件

# native
#應用端口及應用名稱 支付項目
server:
  port: 8011
  servlet:
    context-path: /umz-pay


#數據庫配置
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/umz_pay_db?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: 12345678
    type: com.alibaba.druid.pool.DruidDataSource
    tomcat:
      initial-size: 1
      max-wait: 60000
      min-idle: 3
      max-age: 60000
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM DUAL
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
  mvc:
    view:
      prefix: /WEB-INF/
      suffix: .jsp
  http:
    encoding:
      charset: UTF-8
      enabled: true
      force: true 
  #郵箱服務器
  mail:
    host: smtp.qq.com
    #發送郵箱用戶名
    username: [email protected]
    #此處爲客戶端授權密碼
    password: kmwptnlpeznjebaj
  #編碼集
    default-encoding: UTF-8
  #必須有!郵箱授權開啓,不然報錯
  properties:
    mail:
      smtp:
        auth: true
          #服務器地址校正
        localhost: smtp.qq.com

  
  application:
    name: umz-pay   #客戶端服務名

  #消息隊列
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: admin
    password: admin
    ### 地址
    virtual-host: /



#註冊信息
eureka:
  instance:
    hostname: localhost  #eureka客戶端主機實例名稱
    instance-id: umz-pay:8764 #客戶端實例名稱
    prefer-ip-address: true #顯示IP
    lease-renewal-interval-in-seconds: 1 #eureka 客戶端向服務端發送心跳間隔時間  單位秒
    lease-expiration-duration-in-seconds: 2 #eureka服務端收到最後一次等筆
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
      # 單機 defaultZone: http://localhost:2001/eureka   #把服務註冊到eureka註冊中心
      #defaultZone: http://eureka2001.java1234.com:2001/eureka/,http://eureka2002.java1234.com:2002/eureka/,http://eureka2003.java1234.com:2003/eureka/ # 集羣

mybatis:
  mapper-locations:
  - classpath:com/microservice/soa/dao/*.xml
  config-location: classpath:com/microservice/mybatis-config.xml
  type-aliases-package: com.microservice.soa.model

9.pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.microservice</groupId>
	<artifactId>umz-pay</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>umz-pay</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
		<zkclient.version>0.10</zkclient.version>
		<spring-cloud.version>Hoxton.SR4</spring-cloud.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
		
		<dependency>
		    <groupId>com.rabbitmq</groupId>
		    <artifactId>amqp-client</artifactId>
		    <version>5.5.1</version>
		</dependency>
		
		<dependency>
			<groupId>net.sf.json-lib</groupId>
			<artifactId>json-lib</artifactId>
			<version>2.4</version>
			<classifier>jdk15</classifier>
		</dependency>
		
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        
         <dependency>
		    <groupId>com.alibaba</groupId>
		    <artifactId>druid</artifactId>
		    <version>1.1.22</version>
		</dependency>  
		
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.2</version>
		</dependency>
		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		
		<!--日誌相關依賴-->                                                          
	   <dependency>                                                           
	       <groupId>org.springframework.boot</groupId>                        
	       <artifactId>spring-boot-starter-log4j2</artifactId>                
	   </dependency>                

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		
		<dependency>
         <groupId>com.101tec</groupId>
         <artifactId>zkclient</artifactId>
         <version>${zkclient.version}</version>
     </dependency>
	</dependencies>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

10.測試

 

訂單插入異常時進入補嘗隊列 

支付項目執行監聽訂單信息

數據庫

訂單表

 交易流水錶

項目下載 地址:https://download.csdn.net/download/qq_31987649/12394885

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