【30分鐘未支付訂單自動取消】SpringBoot整合Elastic-Job分佈式定時任務框架

前言

技術棧使用SpringBoot+Mybatis+ElasticJob


一、設計

1.方案設計

- 被動取消(查詢訂單,判斷是否超時,更改訂單狀態)

在這裏插入圖片描述

- 延遲隊列(隊列中的消息會延遲一定時間傳遞給消費者)

在這裏插入圖片描述

- 定時輪詢(每隔1分鐘查詢30分鐘未支付訂單,更改狀態)

在這裏插入圖片描述

2.數據庫設計

在這裏插入圖片描述
在這裏插入圖片描述


CREATE TABLE `order` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '訂單id',
  `amount` decimal(10,2) NOT NULL COMMENT '訂單價格',
  `status` int NOT NULL DEFAULT '1' COMMENT '訂單狀態:(1未支付,2已支付,3已取消)',
  `receive_name` varchar(10) NOT NULL COMMENT '收穫人姓名',
  `receive_address` varchar(50) NOT NULL COMMENT '收穫人地址',
  `receive_mmobile` varchar(11) NOT NULL COMMENT '收貨人手機號',
  `create_user` varchar(10) NOT NULL COMMENT '創建人',
  `create_time` datetime NOT NULL COMMENT '創建時間',
  `update_user` varchar(10) NOT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL COMMENT '更新時間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

二、項目搭建

- 創建項目

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

- 創建配置類

在這裏插入圖片描述


package com.zcw.autoconfig;

import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperConfiguration;
import com.dangdang.ddframe.job.reg.zookeeper.ZookeeperRegistryCenter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName : ZookeeperAutoConfig
 * @Description :自動配置類
 * @Author : Zhaocunwei
 * @Date: 2020-06-06 09:21
 */
@Configuration
@ConditionalOnProperty("elasticjob.zookeeper.server-list")
@EnableConfigurationProperties(ZookeeperProperties.class)
public class ZookeeperAutoConfig {

    private final ZookeeperProperties zookeeperProperties;

    public ZookeeperAutoConfig(ZookeeperProperties zookeeperProperties) {
        this.zookeeperProperties = zookeeperProperties;
    }

    @Bean(initMethod = "init")
    public CoordinatorRegistryCenter zkCenter(){
        String serverList = zookeeperProperties.getServerlist();
        String namespace = zookeeperProperties.getNamespace();

        ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(serverList, namespace);
        ZookeeperRegistryCenter zookeeperRegistryCenter = new ZookeeperRegistryCenter(zookeeperConfiguration);
        return zookeeperRegistryCenter;
    }
}


package com.zcw.autoconfig;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @ClassName : ZookeeperProperties
 * @Description :屬性配置類
 * @Author : Zhaocunwei
 * @Date: 2020-06-06 09:22
 */
@Getter
@Setter
@ConfigurationProperties(prefix = "elasticjob.zookeeper")
public class ZookeeperProperties {
    //zookeeper地址列表
    private String serverlist;
    //zookeeper命名空間
    private String namespace;

}


- 啓動類 定時任務

在這裏插入圖片描述

package com.zcw.autoconfig;

import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.dangdang.ddframe.job.config.JobCoreConfiguration;
import com.dangdang.ddframe.job.config.simple.SimpleJobConfiguration;
import com.dangdang.ddframe.job.lite.api.JobScheduler;
import com.dangdang.ddframe.job.lite.config.LiteJobConfiguration;
import com.dangdang.ddframe.job.reg.base.CoordinatorRegistryCenter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.util.Map;

/**
 * @ClassName : SimpleJobAutoConfig
 * @Description :
 * @Author : Zhaocunwei
 * @Date: 2020-06-06 09:21
 */
@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {

    @Autowired
    private CoordinatorRegistryCenter coordinatorRegistryCenter;
    @Autowired
    private ApplicationContext applicationContext;

    //自動註冊
    @PostConstruct
    public void initSimpleJob(){
        //獲取spring的上下文
        Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(ElasticSimpleJob.class);
        for(Map.Entry<String,Object> entry: beansWithAnnotation.entrySet()){
            Object instance = entry.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if(superInterface == SimpleJob.class){
                    ElasticSimpleJob annotation = instance.getClass().getAnnotation(ElasticSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite =annotation.overwrite();
                    //註冊定時任務
                    //job 核心配置
                    JobCoreConfiguration buildJcc = JobCoreConfiguration
                            .newBuilder(jobName, cron, shardingTotalCount)
                            .build();
                    //job類型配置
                    SimpleJobConfiguration simpleJobConfiguration = new SimpleJobConfiguration(
                            buildJcc, instance.getClass().getCanonicalName()
                    );
                    // job配置(LiteJobConfiguration)
                    LiteJobConfiguration buildLiteJobConfiguration = LiteJobConfiguration
                            .newBuilder(simpleJobConfiguration)
                            .overwrite(overwrite)
                            .build();

                    //啓動
               new SpringJobScheduler((ElasticJob) instance,coordinatorRegistryCenter,buildLiteJobConfiguration).init();

                }
            }
        }
    }
}



package com.zcw.springbootelasticjob;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.zcw.springbootelasticjob.dao")
public class ZcwSpringbootElasticjobApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZcwSpringbootElasticjobApplication.class, args);
    }

}


- 自定義註解

在這裏插入圖片描述

package com.zcw.autoconfig;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)//表示使用在哪:這裏是類,
@Retention(RetentionPolicy.RUNTIME)//表示運行時進行啓動
public @interface ElasticSimpleJob {
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default  false;
}


- job
package com.zcw.springbootelasticjob.job;

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.zcw.autoconfig.ElasticSimpleJob;
import com.zcw.springbootelasticjob.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @ClassName : MySimpleJob
 * @Description :
 * @Author : Zhaocunwei
 * @Date: 2020-06-06 09:18
 */
@Slf4j
@ElasticSimpleJob(jobName = "mySimpleJob",
        cron = "0/5 * * * * ?",
        shardingTotalCount = 1,
        overwrite = true)
@Component
public class MySimpleJob implements SimpleJob {
    @Autowired
    private OrderService orderService;
    @Override
    public void execute(ShardingContext shardingContext) {
        log.info("我是分片項:"+shardingContext.getShardingItem()+",總分片數是:"+
                shardingContext.getShardingTotalCount());
        //模擬創建訂單方法:
        for(int i=0;i<10;i++){
            orderService.insertOrder();
        }
    }
}



- job自動映射類

在這裏插入圖片描述


org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.zcw.autoconfig.ZookeeperAutoConfig,\
com.zcw.autoconfig.SimpleJobAutoConfig

- 修改pom,引入Mybatis的Starter依賴
<?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.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.zcw</groupId>
    <artifactId>zcw-springboot-elasticjob</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>zcw-springboot-elasticjob</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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>
        <!--elasic-job-->
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>elastic-job-lite-core</artifactId>
            <version>2.1.5</version>
        </dependency>
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>elastic-job-lite-spring</artifactId>
            <version>2.1.5</version>
        </dependency>
        <!--添加配置文件的註解類-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--引入mybatis生成器-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
            </plugin>
        </plugins>
    </build>


</project>


- 配置Mybatis生成文件,生成數據庫對應實體

在這裏插入圖片描述
在這裏插入圖片描述


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

    <context id="MysqlTables" targetRuntime="MyBatis3">
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/order?serverTimezone=Asia/Shanghai"
                        userId="root"
                        password="root">
            <property name="nullCatalogMeansCurrent" value="true"/>
        </jdbcConnection>

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.zcw.springbootelasticjob.model" targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="mybatis"  targetProject="src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.zcw.springbootelasticjob.dao"  targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <table schema="order" tableName="order" domainObjectName="Order" >

        </table>

    </context>
</generatorConfiguration>


- 配置數據源
  • 配置Mybatis的xml文件路徑
  • 配置註解掃描Mybatis接口類
server.port=8074
elasticjob.zookeeper.namespace=springboot-elasticjob
elasticjob.zookeeper.server-list=localhost:2181


spring.datasource.username=root
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/order?serverTimezone=Asia/Shanghai&useSSL=false

mybatis.mapper-locations=/mybatis/*.xml





package com.zcw.springbootelasticjob;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.zcw.springbootelasticjob.dao")
public class ZcwSpringbootElasticjobApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZcwSpringbootElasticjobApplication.class, args);
    }

}


- 編寫Controller
- 編寫service
package com.zcw.springbootelasticjob.service;

import com.zcw.springbootelasticjob.dao.OrderMapper;
import com.zcw.springbootelasticjob.model.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

/**
 * @ClassName : OrderService
 * @Description :
 * @Author : Zhaocunwei
 * @Date: 2020-06-05 18:36
 */
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    public int insertOrder(){
        Order order = new Order();
        order.setAmount(BigDecimal.TEN);
        order.setReceiveName("zcw");
        order.setReceiveAddress("中國北京朝陽區xxxxxx");
        order.setReceiveMmobile("1381231401");
        order.setStatus(1);
        order.setCreateTime(new Date());
        order.setCreateUser("zcw");
        order.setUpdateTime(new Date());
        order.setUpdateUser("zcw");
        return orderMapper.insertSelective(order);
    }
}


- 測試項目
package com.zcw.springbootelasticjob;

import com.zcw.springbootelasticjob.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
class ZcwSpringbootElasticjobApplicationTests {
   @Autowired
   private OrderService orderService;
    @Test
    void contextLoads() {
    }

   @Test
    public void testOrder(){
        orderService.insertOrder();
    }
}


在這裏插入圖片描述

- 模擬訂單生成過程

在這裏插入圖片描述
在這裏插入圖片描述

- 編寫創建訂單方法
  • 配置定時任務每5秒執行一次
    在這裏插入圖片描述
- 編寫超時訂單SQL
- 使用多線程取消訂單

在這裏插入圖片描述

  <select id="getOrder" resultType="com.zcw.springbootelasticjob.model.Order">
      select
      <include refid="Base_Column_List"/>
      from t_order
      <where>
        create_time &lt; #{param1}
        and status =1
        and id% #{param2} =#{param3}
      </where>
    </select>

在這裏插入圖片描述

package com.zcw.springbootelasticjob.dao;

import com.zcw.springbootelasticjob.model.Order;
import com.zcw.springbootelasticjob.model.OrderExample;

import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;

public interface OrderMapper {
    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    long countByExample(OrderExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int deleteByExample(OrderExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int deleteByPrimaryKey(Integer id);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int insert(Order record);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int insertSelective(Order record);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    List<Order> selectByExample(OrderExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    Order selectByPrimaryKey(Integer id);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int updateByExampleSelective(@Param("record") Order record, @Param("example") OrderExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int updateByExample(@Param("record") Order record, @Param("example") OrderExample example);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int updateByPrimaryKeySelective(Order record);

    /**
     * This method was generated by MyBatis Generator.
     * This method corresponds to the database table t_order
     *
     * @mbg.generated Fri Jun 05 19:36:47 CST 2020
     */
    int updateByPrimaryKey(Order record);

    List<Order> getOrder(Date time, int shardingTotalCount, int shardingItem);
}

在這裏插入圖片描述


 public List<Order> getOrder(Calendar now, int shardingTotalCount, int shardingItem) {
        return orderMapper.getOrder(now.getTime(),shardingTotalCount,shardingItem);
    }

在這裏插入圖片描述

  • Job

  • Junit測試
    在這裏插入圖片描述
    在這裏插入圖片描述

- 使用樂觀鎖實現取消訂單業務

在這裏插入圖片描述

 <update id="cancelOrder">
      update t_order set
      status= #{param3},
      update_user = #{param4},
      update_time=#{param5}
      <where>
        id=#{param1}
        and update_time=#{param2}
      </where>
    </update>

在這裏插入圖片描述

    int cancelOrder(Integer orderId, Date updateTime, int status, String updateUser, Date updateNow);


在這裏插入圖片描述

- 使用@ElasticSimpleJob配置分片總數,定時規則
  • 上截圖最終優化後的代碼爲:

package com.zcw.springbootelasticjob.job;

import com.dangdang.ddframe.job.api.ShardingContext;
import com.dangdang.ddframe.job.api.simple.SimpleJob;
import com.zcw.autoconfig.ElasticSimpleJob;
import com.zcw.springbootelasticjob.model.Order;
import com.zcw.springbootelasticjob.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName : OrderCancelJob
 * @Description : 訂單取消Job
 * @Author : Zhaocunwei
 * @Date: 2020-06-06 10:18
 */
@ElasticSimpleJob(
      jobName = "orderCancelJob",
        cron = "0/15 * * * * ?",
        shardingTotalCount = 2,
        overwrite = true
)
@Component
public class OrderCancelJob implements SimpleJob {
    @Autowired
    private OrderService orderService;
    @Override
    public void execute(ShardingContext shardingContext) {
        //查詢出,三十秒之前,沒有支付的訂單,
        //首先查詢出當前時間
        Calendar now  = Calendar.getInstance();
        now.add(Calendar.SECOND,-30);
        //使用分片項,檢索符合這個分片項規則的訂單
        //訂單尾號% 分片總數== 當前分片項
        List<Order> orders = orderService.getOrder(now,
                shardingContext.getShardingTotalCount(),
                shardingContext.getShardingItem());
        //使用多線程進行取消訂單
        if(orders !=null && orders.size()>0){
            ExecutorService es = Executors.newFixedThreadPool(4);
            for(Order order:orders){
                es.execute(()->{
                    Integer orderId = order.getId();
                    //更新時間,在這裏當樂觀鎖進行使用
                    /**
                     * 是因爲存在這樣的場景,當我取出訂單時,用戶已經支付了
                     * 所以我們需要添加更新時間,防止出現把已支付的訂單,
                     * 給取消了。
                     */
                    Date updateTime = order.getUpdateTime();

                    int status = 3;//表示取消
                    String updateUser ="system";
                    Date updateNow = new Date();

                    orderService.cancelOrder(orderId,updateTime,status,updateUser,updateNow);
                });

            }
            es.shutdown();
        }
    }
}


在這裏插入圖片描述

  public void cancelOrder(Integer orderId, Date updateTime,
                            int status, String updateUser, Date updateNow) {
        orderMapper.cancelOrder(orderId,updateTime,status,updateUser,updateNow);
    }

在這裏插入圖片描述

  • 測試
    在這裏插入圖片描述
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章