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: 2659217497@qq.com
#此处为客户端授权密码
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: 2659217497@qq.com
#此处为客户端授权密码
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