前言
技術棧使用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生成的插件
http://mybatis.org/generator/configreference/xmlconfig.html
- 配置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 < #{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);
}
- 測試