分佈式事務項目實戰

網上找了個小項目,然後覺得收穫很大,有必要寫出來分享hhh

自動選課需求

1.支付成功即完成訂單,訂單完成之後系統需自動添加選課。

2.下圖是微信支付、學成在線訂單服務、學成在線學習服務交互圖:

 

 

 1、用戶支付完成,微信支付系統會主動通知學成在線支付結果,學成在線也可主動請求微信支付查詢訂單的支付結果。最終得到支付結果後將訂單支付結果保存到訂單數據庫中。

2、訂單支付完成系統自動向選課表添加學生選課記錄。

3、選課記錄添加完成學習即可在線開始學習。

 

訂單數據庫

訂單任務表     --訂單付款後,往訂單任務表插入一條數據,

 

在任務表中包括了交換機的名稱、路由key等信息爲了是將任務的處理做成一個通用的功能。考慮分佈式系統併發讀取任務處理任務的情況發生項目使用樂觀鎖的方式解決併發問題

已完成任務表: 自動給用戶添加完成選課後,往該表插入一條記錄,並刪除任務表的記錄

 

 

選課數據庫

學生選課表  ->   訂單完成後往該表插入一條記錄

學生選課記錄表    -> 歷史任務表,選課後往歷史記錄表插入一條記錄

 

 

 


由於訂單付款成功,那麼該課程自動添加進選課,他們直接的關係是付款成功那麼一定要把課程進行選課'

該項目綜合考慮選擇基於消息的分佈式事務解決方案,解決方案如下圖:

 

 

 1. 訂單完成後,在訂單任務表插入一條信息,

 2.使用定時器按時掃描訂單任務表,並使用mq發送隊列消息

 3.學習服務接收消息隊列消息,把信息插入選課表後,再插入一條歷史記錄表,然後通過mq響應結果

 4.訂單服務接收到mq後,把任務表的數據刪除,然後再往歷史記錄表中插入數據,更新選課事件爲完成

 

異常分析

如果訂單發送mq出現了異常,那麼任務表中的任務會一直存在,會一直髮送,知道成功了爲止.該節點奔潰不會影響數據結構

如果課程中心選課異常,那麼將無法發送消息到訂單中心標記完成任務,訂單中心依舊會發送消息給課程中心,直到成功了爲止,(需要注意的是,課程選課需要實現冪等性)

該思路是沒有問題的,不會出現數據丟失的情況


Spring Task定時任務

根據分佈式事務的研究結果,訂單服務需要定時掃描任務表向MQ發送任務。本節研究定時任務處理的方案,並實
現定時任務掃描任務表並向MQ發送消息.

實現定時任務的方案如下:

1、使用jdk的Timer和TimerTask實現:可以實現簡單的間隔執行任務,無法實現按日曆去調度執行任務。

2、使用Quartz實現Quartz 是一個異步任務調度框架,功能豐富,可以實現按日曆調度。

3、使用Spring Task實現Spring 3.0後提供Spring Task實現任務調度,支持按日曆調度,相比Quartz功能稍簡單,但是在開發基本夠用,支持註解編程方式。

 

選擇spring Task,別問我爲什麼,spring忠實粉絲,哈哈哈

 


訂單服務定時發送消息

定時任務發送消息流程如下:

1、每隔1分鐘掃描一次任務表。一次取出多個任務,取出超過1分鐘未處理的任務

2、考慮訂單服務可能集羣部署,爲避免重複發送任務使用樂觀鎖的方式每次從任務列表取出要處理的任務3、任務發送完畢更新任務發送時間

 

 

RabbitMQ配置

聲明訂單交換機,綁定倆個隊列,並設置路由規則

package com.xuecheng.order.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    //添加選課任務交換機
    public static final String EX_LEARNING_ADDCHOOSECOURSE = "ex_learning_addchoosecourse";

    //完成添加選課消息隊列
    public static final String XC_LEARNING_FINISHADDCHOOSECOURSE = "xc_learning_finishaddchoosecourse";

    //添加選課消息隊列
    public static final String XC_LEARNING_ADDCHOOSECOURSE = "xc_learning_addchoosecourse";


    //添加選課路由key
    public static final String XC_LEARNING_ADDCHOOSECOURSE_KEY = "addchoosecourse";
    //完成添加選課路由key
    public static final String XC_LEARNING_FINISHADDCHOOSECOURSE_KEY = "finishaddchoosecourse";

    /**
     * 交換機配置
     * @return the exchange
     */
    @Bean(EX_LEARNING_ADDCHOOSECOURSE)   //訂單交換機
    public Exchange EX_DECLARE() {
        return ExchangeBuilder.directExchange(EX_LEARNING_ADDCHOOSECOURSE).durable(true).build();
    }
    //聲明隊列
    @Bean(XC_LEARNING_FINISHADDCHOOSECOURSE)  //完成選課隊列
    public Queue QUEUE_finishaddchoose() {
        Queue queue = new Queue(XC_LEARNING_FINISHADDCHOOSECOURSE,true,false,true);
        return queue;
    }


    @Bean(XC_LEARNING_ADDCHOOSECOURSE)  //完成選課隊列
    public Queue QUEUE_addchoose() {
        Queue queue = new Queue(XC_LEARNING_ADDCHOOSECOURSE,true,false,true);
        return queue;
    }
    /**
     * 綁定隊列到交換機 .
     * @param queue    the queue
     * @param exchange the exchange
     * @return the binding
     */
    @Bean    //完成選課隊列綁定交換機
    public Binding BINDING_QUEUE_FINISHADDCHOOSECOURSE(@Qualifier(XC_LEARNING_FINISHADDCHOOSECOURSE) Queue queue, @Qualifier(EX_LEARNING_ADDCHOOSECOURSE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(XC_LEARNING_FINISHADDCHOOSECOURSE_KEY).noargs();
    }

    @Bean    //添加選課隊列綁定交換機
    public Binding BINDING_QUEUE_ADDCHOOSECOURSE(@Qualifier(XC_LEARNING_ADDCHOOSECOURSE) Queue queue, @Qualifier(EX_LEARNING_ADDCHOOSECOURSE) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(XC_LEARNING_ADDCHOOSECOURSE_KEY).noargs();
    }

}

 

 

Repository

在XcTaskRepository中自定義方法如下: 使用的是jpa,查詢發佈一分鐘後的任務,,,

import com.github.pagehelper.Page;
import com.xuecheng.framework.domain.task.XcTask;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Date;

public interface XcTaskRepository extends JpaRepository<XcTask,String> {

//    查詢1分鐘前的任務
    Page<XcTask> findByUpdateTimeBefore(Pageable pageable, Date updateTime);

   @Modifying  //使用樂觀鎖,放着多個服務器搶到同一個任務
    @Query("update XcTask t set t.updateTime = :updateTime where t.id = :id ")
    public int updateTaskTime(@Param(value = "id") String id,@Param(value =        "updateTime")Date updateTime);
}

 

Service


import com.github.pagehelper.Page;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.order.dao.XcTaskRepository;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Optional;

@Service
public class TaskService {

    @Autowired
    XcTaskRepository xcTaskRepository;


    @Autowired
    RabbitTemplate rabbitTemplate;

//    取出前n條記錄
    public List<XcTask> findTaskList(int n, Date updateTime){

        //設置分頁參數,取出前n 條記錄
        Pageable pageable = new PageRequest(0, n);

        Page<XcTask> xcTasks = xcTaskRepository.findByUpdateTimeBefore(pageable, updateTime);
        return xcTasks.getResult();
    }


    public void publish(XcTask xcTask,String ex,String routingKey){

        Optional<XcTask> optional = xcTaskRepository.findById(xcTask.getId());//效驗任務是否存在

        if(optional.isPresent()){
            XcTask one = optional.get();
            rabbitTemplate.convertAndSend(ex,routingKey ,one);

            //更新任務時間爲當前時間
            one.setUpdateTime(new Date());
            xcTaskRepository.save(one);
        }


    }
}

 

 

發送選課消息 任務類

rabbitmq

package com.xuecheng.order.scheduling;

import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.order.config.RabbitMQConfig;
import com.xuecheng.order.service.TaskService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

@Component
public class ChooseCourseTask {

    @Resource
    TaskService taskService;


    @Scheduled(fixedDelay = 6000)
    public void sendChoosecourseTask(){

//        取出當前時間一分鐘錢的時間
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(new Date());
        calendar.set(GregorianCalendar.MINUTE, -1);  //前一分鐘的記錄

        Date time = calendar.getTime();

        List<XcTask> taskList = taskService.findTaskList(1000, time);


        //發送任務
        for (XcTask xcTask : taskList) {
            taskService.publish(xcTask, RabbitMQConfig.EX_LEARNING_ADDCHOOSECOURSE, RabbitMQConfig.XC_LEARNING_ADDCHOOSECOURSE_KEY);
        }
    }
}

 

 

考慮訂單服務將來會集羣部署,爲了避免任務在1分鐘內重複執行,這裏使用樂觀鎖,實現思路如下:
1) 每次取任務時判斷當前版本及任務id是否匹配,如果匹配則執行任務,如果不匹配則取消執行。
2) 如果當前版本和任務Id可以匹配到任務則更新當前版本加1

1、在Dao中增加校驗當前版本及任務id的匹配方法

//使用樂觀鎖方式校驗任務id和版本號是否匹配,匹配則版本號加1
@Modifying@Query("update XcTask t set t.version = :version+1 where t.id = :id and t.version =:version")
public int updateTaskVersion(@Param(value = "id") String id,@Param(value = "version") int version);

 

2.在service中增加方法,使用樂觀鎖方法校驗任務

  public int  getTask(XcTask task){
        return xcTaskRepository.updateTaskVersion(task.getId(), task.getVersion());
    }

3、執行任務類中修改

package com.xuecheng.order.scheduling;

import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.order.config.RabbitMQConfig;
import com.xuecheng.order.service.TaskService;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

@Component
public class ChooseCourseTask {

    @Resource
    TaskService taskService;


    @Scheduled(fixedDelay = 6000)
    public void sendChoosecourseTask(){

//        取出當前時間一分鐘錢的時間
        Calendar calendar = new GregorianCalendar();
        calendar.setTime(new Date());
        calendar.set(GregorianCalendar.MINUTE, -1);  //前一分鐘的記錄

        Date time = calendar.getTime();

        List<XcTask> taskList = taskService.findTaskList(1000, time);


        //發送任務
        for (XcTask xcTask : taskList) {

//          使用樂觀鎖,確認是否發送信息
            if (taskService.getTask(xcTask) > 0){
                //   數據庫中的 version  跟   task傳入的version一致,才能被更新,
                taskService.publish(xcTask, RabbitMQConfig.EX_LEARNING_ADDCHOOSECOURSE, RabbitMQConfig.XC_LEARNING_ADDCHOOSECOURSE_KEY);
            }
        }
    }
}

 

 

自動選課課程開發

 

需求分析

學習服務接收MQ發送添加選課消息,執行添加 選 課操作。
添加選課成功向學生選課表插入記錄、向歷史任務表插入記錄、並向MQ發送“完成選課”消息。

 

Dao

@Repository
public interface XcLearningCourseRepository extends JpaRepository<XcLearningCourse,String> {
    //根據用戶和課程查詢選課記錄,用於判斷是否添加選課
    XcLearningCourse findXcLearningCourseByUserIdAndCourseId(String userId, String courseId);


}



@Repository//歷史任務dao
public interface XcTaskHisRepository extends JpaRepository<XcTaskHis,String> {
}

 

Service

 

1、添加選課方法

向xc_learning_course添加記錄,爲保證不重複添加選課,先查詢歷史任務表,如果從歷史任務表查詢不到任務說明此任務還沒有處理,此時則添加選課並添加歷史任務。

package com.xuecheng.learning.service;

import com.xuecheng.framework.domain.learning.XcLearningCourse;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.framework.domain.task.XcTaskHis;
import com.xuecheng.framework.model.response.CommonCode;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.learning.dao.XcLearningCourseRepository;
import com.xuecheng.learning.dao.XcTaskHisRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;

//import com.xuecheng.framework.domain.learning.respones.GetMediaResult;
//import com.xuecheng.framework.domain.learning.respones.LearningCode;

/**
 * @author Administrator
 * @version 1.0
 **/
@Service
public class LearningService {

    @Autowired
    XcTaskHisRepository xcTaskHisRepository;

    @Autowired
    XcLearningCourseRepository xcLearningCourseRepository;

    @Transactional
    public ResponseResult addCourse(String userId, String courseId, String valid, Date
            startTime, Date endTime, XcTask xcTask){

        if (StringUtils.isBlank(userId)  ||StringUtils.isBlank(courseId) ){
//            異常處理
            return new ResponseResult(CommonCode.FAIL);
        }

        if(xcTask == null || StringUtils.isEmpty(xcTask.getId())){
            return new ResponseResult(CommonCode.FAIL);   //假裝有很認真處理
        }

        //效驗任務是否存在
        XcTaskHis one = xcTaskHisRepository.getOne(xcTask.getId());

        if (one != null){
            //已經執行,無需執行
            return new ResponseResult(CommonCode.SUCCESS);
        }

        XcLearningCourse xcLearningCourse = xcLearningCourseRepository.findXcLearningCourseByUserIdAndCourseId(userId, courseId);//效驗用戶是否插入過該課程


        if (xcLearningCourse != null){//選課記錄存在,修改狀態及基本信息
            xcLearningCourse.setValid(valid);
            xcLearningCourse.setStartTime(startTime);
            xcLearningCourse.setEndTime(endTime);
            xcLearningCourse.setStatus("501001");
            xcLearningCourseRepository.save(xcLearningCourse);
        }else {
            //插入課程記錄

            xcLearningCourse = new XcLearningCourse();
            xcLearningCourse.setUserId(userId);
            xcLearningCourse.setCourseId(courseId);
            xcLearningCourse.setValid(valid);
            xcLearningCourse.setStartTime(startTime);
            xcLearningCourse.setEndTime(endTime);
            xcLearningCourse.setStatus("501001");
            xcLearningCourseRepository.save(xcLearningCourse);
        }


//        像歷史記錄表插入記錄
        XcTaskHis xcTaskHis = new XcTaskHis();

        BeanUtils.copyProperties(xcTask, xcTaskHis);
        xcTaskHisRepository.save(xcTaskHis);
        return new ResponseResult(CommonCode.SUCCESS);
    }

}

 

 

 

接收添加選課消息

接收到添加選課的消息調用添加選課方法完成添加選課,併發送完成選課消息。


import com.alibaba.fastjson.JSON;
import com.xuecheng.framework.domain.task.XcTask;
import com.xuecheng.framework.model.response.ResponseResult;
import com.xuecheng.learning.config.RabbitMQConfig;
import com.xuecheng.learning.service.LearningService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

@Component
public class ChooseCourseTask {

    private static final Logger LOGGER = LoggerFactory.getLogger(ChooseCourseTask.class);

    @Autowired
    LearningService learningService;
    @Autowired
    RabbitTemplate rabbitTemplate;


    @RabbitListener(queues = {RabbitMQConfig.XC_LEARNING_ADDCHOOSECOURSE})
    public void receiveChoosecourseTask(XcTask xcTask){

        //接收到 的消息id
        String id = xcTask.getId();

        try {
            //參數封裝,轉換
            String requestBody = xcTask.getRequestBody();//JSON數據,轉換成Map
            Map map = JSON.parseObject(requestBody, Map.class);
            String userId = (String) map.get("userId");
            String courseId = (String) map.get("courseId");
            String valid = (String) map.get("valid");

            Date startTime =  null;
            Date endTime = null;

            SimpleDateFormat dateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:dd");
            if(map.get("startTime")!=null){
                startTime = dateFormat.parse((String) map.get("startTime"));
            }
            if(map.get("endTime")!=null){
                endTime =dateFormat.parse((String) map.get("endTime"));
            }


            //添加選課
            ResponseResult addCourse = learningService.addCourse(userId, courseId, null, startTime, endTime, xcTask);


            if (addCourse.isSuccess()){
                //執行成功.....

//                發送mq到訂單中心,響應完成
                rabbitTemplate.convertAndSend(RabbitMQConfig.EX_LEARNING_ADDCHOOSECOURSE, RabbitMQConfig.XC_LEARNING_FINISHADDCHOOSECOURSE_KEY, xcTask);
            }
        }catch (Exception e){
            LOGGER.error("send finish choose course taskId:{}", id);
        }
    }
}

 

訂單服務結束任務

訂單服務接收MQ完成選課的消息,將任務從當前任務表刪除,將完成的任務添加到完成任務表。

 

service

 

   //刪除任務
    @Transactional
    public void finishTask(String taskId){
        Optional<XcTask> taskOptional = xcTaskRepository.findById(taskId);


        if (taskOptional.isPresent()){
            XcTask xcTask = taskOptional.get();
            //任務歷史記錄
            XcTaskHis xcTaskHis = new XcTaskHis();
            BeanUtils.copyProperties(xcTask, xcTaskHis);
//            保存歷史記錄

            xcTaskHlsRepository.save(xcTaskHis);
            xcTaskRepository.deleteById(xcTask.getId());
        }

}

 

 

最後用mq 監聽一下,調用方法就行啦

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