云商城-商品秒杀-数据处理
第9章 秒杀数据处理
课程目标
1、秒杀设计
1)秒杀业务设计
2)秒杀架构设计
3)秒杀表结构设计
2、活动管理
1)活动分析
2)有效活动列表查询
3、搜索管理
1)搜索业务分析
2)搜索索引实时导入
3)秒杀搜索实现
4、商品详情处理
1)搜索详情页生成
5、 数据同步
1)静态页同步
2)索引同步
1 秒杀设计
1.1 秒杀业务


1.2 秒杀业务特点介绍
秒杀业务是互联网公司电商项目中的标志性业务,是典型的高并发场景。秒杀业务主要的问题是大量用户短时间内涌入,导致瞬时流量巨大,对于数据库、缓存的性能是一个巨大考验。
1)高并发
我们通常衡量一个服务器的吞吐率的指标是QPS(每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。
假设处理一个业务请求平均响应时间为 100 ms,同时,系统内有 20 台 服务器,配置最大连接数为 500 个,Web 系统的理论峰值QPS为(理想化的计算方式):100000 (10万QPS)意味着1 秒钟可以处理完 10 万的请求,而“秒杀”的那 5w/s 的秒杀似乎是“纸老虎”。
然而实际情况,在高并发的实际场景下,服务器处于高负载的状态,网络带宽被挤满,在这个时候平均响应时间会被大大增加。随着用户数量的增加,数据库连接进程增加,需要处理的上下文切换也越多,造成服务器负载越来越重。
2)业务耦合高
秒杀最大的问题是,当系统上某个应用因为延迟而变得不可用,用户的点击越频繁,恶性循环最终导致“雪崩”,因为其中一台服务器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环,将整个系统拖垮。
3)冷热商品处理
秒杀过程中并发量极高被抢购的商品其实并不多,都是大家所熟知的一些品牌和商品,针对这些商品我们应该和抢购人数并不是特别多的商品进行区分,热门商品走排队系统,非热门商品直接抢单即可。
在秒杀过程中,还会出现一些特殊现象,例如一直都是冷门的商品口罩在2020年突然变成全球热门产品,流量突然增长的产品,我们系统要具备发现能力。

1.3 秒杀架构设计

针对秒杀抢单业务的架构设计,不能和普通的商品抢单一样,架构应该考虑热门商品造成的并发、冷门商品能有可用的系统资源、波动商品走向热门的发现功能、高并发抢单超卖问题解决等,如上图架构设计:
1:用户抢单的时候,会将用户抢购的商品信息存入到大数据分析系统Apache Druid。
2:抢单信息存入到Apache Druid后,进行实时分析,访问频率高的商品将被存入到Redis缓存单独隔离。
3:用户抢单先经过Nginx,Nginx做了限流后,请求会到达Gateway集群。
4:Gateway集群会判断当前商品是否是热门商品。
5:如果商品是热门商品,在Gateway中会进行排队(MQ)下单,此时抢单流程结束。如果是非热门商品,直接下单即可。
6:普通订单直接下单,热门商品下单需要读取排队信息(MQ)进行下单,下单后将抢单信息通知到前端(WebSocket)。
1.4 秒杀表结构
上面我们分析过秒杀业务,我们接下来对秒杀业务的表结构进行分析。
商品表:
CREATE TABLE `seckill_goods` (
`id` varchar(60) NOT NULL,
`sup_id` varchar(60) DEFAULT NULL COMMENT 'spu ID',
`sku_id` varchar(60) DEFAULT NULL COMMENT 'sku ID',
`name` varchar(100) DEFAULT NULL COMMENT '标题',
`images` varchar(150) DEFAULT NULL COMMENT '商品图片',
`price` int(20) DEFAULT NULL COMMENT '原价格',
`seckill_price` int(20) DEFAULT NULL COMMENT '秒杀价格',
`num` int(11) DEFAULT NULL COMMENT '秒杀商品数',
`store_count` int(11) DEFAULT NULL COMMENT '剩余库存数',
`content` varchar(2000) DEFAULT NULL COMMENT '描述',
`activity_id` varchar(60) DEFAULT NULL COMMENT '活动ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
秒杀订单表:
CREATE TABLE `seckill_order` (
`id` varchar(60) NOT NULL COMMENT '主键',
`seckill_goods_id` varchar(60) DEFAULT NULL COMMENT '秒杀商品ID',
`money` int(10) DEFAULT NULL COMMENT '支付金额',
`username` varchar(50) DEFAULT NULL COMMENT '用户',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`pay_time` datetime DEFAULT NULL COMMENT '支付时间',
`status` int(1) DEFAULT NULL COMMENT '状态,0未支付,1已支付',
`weixin_transaction_id` varchar(30) DEFAULT NULL COMMENT '交易流水',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
活动表:
CREATE TABLE `seckill_activity` (
`id` varchar(60) NOT NULL,
`activity_name` varchar(60) NOT NULL COMMENT '活动名字',
`type` int(1) NOT NULL COMMENT '活动分类 0 shop秒杀、1 每日特价、2 大牌闪购 、 3 品类秒杀 、 4 节日活动',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2 工程搭建
我们需要搭建秒杀工程,实现秒杀功能的操作。
2.1 Api工程创建
搭建mall-seckill-api工程,并创建对应实体Bean
坐标如下:
<modelVersion>4.0.0</modelVersion>
<version>0.0.1-SNAPSHOT</version>
<artifactId>mall-seckill-api</artifactId>
创建com.gupaoedu.vip.mall.seckill.model.SeckillActivity
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "seckill_activity")
public class SeckillActivity {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String activityName;
private Integer type; //活动分类 0shop秒杀、1 每日特价、2 大牌闪购、3 品类秒杀、4 节日活动
private Date startTime;
private Date endTime;
}
创建com.gupaoedu.vip.mall.seckill.model.SeckillGoods
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "seckill_goods")
public class SeckillGoods {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String supId;
private String skuId;
private String name;
private String images;
private String content;
private Integer price;
private Integer seckillPrice;
private Integer num;
private Integer storeCount;
private Date createTime;
private String activityId;
}
创建com.gupaoedu.vip.mall.seckill.model.SeckillOrder
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "seckill_order")
public class SeckillOrder {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String seckillGoodsId;
private String weixinTransactionId;
private String username;
private Integer money;
private Integer status;
private Date createTime;
private Date payTime;
private Integer num; //抢购数量
}
2.2 Service工程搭建
创建mall-seckill-service工程
1)pom.xml
<dependencies>
<dependency>
<groupId>com.gupaoedu.vip.mall</groupId>
<artifactId>mall-seckill-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
2)bootstrap.yml
server:
port: 8092
spring:
application:
name: mall-seckill
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.100.130:3306/shop_seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
cloud:
nacos:
config:
file-extension: yaml
server-addr: 192.168.100.130:8848
discovery:
#Nacos的注册地址
server-addr: 192.168.100.130:8848
# ====================MybatisPlus====================
mybatis-plus:
mapper-locations: mapper/*.xml
type-aliases-package: com.gupaoedu.vip.mall.*.model
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#日志配置
logging:
pattern:
console: "%msg%n"
3)启动类
创建启动类com.gupaoedu.vip.mall.seckill.SeckillApplication
@SpringBootApplication
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class,args);
}
}
4)Service/dao
将秒杀\seckill-service类中创建好的空Service和Dao引入到工程中,如下图:

3 活动管理
活动管理包括每日秒杀和节假日秒杀,但无论是什么活动,基本都是一件商品不会重复参与秒杀活动,我们先对秒杀的活动做一个分析。
3.1 秒杀活动分析

秒杀活动有很多种,并非只有大家平时看到的双十一,其实每天都有秒杀活动,但我们设计秒杀活动表结构应该满足不同类型的秒杀活动,我们再来看下秒杀活动表结构:
CREATE TABLE `seckill_activity` (
`id` varchar(60) NOT NULL,
`activity_name` varchar(60) NOT NULL COMMENT '活动名字',
`type` int(1) NOT NULL COMMENT '活动分类 0 shop秒杀、1 每日特价、2 大牌闪购 、 3 品类秒杀 、 4 节日活动',
`start_time` datetime NOT NULL COMMENT '开始时间',
`end_time` datetime NOT NULL COMMENT '结束时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
**活动时间:**秒杀有活动开始时间和结束时间,这两个时间可以灵活设计,可以设计到商品里面,也可以设计到活动里面,我们这里选择设计到活动里面,更符合显示需求,但也有特定的业务需求需要设计到商品里面。
**每日秒杀:**如果是上图的每日秒杀,start_time和end_time就是时间间隔,而type则是活动类型,活动类型并非是商品分类,而是活动分类,分类的值为0,1,2,3均为每日秒杀。
**节假日秒杀:**如果type=4则表示节假日秒杀活动,例如双十一等,节假日秒杀活动并非。
我们这里做shop秒杀功能,因此首先需要根据活动表查询出活动时间,再每次点击一个活动时间加载对应的商品列表即可。
3.2 活动列表

活动列表查询我们需要分析一下如何查询,需要满足如下几个条件:
1:活动未结束、已开始的要查询
2:活动只查询5个出来
3:每个活动下显示对应活动的商品列表
SQL语句:
SELECT * FROM seckill_activity WHERE end_time>NOW() ORDER BY start_time ASC LIMIT 5
3.2.1 活动列表查询
1)Dao
修改com.gupaoedu.vip.mall.seckill.mapper.SeckillActivityMapper添加查询方法,代码如下:
/**
* 有效活动查询
* @return
*/
@Select("SELECT * FROM seckill_activity WHERE end_time>NOW() ORDER BY start_time ASC LIMIT 5")
List<SeckillActivity> validActivity();
2)Service
接口:修改com.gupaoedu.vip.mall.seckill.service.SeckillActivityService添加活动查询方法
//有效活动时间查询
List<SeckillActivity> validActivity();
实现类:修改com.gupaoedu.vip.mall.seckill.service.impl.SeckillActivityServiceImpl添加实现方法
/****
* 有效活动时间查询
* @return
*/
@Override
public List<SeckillActivity> validActivity() {
return seckillActivityMapper.validActivity();
}
3)Controller
修改com.gupaoedu.vip.mall.seckill.controller.SeckillActivityController添加调用方法
/***
* 未过期的活动列表
*/
@GetMapping
public RespResult<List<SeckillActivity>> list(){
//有效的活动时间查询
List<SeckillActivity> list = seckillActivityService.validActivity();
return RespResult.ok(list);
}
3.2.2 有效活动列表测试
访问:http://localhost:8092/activity效果如下:

4 搜索管理
商品搜索我们采用Elasticsearch,因为秒杀商品数据量太大,不建议大量使用Redis缓存。
4.1 业务分析

如上图,首先加载出5个秒杀时间段,当用户点击其中一个时间的时候加载对应的数据,默认加载第一个的数据即可,我们可以采用如下流程实现:

4.2 秒杀索引导入
秒杀数据导入到索引库去,要先创建ES对应的Pojo,再将SeckillGoods转成ES的Pojo存入ES中,我们接下来一步一步实现该操作。
1)ES映射Pojo创建
在mall-search-api中创建com.gupaoedu.vip.mall.search.model.SeckillGoodsEs代码如下:
@Data
@Document(indexName = "shopsearch",type = "seckillgoodses")
public class SeckillGoodsEs implements Serializable {
@Id
private String id;
private String supId;
private String skuId;
@Field(type=FieldType.Text,searchAnalyzer = "ik_smart",analyzer = "ik_smart")
private String name;
private String images;
private Integer price;
private Integer seckillPrice;
private Integer num;
private Integer storeCount;
private Date createTime;
@Field(type=FieldType.Keyword)
private String activityId;
}
2)数据导入
修改mall-search-service在该工程中实现秒杀数据导入到ES,我们首先引入mall-seckill-api
<!--seckill-api-->
<dependency>
<groupId>com.gupaoedu.vip.mall</groupId>
<artifactId>mall-seckill-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
3)Dao
创建com.gupaoedu.vip.mall.search.mapper.SeckillGoodsSearchMapper代码如下:
public interface SeckillGoodsSearchMapper extends ElasticsearchRepository<SeckillGoodsEs,String> {
}
4)Service
接口:创建com.gupaoedu.vip.mall.search.service.SeckillGoodsSearchService并添加导入索引方法
public interface SeckillGoodsSearchService {
/***
* 导入数据到ES中
* @param seckillGoodsEs
*/
void add(SeckillGoodsEs seckillGoodsEs);
}
实现类:创建com.gupaoedu.vip.mall.search.service.impl.SeckillGoodsSearchServiceImpl并实现导入索引方法:
@Service
public class SeckillGoodsSearchServiceImpl implements SeckillGoodsSearchService {
@Autowired
private SeckillGoodsSearchMapper seckillGoodsSearchMapper;
/***
* 导入数据到ES中
* @param seckillGoodsEs
*/
@Override
public void add(SeckillGoodsEs seckillGoodsEs) {
seckillGoodsSearchMapper.save(seckillGoodsEs);
}
}
5)Controller
创建com.gupaoedu.vip.mall.search.controller.SeckillGoodsSearchController实现导入索引调用操作,代码如下:
@RestController
@RequestMapping(value = "/seckill/goods")
public class SeckillGoodsSearchController {
@Autowired
private SeckillGoodsSearchService seckillGoodsSearchService;
/***
* 导入数据到索引库
*/
@PostMapping(value = "/add")
public RespResult add(@RequestBody SeckillGoodsEs seckillGoodsEs){
seckillGoodsSearchService.add(seckillGoodsEs);
return RespResult.ok();
}
}
6)Feign创建
其他服务为了能够同步索引,需要调用上面方法,所以我们需要创建一个Feign接口。
在mall-search-api中创建com.gupaoedu.vip.mall.search.feign.SeckillGoodsSearchFeign代码如下:
@FeignClient(value = "mall-search")
public interface SeckillGoodsSearchFeign {
/***
* 导入数据到索引库
*/
@PostMapping(value = "/seckill/goods/add")
RespResult add(@RequestBody SeckillGoodsEs seckillGoodsEs);
}
4.3 秒杀商品搜索
秒杀商品搜索从页面上看是根据时间搜索,其实是根据活动ID搜索,只是可以根据时间排序。所以我们需要在后台实现根据活动ID搜索商品数据。
1)Dao
修改com.gupaoedu.vip.mall.search.mapper.SeckillGoodsSearchMapper添加根据活动ID搜索商品,代码如下:
//根据ActivityId搜索数据
List<SeckillGoodsEs> searchByActivityId(String acid);
2)Service
接口:修改com.gupaoedu.vip.mall.search.service.SeckillGoodsSearchService添加根据活动ID搜索商品方法,代码如下:
/**
* 根据活动ID搜索
* @param acid
* @return
*/
List<SeckillGoodsEs> search(String acid);
实现类:修改com.gupaoedu.vip.mall.search.service.impl.SeckillGoodsSearchServiceImpl添加根据ID搜索商品的实现方法,代码如下:
/***
* 根据活动ID搜索
* @param acid
* @return
*/
@Override
public List<SeckillGoodsEs> search(String acid) {
return seckillGoodsSearchMapper.searchByActivityId(acid);
}
3)Controller
修改com.gupaoedu.vip.mall.search.controller.SeckillGoodsSearchController添加商品搜索实现控制方法
/****
* 搜索商品数据
*/
@GetMapping(value = "/search")
public RespResult<List<SeckillGoodsEs>> list(@RequestParam("acid")String acid){
//根据活动ID搜索
List<SeckillGoodsEs> seckillGoodsEsList = seckillGoodsSearchService.search(acid);
return RespResult.ok(seckillGoodsEsList);
}
5 商品详情页
秒杀商品详情页访问频率一定非常高,我们将详情页做成生成静态页或者从缓存加载用来提升访问效率。我们这里选择将详情页生成静态页。
5.1 静态页生成实现
我们直接修改之前的mall-page-web工程,在里面创建Controller、Service。静态页生成保存路径存在到D:/pages/seckillitems/。
1)bootstrap.yml配置
在bootstrap.yml中添加如下路径配置:
#秒杀静态页
itemPath: D:/pages/seckillitems/
2)Service
接口:创建com.gupaoedu.vip.mall.page.service.SeckillPageService,并添加生成秒杀静态页方法:
public interface SeckillPageService {
/***
* 生成静态页
*/
void html(String id) throws Exception;
}
实现类:创建com.gupaoedu.vip.mall.page.service.impl.SeckillPageServiceImpl并实现生成静态页方法:
@Service
public class SeckillPageServiceImpl implements SeckillPageService {
@Autowired
private TemplateEngine templateEngine;
@Value("${itemPath}")
private String itemPath;
@Autowired
private SeckillGoodsFeign seckillGoodsFeign;
/****
* 生成静态页
*/
@Override
public void html(String id) throws Exception {
//加载数据
Map<String,Object> dataMap = dataLoad(id);
//创建Thymeleaf容器对象
Context context = new Context();
//设置页面数据模型
context.setVariables(dataMap);
//文件名字 id.html
File dest = new File(itemPath, id + ".html");
PrintWriter writer = new PrintWriter(dest, "UTF-8");
//生成页面
templateEngine.process("item", context, writer);
}
/***
* 加载数据
* @param id
* @return
*/
private Map<String,Object> dataLoad(String id) {
RespResult<SeckillGoods> goodsResp = seckillGoodsFeign.one(id);
//将商品信息存入到Map中
if(goodsResp.getData()!=null){
Map<String,Object> dataMap = new HashMap<String,Object>();
dataMap.put("item",goodsResp.getData());
return dataMap;
}
return null;
}
}
3)Controller
创建com.gupaoedu.vip.mall.page.controller.SeckillPageController实现生成静态页方法调用,代码如下:
@RestController
@RequestMapping(value = "/page")
public class SeckillPageController {
@Autowired
private SeckillPageService seckillPageService;
/***
* 秒杀详情页生成
* @param id
* @return
*/
@GetMapping(value = "/seckill/goods/{id}")
public RespResult page(@PathVariable(value = "id")String id) throws Exception {
//生成静态页
seckillPageService.html(id);
return RespResult.ok();
}
}
5.2 Feign接口实现
其他工程一旦发现数据发生变更,会调用上面方法生成静态页,因此我们需要编写feign接口。
在mall-page-api中创建com.gupaoedu.vip.mall.page.feign.SeckillPageFeign
@FeignClient(value = "mall-web-page")
public interface SeckillPageFeign {
/***
* 秒杀详情页生成
* @param id
* @return
*/
@GetMapping(value = "/page/seckill/goods/{id}")
RespResult page(@PathVariable(value = "id")String id) throws Exception;
}
详情页的模板在秒杀\详情页模板\seckillitem.html,页面内容已经写好,大家可以直接使用。
6 数据同步
数据同步不仅仅是将数据库数据同步到ES中,还要实时更新静态页,我们来分析下实现方案。
6.1 数据同步方案

数据同步方案如上图:
1:秒杀服务更新商品信息到数据库
2:Canal监听数据库变化,数据同步服务消费增量变化
3:数据同步服务根据变化调用搜索服务实现数据同步
4:数据同步服务根据变化调用静态页服务实现静态页同步
6.2 数据同步实现
6.2.1 Canal监听配置
修改canal配置,添加seckill_goods和seckill_activity表的监听,并重启canal,如下图:

6.2.2 同步实现
1)JPA映射配置
修改SeckillGoods添加JPA注解,实现列名和属性名不同映射,代码如下:
@Data
@AllArgsConstructor
@NoArgsConstructor
//MyBatisPlus表映射注解
@TableName(value = "seckill_goods")
@Table
public class SeckillGoods {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@Column(name = "sup_id")
private String supId;
@Column(name = "sku_id")
private String skuId;
private String name;
private String images;
private String content;
private Integer price;
@Column(name = "seckill_price")
private Integer seckillPrice;
private Integer num;
@Column(name = "store_count")
private Integer storeCount;
@Column(name = "activity_id")
private String activityId;
}
修改mall-canal-service添加com.gupaoedu.vip.canal.listener.SeckillGoodsHandler并调用搜索服务行业静态页服务实现静态页生成和索引创建,代码如下:
@CanalTable(value = "seckill_goods")
@Component
public class SeckillGoodsHandler implements EntryHandler<SeckillGoods>{
@Autowired
private SeckillGoodsSearchFeign seckillGoodsSearchFeign;
@Autowired
private SeckillPageFeign seckillPageFeign;
/**
* 增加数据
* @param seckillGoods
*/
@SneakyThrows
@Override
public void insert(SeckillGoods seckillGoods) {
//静态页生成
seckillPageFeign.page(seckillGoods.getId());
//索引生成
seckillGoodsSearchFeign.add(JSON.parseObject(JSON.toJSONString(seckillGoods), SeckillGoodsEs.class));
}
@SneakyThrows
@Override
public void update(SeckillGoods before, SeckillGoods after) {
//静态页生成
seckillPageFeign.page(after.getId());
//索引生成
seckillGoodsSearchFeign.add(JSON.parseObject(JSON.toJSONString(after), SeckillGoodsEs.class));
}
@Override
public void delete(SeckillGoods seckillGoods) {
}
}
7 静态页定时更新

秒杀活动是一个时间段为单位,前面虽然做到了实时更新秒杀商品索引和静态页,但无法做到定时更新秒杀商品详情页和索引,如上图,第1个活动结束的时候,应该要清理00:00-08:00活动的索引数据和静态页,而我们目前做不到,我们需要采用定时器的机制实现。
7.1 elastic-job介绍
当前主流的分布式任务调度:
| feature | quartz | elastic-job | xxl-job | antares | opencron |
|---|---|---|---|---|---|
| 依赖 | mysql | jdk1.7+, zookeeper 3.4.6+ ,maven3.0.4+ | mysql ,jdk1.7+ , maven3.0+ | jdk 1.7+ , redis , zookeeper | jdk1.7+ , Tomcat8.0+ |
| HA | 多节点部署,通过竞争数据库锁来保证只有一个节点执行任务 | 通过zookeeper的注册与发现,可以动态的添加服务器。 支持水平扩容 | 集群部署 | 集群部署 | — |
| 任务分片 | — | 支持 | 支持 | 支持 | — |
| 文档完善 | 完善 | 完善 | 完善 | 文档略少 | 文档略少 |
| 管理界面 | 无 | 支持 | 支持 | 支持 | 支持 |
| 难易程度 | 简单 | 简单 | 简单 | 一般 | 一般 |
| 公司 | OpenSymphony | 当当网 | 个人 | 个人 | 个人 |
| 高级功能 | — | 弹性扩容,多种作业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持 | 弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化 | 任务分片, 失效转移,弹性扩容 , | 时间规则支持quartz和crontab ,kill任务, 现场执行,查询任务运行状态 |
| 使用企业 | 大众化产品,对分布式调度要求不高的公司大面积使用 | 36氪,当当网,国美,金柚网,联想,唯品会,亚信,平安,猪八戒 | 大众点评,运满满,优信二手车,拍拍贷 | — | — |
ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。
ElasticJob 已于 2020 年 5 月 28 日成为 Apache ShardingSphere 的子项目。

7.2 elastic-job静态定时任务
静态定时任务也就是指将定时执行的周期固定,每次都会按照指定时间执行,elasticjob实现该操作比较简单。我们来实现一次该操作。
后面执行定时更新静态页,我们可以在canal服务中执行操作。
1)引入依赖包
<!-- ElasticJobAutoConfiguration自动配置类作用-->
<dependency>
<groupId>com.github.kuhn-he</groupId>
<artifactId>elastic-job-lite-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
2)配置elasticjob注册中心
修改bootstrap.yml配置zookeeper服务地址信息以及注册的命名空间名字:
elaticjob:
zookeeper:
server-lists: 192.168.100.130:2181
namespace: synctask
3)创建作业
创建作业类com.gupaoedu.vip.canal.task.statictask.SyncStaticTask,代码如下:
@ElasticSimpleJob(
cron = "0/10 * * * * ?",
jobName = "synctask",
shardingTotalCount = 1
)
@Component
public class SyncStaticTask implements SimpleJob {
@Override
public void execute(ShardingContext shardingContext) {
System.out.println("执行任务。。。");
}
}
7.3 elastic-job动态定时任务
修改mall-canal-service的核心配置文件,添加zookeeper服务信息,配置如下:
#动态定时任务案例
dynamiczk: 192.168.100.130:2181
dynamicnamespace: dynamictask
向Zookeeper注册信息,创建com.gupaoedu.vip.canal.job.dynamic.DynamicConfig代码如下:
@Configuration
public class DynamicConfig {
@Value("${dynamiczk}")
private String dynamiczk;
@Value("${dynamicnamespace}")
private String dynamicnamespace;
/****
* 指定当前注册地址信息
*/
@Bean
public ZookeeperConfiguration zookeeperConfiguration() {
return new ZookeeperConfiguration(dynamiczk,dynamicnamespace);
}
/****
* 向Zookeeper服务注册
*/
@Bean(initMethod = "init")
public ZookeeperRegistryCenter zookeeperRegistryCenter(ZookeeperConfiguration zookeeperConfiguration){
return new ZookeeperRegistryCenter(zookeeperConfiguration);
}
}
作业调度创建com.gupaoedu.vip.canal.job.dynamic.DynamicTaskCreate,代码如下:
@Component
public class DynamicTaskCreate {
@Autowired
private ZookeeperRegistryCenter zookeeperRegistryCenter;
/***
* 作业创建
* @param jobName:作业名字
* @param cron:表达式
* @param shardingTotalCount:分片数量
* @param instance:作业实例
* @param parameters:额外参数
*/
public void create(String jobName, String cron, int shardingTotalCount, SimpleJob instance,String parameters){
//1.配置作业->Builder->构建:LiteJobConfiguration
LiteJobConfiguration.Builder builder = LiteJobConfiguration.newBuilder(new SimpleJobConfiguration(
JobCoreConfiguration.newBuilder(
jobName,
cron,
shardingTotalCount
).jobParameter(parameters).build(),
instance.getClass().getName()
)).overwrite(true);
LiteJobConfiguration liteJobConfiguration = builder.build();
//2.开启作业
new SpringJobScheduler(instance,zookeeperRegistryCenter,liteJobConfiguration).init();
}
}
7.4 静态页定时删除
我们开始实现静态页动态删除操作。静态页动态删除我们需要根据活动ID查询所有商品详情,然后根据商品详情删除即可。
7.4.1 查询商品详情
1)Service
接口:修改mall-seckill-service的com.gupaoedu.vip.mall.seckill.service.SeckillGoodsService增加根据活动ID查询商品列表方法:
public interface SeckillGoodsService extends IService<SeckillGoods> {
//根据活动ID查询商品信息
List<SeckillGoods> actGoods(String acid);
}
实现类:修改com.gupaoedu.vip.mall.seckill.service.impl.SeckillGoodsServiceImpl增加根据活动ID查询商品列表实现方法:
@Service
public class SeckillGoodsServiceImpl extends ServiceImpl<SeckillGoodsMapper,SeckillGoods> implements SeckillGoodsService {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
//根据活动ID查询商品信息
@Override
public List<SeckillGoods> actGoods(String acid) {
QueryWrapper<SeckillGoods> seckillGoodsQueryWrapper = new QueryWrapper<SeckillGoods>();
seckillGoodsQueryWrapper.eq("activity_id",acid);
return seckillGoodsMapper.selectList(seckillGoodsQueryWrapper);
}
}
2)Controller
修改mall-seckill-service的com.gupaoedu.vip.mall.seckill.controller.SeckillGoodsController创建查询方法:
/***
* 根据活动查询秒杀商品集合
* @param acid
* @return
*/
@GetMapping(value = "/act/{acid}")
public RespResult<List<SeckillGoods>> actGoods(@PathVariable("acid") String acid){
List<SeckillGoods> seckillGoods = seckillGoodsService.actGoods(acid);
return RespResult.ok(seckillGoods);
}
3)Feign接口
在mall-seckill-api中修改com.gupaoedu.vip.mall.seckill.feign.SeckillGoodsFeign添加接口查询方法:
/***
* 根据活动查询秒杀商品集合
* @param acid
* @return
*/
@GetMapping(value = "/seckill/goods/act/{acid}")
RespResult<List<SeckillGoods>> actGoods(@PathVariable("acid") String acid);
7.4.2 Spring容器获取
在动态定时任务作业中,由于作业对象没有交给Spring容器管理,无法使用注入注解,需要从Spring容器中手动获取,因此我们可以创建一个类com.gupaoedu.vip.canal.spring.SpringContext获取容器对象:
@Component
public class SpringContext implements ApplicationContextAware {
private static ApplicationContext act;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
act = applicationContext;
}
public static <T>T getBean(Class clazz){
return act.getBean((Class<T>) clazz);
}
}
7.4.3 作业实现
创建作业对象,实现删除静态页操作,同时作为定时任务作业,在Canal监听的时候执行调用,代码如下:
public class DynamicJob implements SimpleJob {
//执行的作业
@Override
public void execute(ShardingContext shardingContext) {
//静态页删除
delete(shardingContext.getJobParameter());
}
/***
* 执行静态页删除
*/
public void delete(String acid){
//从容器中获取指定的实例
SeckillPageFeign seckillPageFeign = SpringContext.getBean(SeckillPageFeign.class);
seckillPageFeign.deleByAct(acid);
}
}
创建Canal监听类com.gupaoedu.vip.canal.listener.SeckillActivityHandler实现对活动的监听,活动发生变更时,执行动态创建定时任务删除静态页,代码如下:
@Component
@CanalTable(value = "seckill_activity")
public class SeckillActivityHandler implements EntryHandler<SeckillActivity> {
@Autowired
private DynamicTaskCreate dynamicTaskCreate;
/****
* 增加活动
* @param seckillActivity
*/
@Override
public void insert(SeckillActivity seckillActivity) {
//创建任务调度,活动结束的时候执行
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("ss mm HH dd MM ? yyyy");
String cron = simpleDateFormat.format(seckillActivity.getEndTime());
dynamicTaskCreate.create(seckillActivity.getId(), cron, 1, new DynamicJob(),seckillActivity.getId());
}
@Override
public void update(SeckillActivity before, SeckillActivity after) {
//创建任务调度,活动结束的时候执行
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("ss mm HH dd MM ? yyyy");
String cron = simpleDateFormat.format(after.getEndTime());
dynamicTaskCreate.create(after.getId(), cron, 1, new DynamicJob(),after.getId());
}
@Override
public void delete(SeckillActivity seckillActivity) {
}
}