
一. 跨庫事務解決方案
使用
二階段送出(try-commit/rollback)的方式:
二. 微服務解決方案
1. TCC(try - confirm/cancel)二階段送出方案——追求強一緻性方案訂單類 :
@Service(value = "orderServiceImpl")
public class OrderServiceImpl extends ServersManager<Order, OrderDao> implements
OrderService {
private static final org.slf4j.Logger logger = LoggerFactory
.getLogger(OrderServiceImpl.class);
@Resource(name="orderDaoFront")
@Override
public void setDao(OrderDao orderDao) {
this.dao = orderDao;
}
@Autowired
private OrderdetailDao orderdetailDao;
@Autowired
private OrderpayDao orderpayDao;
@Autowired
private OrdershipDao ordershipDao;
@Autowired
private OrderlogDao orderlogDao;
@Autowired
private ProductService productService;
@Autowired
private AccountService accountService;
public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}
public void setProductService(ProductService productService) {
this.productService = productService;
}
public void setOrderpayDao(OrderpayDao orderpayDao) {
this.orderpayDao = orderpayDao;
}
public void setOrderlogDao(OrderlogDao orderlogDao) {
this.orderlogDao = orderlogDao;
}
public void setOrdershipDao(OrdershipDao ordershipDao) {
this.ordershipDao = ordershipDao;
}
public void setOrderdetailDao(OrderdetailDao orderdetailDao) {
this.orderdetailDao = orderdetailDao;
}
public Order createOrder(Order order, List<Orderdetail> orderdetailList, Ordership ordership)
throws Exception {
if(order==null || orderdetailList==null || orderdetailList.size()==0 || ordership==null){
throw new NullPointerException("參數不能為空!");
}
//建立訂單
int orderID = dao.insert(order);
logger.debug("orderID=" + orderID);
//建立訂單項
for (int i = 0; i < orderdetailList.size(); i++) {
Orderdetail orderdetail = orderdetailList.get(i);
orderdetail.setOrderID(orderID);
orderdetailDao.insert(orderdetail);
}
//建立支付記錄對象
Orderpay orderpay = new Orderpay();
orderpay.setOrderid(order.getId());
orderpay.setPaystatus(Orderpay.orderpay_paystatus_n);
orderpay.setPayamount(Double.valueOf(order.getAmount()));
orderpay.setPaymethod(Orderpay.orderpay_paymethod_alipayescow);
int orderpayID = orderpayDao.insert(orderpay);
logger.error("orderpayID="+orderpayID);
order.setOrderpayID(String.valueOf(orderpayID));
//記錄配送資訊
ordership.setOrderid(String.valueOf(orderID));
ordershipDao.insert(ordership);
//記錄訂單建立日志
Orderlog orderlog = new Orderlog();
orderlog.setOrderid(String.valueOf(orderID));
orderlog.setAccount(order.getAccount());
orderlog.setContent("【建立訂單】使用者建立訂單。訂單總金額:"+order.getAmount());
orderlog.setAccountType(Orderlog.orderlog_accountType_w);
orderlogDao.insert(orderlog);
Order orderData=new Order();
orderData.setId(String.valueOf(orderID));
orderData.setOrderpayID(order.getOrderpayID());
return orderData;
}
@Autowired
public MessageService messageService;
public Order createMiaoshaOrder(Order order) {
if(order==null){
throw new RuntimeException("訂單參數不能為空!");
}
List<Orderdetail> orderdetailList = order.getOrderdetail();
Ordership ordership = order.getOrdership();
if(orderdetailList==null || orderdetailList.size()==0 || ordership==null){
throw new RuntimeException("訂單明細或位址參數不能為空!");
}
//建立訂單
int orderID = dao.insert(order);
logger.debug("orderID=" + orderID);
//使用mq的方式解決分布式事務
Message message = new Message(orderID,"",productId,"init");
messageService.receiveMsg(message);
//建立訂單明細
for (int i = 0; i < orderdetailList.size(); i++) {
Orderdetail orderdetail = orderdetailList.get(i);
orderdetail.setOrderID(orderID);
orderdetailDao.insert(orderdetail);
}
//建立支付記錄對象
Orderpay orderpay = new Orderpay();
orderpay.setOrderid(order.getId());
orderpay.setPaystatus(Orderpay.orderpay_paystatus_n);
orderpay.setPayamount(Double.valueOf(order.getAmount()));
orderpay.setPaymethod(Orderpay.orderpay_paymethod_alipayescow);
int orderpayID = orderpayDao.insert(orderpay);
logger.error("orderpayID="+orderpayID);
order.setOrderpayID(String.valueOf(orderpayID));
//記錄配送資訊
ordership.setOrderid(String.valueOf(orderID));
ordershipDao.insert(ordership);
//記錄訂單建立日志
Orderlog orderlog = new Orderlog();
orderlog.setOrderid(String.valueOf(orderID));
orderlog.setAccount(order.getAccount());
orderlog.setContent("【建立訂單】使用者建立訂單。訂單總金額:"+order.getAmount());
orderlog.setAccountType(Orderlog.orderlog_accountType_w);
orderlogDao.insert(orderlog);
//TODO 插入日志 log(orderId,productId,deductStockCount,status)
//定時任務 輪詢執行程式,從日志裡擷取該訂單資訊,往庫存庫裡操作,如果插入成功,定時任務就不需要繼續執行,如果不成功,繼續執行。
//減庫存,使用分布式事務後減庫存邏輯需交給消息中心處理,這塊邏輯需要注釋掉
for (int i = 0; i < orderdetailList.size(); i++) {
String productID = String.valueOf(orderdetailList.get(i).getProductID());
//資料庫減庫存
Product product = new Product();
product.setId(productID);
//TODO 插入日志 log(orderId,productId,deductStockCount,status)
int updateNums = productService.updateStockAfterMiaoshaSuccess(product);
if (updateNums <= 0) {
throw new ProductSoldOutException("更新庫存失敗");
}
}
Order orderData=new Order();
orderData.setId(String.valueOf(orderID));
orderData.setOrderpayID(order.getOrderpayID());
//發送确認消息
//使用mq的方式解決分布式事務
Message message = new Message(orderID,"",productId,"sent");
messageService.receiveMsg(message);
return orderData;
}
@Override
public List<Order> selectOrderInfo(Order order) {
return dao.selectOrderInfo(order);
}
// @Override
// public boolean updateOrderStatus(Order order) {
// if(order==null){
// throw new NullPointerException("參數不能為空!");
// }
//
// Orderpay orderpay = orderpayDao.selectById(order.getOrderpayID());
// if(orderpay==null){
// throw new NullPointerException("根據支付記錄号查詢不到支付記錄資訊!");
// }
// String orderid = orderpay.getOrderid();//訂單ID
//
// //更新支付記錄為成功支付
// Orderpay orderpay2 = new Orderpay();
// orderpay2.setId(order.getOrderpayID());
// orderpay2.setTradeNo(order.getTradeNo());
// orderpay2.setPaystatus(Orderpay.orderpay_paystatus_y);
// orderpayDao.update(orderpay2);
//
// //更新訂單的支付狀态為成功支付
// order.setId(orderid);
// order.setPaystatus(Order.order_paystatus_y);
// dao.update(order);
// return true;
// }
@Override
public boolean alipayNotify(String trade_status,String refund_status,String out_trade_no,String trade_no) {
try {
return alipayNotify0(trade_status, refund_status, out_trade_no, trade_no);
} catch (Exception e) {
logger.error(">>>alipayNotify...Exception..");
e.printStackTrace();
return false;
}
}
private boolean alipayNotify0(String trade_status,String refund_status,String out_trade_no,String trade_no) {
synchronized (FrontContainer.alipay_notify_lock) {
logger.error("trade_status = " + trade_status + ",refund_status = " + refund_status + ",out_trade_no = " + out_trade_no + ",trade_no = " + trade_no);
if ((StringUtils.isBlank(trade_status)
&& StringUtils.isBlank(refund_status)) || (StringUtils.isBlank(out_trade_no)
&& StringUtils.isBlank(trade_no))) {
logger.error("請求非法!");
return false;
}
String orderpayID = null;
if(out_trade_no.startsWith("test")){
//此處做一個說明,localhost或127.0.0.1下的訂單的請求發給支付寶的商戶訂單号都是test開頭的,正式的都是非test開頭的。
//可以參見OrdersAction.createPayInfo()方法。
orderpayID = out_trade_no.substring(4);
}else{
orderpayID = out_trade_no;
}
logger.error("orderpayID = " + orderpayID);
Orderpay orderpay = orderpayDao.selectById(orderpayID);
if(orderpay==null){
throw new NullPointerException("根據支付記錄号查詢不到支付記錄資訊!");
}
String orderid = orderpay.getOrderid();//訂單ID
String content = null;
if(StringUtils.isNotBlank(refund_status)){
/**
* 退款流程
*/
if(refund_status.equals("WAIT_SELLER_AGREE")){//等待賣家同意退款 ==>賣家需處理
content = "【支付寶異步通知-->退款流程】等待賣家同意退款(WAIT_SELLER_AGREE)。";
}else if(refund_status.equals("WAIT_BUYER_RETURN_GOODS")){//賣家同意退款,等待買家退貨 ==>通知買家退貨,此 可以發站内信、短信、或郵件 通知對方
content = "【支付寶異步通知-->退款流程】退款協定達成,等待買家退貨(WAIT_BUYER_RETURN_GOODS)。";
}else if(refund_status.equals("WAIT_SELLER_CONFIRM_GOODS")){//買家已退貨,等待賣家收到退貨 ==>支付寶會通知賣家
content = "【支付寶異步通知-->退款流程】等待賣家收貨(WAIT_SELLER_CONFIRM_GOODS)。";
}else if(refund_status.equals("REFUND_SUCCESS")){//賣家收到退貨,退款成功,交易關閉 ==>賣家登陸支付寶,确認OK。
//http://club.alipay.com/simple/?t9978565.html
content = "【支付寶異步通知-->退款流程】退款成功(REFUND_SUCCESS)。";
}else if(refund_status.equals("REFUND_CLOSED")){//賣家收到退貨,退款成功,交易關閉 ==>賣家登陸支付寶,确認OK。
//http://club.alipay.com/simple/?t9978565.html
content = "【支付寶異步通知-->退款流程】退款關閉(REFUND_CLOSED)。";
}else if(refund_status.equals("SELLER_REFUSE_BUYER")){//賣家收到退貨,退款成功,交易關閉 ==>賣家登陸支付寶,确認OK。
//http://club.alipay.com/simple/?t9978565.html
content = "【支付寶異步通知-->退款流程】賣家不同意協定,等待買家修改(SELLER_REFUSE_BUYER)。";
}
else{
//一般不會出現
content = "【支付寶異步通知-->退款流程】未知。refund_status = " + refund_status;
}
updateRefundStatus(orderid, refund_status);
}else if(StringUtils.isNotBlank(trade_status)){
/**
* 交易流程
*/
if(trade_status.equals("WAIT_BUYER_PAY")){//等待買家付款
content = "【支付寶異步通知】等待買家付款(WAIT_BUYER_PAY)。";
}else if(trade_status.equals("WAIT_SELLER_SEND_GOODS")){//已付款,等待賣家發貨
if(orderpay.getPaystatus().equals(Orderpay.orderpay_paystatus_y)){
//由于支付寶的同步通知、異步通知,那麼WAIT_SELLER_SEND_GOODS的時候會有2次通知,是以需要synchronized處理好,保證訂單狀态和日志的一緻性。
return true;
}
content = "【支付寶異步通知】已付款,等待賣家發貨(WAIT_SELLER_SEND_GOODS)。";
//更新支付記錄為【成功支付】
Orderpay orderpay2 = new Orderpay();
orderpay2.setId(orderpayID);
orderpay2.setTradeNo(trade_no);
orderpay2.setPaystatus(Orderpay.orderpay_paystatus_y);
orderpayDao.update(orderpay2);
/**
* 真實砍庫存,并同步減少内容庫存數
*/
logger.error("真實砍庫存,并同步減少内容庫存數...");
Orderdetail orderdetail = new Orderdetail();
orderdetail.setOrderID(Integer.valueOf(orderid));
List<Orderdetail> orderdetailList = orderdetailDao.selectList(orderdetail);
logger.error("orderdetailList = " + orderdetailList.size());
String lowStocks = null;//訂單是否缺貨記錄
// int score = 0;//商品積分彙總
for (int i = 0; i < orderdetailList.size(); i++) {
Orderdetail detailInfo = orderdetailList.get(i);
String productID = String.valueOf(detailInfo.getProductID());
//記憶體砍庫存呢
ProductStockInfo stockInfo = SystemManager.getInstance().getProductStockMap().get(productID);
if(stockInfo.getStock() >= detailInfo.getNumber()){
stockInfo.setStock(stockInfo.getStock() - detailInfo.getNumber());
stockInfo.setChangeStock(true);
//資料庫砍庫存
Product product = new Product();
product.setId(productID);
product.setStock(stockInfo.getStock());
product.setAddSellcount(detailInfo.getNumber());//增加銷量
productService.updateStockAfterPaySuccess(product);
}else{
lowStocks = Order.order_lowStocks_y;
//記錄庫存不足
Orderdetail od = new Orderdetail();
od.setId(detailInfo.getId());
od.setLowStocks(Orderdetail.orderdetail_lowstocks_y);
orderdetailDao.update(od);
}
// score += stockInfo.getScore();
logger.error("productID = " + productID + ",stockInfo.getStock() = " + stockInfo.getStock());
// SystemManager.productStockMap.put(product.getId(),stockInfo);
}
//更新訂單的支付狀态為【已支付】
Order order = new Order();
order.setId(orderid);
if (lowStocks != null) {
order.setLowStocks(Order.order_lowStocks_y);
}
order.setPaystatus(Order.order_paystatus_y);
dao.update(order);
}else if(trade_status.equals("WAIT_BUYER_CONFIRM_GOODS")){//已發貨,等待買家确認收貨
content = "【支付寶異步通知】已發貨,等待買家确認收貨(WAIT_BUYER_CONFIRM_GOODS)。";
//更新訂單狀态為【已發貨】
Order order = dao.selectById(orderid);
if(order==null){
throw new NullPointerException("根據訂單号查詢不到訂單資訊,orderid="+orderid);
}
logger.error("order.getStatus()"+order.getStatus()+",trade_status=WAIT_BUYER_CONFIRM_GOODS");
Orderlog orderlog = new Orderlog();
orderlog.setContent(content);
orderlog.setAccount(order.getAccount());
//臨時解決辦法,防止此日志重複記錄.
if(orderlogDao.selectCount(orderlog) > 0){
return true;
}
// if(order.getStatus().equals(Order.order_status_send)){
// //目前訂單狀态已經是這個狀态了,無需重複操作。
// return true;
// }
//防止由于支付寶異步消息的先後順序,導緻把訂單的狀态更新混亂了。
if(order.getStatus().equals(Order.order_status_pass)){
order = new Order();
order.setId(orderid);
order.setStatus(Order.order_status_send);
dao.update(order);
}
}else if(trade_status.equals("TRADE_FINISHED")){//交易完成
content = "【支付寶異步通知】交易完成(TRADE_FINISHED)。";
Order order = dao.selectById(orderid);
if(order==null){
throw new NullPointerException("根據訂單号查詢不到訂單資訊,orderid="+orderid);
}
//訂單結束後,訂單上面贈送的積分都成功轉移到使用者賬戶上。
Account acc = new Account();
acc.setAccount(order.getAccount());
acc.setAddScore(order.getScore() - order.getAmountExchangeScore());//支付完成,扣除訂單消耗的積分
accountService.updateScore(acc);
//更新訂單狀态為【已簽收】
order = new Order();
order.setId(orderid);
order.setStatus(Order.order_status_sign);
dao.update(order);
}else{
//一般不會出現
content = "【支付寶異步通知】未知。trade_status = " + trade_status;
}
}else{
throw new RuntimeException("運作異常!");
}
/**
* 以上代碼,如不可以傳回都會走到此處,記錄下日志.
*/
insertOrderlog(orderid, content);
return true;
}
}
/**
* 更新訂單的退款狀态
* @param orderid 訂單ID
* @param refundStatus 退款狀态
*/
private void updateRefundStatus(String orderid,String refundStatus){
Order order = new Order();
order.setId(orderid);
order.setRefundStatus(refundStatus);
dao.update(order);
}
/**
* 插入訂單記錄檔
* @param orderid 訂單ID
* @param content 日志内容
*/
private void insertOrderlog(String orderid,String content) {
Orderlog orderlog = new Orderlog();
orderlog.setOrderid(orderid);//訂單ID
orderlog.setAccount("alipay_notify");//操作人賬号
orderlog.setContent(content);//日志内容
orderlog.setAccountType(Orderlog.orderlog_accountType_p);
orderlogDao.insert(orderlog);
}
@Override
public OrderSimpleReport selectOrdersSimpleReport(String account) {
return dao.selectOrdersSimpleReport(account);
}
}
4. 使用mq消息隊列的方式做補償 消息中間件:
/**
* 獨立消息服務,目前隻實作一個大架構,主要目的是給大家理清分布式事務的控制思路
*
*/
@Service(value = "messageServiceImpl")
public class MessageServiceImpl implements MessageService {
@Override
@Transactional
public void receiveMsg(Message message) {
if (message == null) {
throw new RuntimeException("消息為空");
}
switch (message.getStatus()) {
case MessageStatus.INIT:
// TODO save msg
System.out.println("儲存初始消息");
break;
case MessageStatus.SENT:
// TODO update msg status 為sent
// TODO 發送消息到訂單減庫存隊列,這塊待同學們自己實作
System.out.println("更新消息狀态為sent");
break;
case MessageStatus.END:
// TODO update msg status 為end
break;
default:
throw new RuntimeException("消息狀态有誤");
}
}
}
MessageService :
public interface MessageService {
void receiveMsg(Message msg);
}
Message :
import java.io.Serializable;
public class Message implements Serializable {
private Integer id;
private String bizId;
private String bizType;
private String bizData;
private int status;
public Message(String bizId, String bizType, String bizData, int status) {
super();
this.bizId = bizId;
this.bizType = bizType;
this.bizData = bizData;
this.status = status;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getBizId() {
return bizId;
}
public void setBizId(String bizId) {
this.bizId = bizId;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public String getBizData() {
return bizData;
}
public void setBizData(String bizData) {
this.bizData = bizData;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
三. 定時任務 Quartz的使用
1. Quartz 概述Quartz 是 OpenSymphony 開源組織在 Job Scheduling 領域又一個開源項目,它可以與 J2EE 與 J2SE 應用程式相結合也可以單獨使用。Quartz 可以用來建立簡單或為運作十個,百個,甚至是好幾萬個 Jobs 這樣複雜的程式。Jobs 可以做成标準的 Java 元件或 EJBs。
2. 為什麼使用 Quartz?Quartz 是一個任務排程架構。比如你遇到這樣的問題:
- 每天 02:00 發送一份工作郵件給工作組成員并抄送給老闆(假裝自己很努力的工作到深夜)
- 每月 2 号提醒自己還信用卡或自動還款
- 每秒鐘發 N 筆髒資料給競争對手公司的伺服器(!←_←)
這些問題總結起來就是:在某一個有規律的時間點幹某件事。并且時間的觸發的條件可以非常複雜,複雜到需要一個專門的架構來幹這個事。Quartz 就是來幹這樣的事,你給它一個觸發條件的定義,它負責到了時間點,觸發相應的 Job 起來幹活(睡 NMB 起來嗨!)。
3. 什麼是 cron 表達式?一個 cron 表達式具體表現就是一個字元串,這個字元串中包含 6~7 個字段,字段之間是由空格分割的,每個字段可以由任何允許的值以及允許的特殊字元所構成,下面表格列出了每個字段所允許的值和特殊字元
-
字元被用來指定所有的值。如:*
在分鐘的字段域裡表示“每分鐘”。*
-
字元被用來指定一個範圍。如:-
在小時域意味着“10點、11點、12點”10-12
-
字元被用來指定另外的值。如:,
在星期域裡表示“星期一、星期三、星期五”.MON,WED,FRI
-
字元隻在日期域和星期域中使用。它被用來指定“非明确的值”。當你需要通過在這兩個域中的一個來指定一些東西的時候,它是有用的。?
-
字元指定在月或者星期中的某天(最後一天)。即 “Last” 的縮寫。但是在星期和月中 “L” 表示不同的意思,如:在月字段中 “L” 指月份的最後一天 “1月31日,2月28日”,如果在星期字段中則簡單的表示為“7”或者“SAT”。如果在星期字段中在某個 value 值得後面,則表示 “某月的最後一個星期 value” ,如 “6L” 表示某月的最後一個星期五。L
-
字元隻能用在月份字段中,該字段指定了離指定日期最近的那個星期日。W
-
字元隻能用在星期字段,該字段指定了第幾個星期 value 在某月中#
每一個元素都可以顯式地規定一個值(如 6),一個區間(如 9-12 ),一個清單(如 9,11,13 )或一個通配符(如 *)。“月份中的日期”和“星期中的日期”這兩個元素是互斥的,是以應該通過設定一個問号(?)來表明你不想設定的那個字段。
線上Cron表達式生成器
4. Spring Boot 內建 Quartz(1).
建立項目建立一個名為
hello-quartz
的項目
(2).
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.funtl</groupId>
<artifactId>hello-quartz</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>hello-quartz</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
主要增加了
org.springframework.boot:spring-boot-starter-quartz
依賴
(3).
Applicationpackage com.duo.hello.quatrz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling
@SpringBootApplication
public class HelloQuatrzApplication {
public static void main(String[] args) {
SpringApplication.run(HelloQuatrzApplication.class, args);
}
}
使用
@EnableScheduling
注解來開啟計劃任務功能
(4).
建立任務我們建立一個每 5 秒鐘列印目前時間的任務來測試 Quartz
package com.duo.hello.quatrz.tasks;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class PrintCurrentTimeTask {
@Scheduled(cron = "0/5 * * * * ? ")
public void printCurrentTime() {
System.out.println("Current Time is:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}
5. 定時任務 @Component
public class AnnotationQuartz {
@Scheduled(cron = "0/30 * * * * ?")
public void processInitMsg() {
//TODO 回調消息發送服務中心接口用消息業務id查找對應的業務資料是否生成,如果生成則直接将消息改成已發送狀态并投遞消息到mq
}
@Scheduled(cron = "0/60 * * * * ?")
public void processSentMsg() {
//TODO 定時處理未完成的消息,如果已經幾次查找消息都還是處于未完成狀态,則很可能消息沒有被消費者接收成功,由于消費者有幂等性實作,則可重發消息給mq
}
}