跳至主要內容

云商城-商品秒杀-数据处理

soulballad实践项目SpringCloudAlibaba 云商城SpringCloudAlibaba约 5945 字大约 20 分钟

第9章 秒杀数据处理

课程目标

1、秒杀设计

​ 1)秒杀业务设计

​ 2)秒杀架构设计

​ 3)秒杀表结构设计

2、活动管理

​ 1)活动分析

​ 2)有效活动列表查询

3、搜索管理

​ 1)搜索业务分析

​ 2)搜索索引实时导入

​ 3)秒杀搜索实现

4、商品详情处理

​ 1)搜索详情页生成

5、 数据同步

​ 1)静态页同步

​ 2)索引同步

1 秒杀设计

1.1 秒杀业务

1609565459466

1609566443802

1.2 秒杀业务特点介绍

​ 秒杀业务是互联网公司电商项目中的标志性业务,是典型的高并发场景。秒杀业务主要的问题是大量用户短时间内涌入,导致瞬时流量巨大,对于数据库、缓存的性能是一个巨大考验。

1)高并发

​ 我们通常衡量一个服务器的吞吐率的指标是QPS(每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。
​ 假设处理一个业务请求平均响应时间为 100 ms,同时,系统内有 20 台 服务器,配置最大连接数为 500 个,Web 系统的理论峰值QPS为(理想化的计算方式):100000 (10万QPS)意味着1 秒钟可以处理完 10 万的请求,而“秒杀”的那 5w/s 的秒杀似乎是“纸老虎”。
​ 然而实际情况,在高并发的实际场景下,服务器处于高负载的状态,网络带宽被挤满,在这个时候平均响应时间会被大大增加。随着用户数量的增加,数据库连接进程增加,需要处理的上下文切换也越多,造成服务器负载越来越重。

2)业务耦合高

​ 秒杀最大的问题是,当系统上某个应用因为延迟而变得不可用,用户的点击越频繁,恶性循环最终导致“雪崩”,因为其中一台服务器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环,将整个系统拖垮。

3)冷热商品处理

​ 秒杀过程中并发量极高被抢购的商品其实并不多,都是大家所熟知的一些品牌和商品,针对这些商品我们应该和抢购人数并不是特别多的商品进行区分,热门商品走排队系统,非热门商品直接抢单即可。

​ 在秒杀过程中,还会出现一些特殊现象,例如一直都是冷门的商品口罩在2020年突然变成全球热门产品,流量突然增长的产品,我们系统要具备发现能力。

1609564673406

1.3 秒杀架构设计

1609559865198

​ 针对秒杀抢单业务的架构设计,不能和普通的商品抢单一样,架构应该考虑热门商品造成的并发、冷门商品能有可用的系统资源、波动商品走向热门的发现功能、高并发抢单超卖问题解决等,如上图架构设计:

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引入到工程中,如下图:

1609743751028

3 活动管理

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

3.1 秒杀活动分析

1609742088286

秒杀活动有很多种,并非只有大家平时看到的双十一,其实每天都有秒杀活动,但我们设计秒杀活动表结构应该满足不同类型的秒杀活动,我们再来看下秒杀活动表结构:

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_timeend_time就是时间间隔,而type则是活动类型,活动类型并非是商品分类,而是活动分类,分类的值为0,1,2,3均为每日秒杀。

**节假日秒杀:**如果type=4则表示节假日秒杀活动,例如双十一等,节假日秒杀活动并非。

我们这里做shop秒杀功能,因此首先需要根据活动表查询出活动时间,再每次点击一个活动时间加载对应的商品列表即可。

3.2 活动列表

1609742088286

活动列表查询我们需要分析一下如何查询,需要满足如下几个条件:

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效果如下:

1609744822657

4 搜索管理

商品搜索我们采用Elasticsearch,因为秒杀商品数据量太大,不建议大量使用Redis缓存。

4.1 业务分析

1609744958723

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

1609745438896

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 数据同步方案

1609791728137

数据同步方案如上图:

1:秒杀服务更新商品信息到数据库
2:Canal监听数据库变化,数据同步服务消费增量变化
3:数据同步服务根据变化调用搜索服务实现数据同步
4:数据同步服务根据变化调用静态页服务实现静态页同步

6.2 数据同步实现

6.2.1 Canal监听配置

修改canal配置,添加seckill_goodsseckill_activity表的监听,并重启canal,如下图:

1609792084488

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 静态页定时更新

1609812270967

秒杀活动是一个时间段为单位,前面虽然做到了实时更新秒杀商品索引和静态页,但无法做到定时更新秒杀商品详情页和索引,如上图,第1个活动结束的时候,应该要清理00:00-08:00活动的索引数据和静态页,而我们目前做不到,我们需要采用定时器的机制实现。

7.1 elastic-job介绍

当前主流的分布式任务调度:

featurequartzelastic-jobxxl-jobantaresopencron
依赖mysqljdk1.7+, zookeeper 3.4.6+ ,maven3.0.4+mysql ,jdk1.7+ , maven3.0+jdk 1.7+ , redis , zookeeperjdk1.7+ , Tomcat8.0+
HA多节点部署,通过竞争数据库锁来保证只有一个节点执行任务通过zookeeper的注册与发现,可以动态的添加服务器。 支持水平扩容集群部署集群部署
任务分片支持支持支持
文档完善完善完善完善文档略少文档略少
管理界面支持支持支持支持
难易程度简单简单简单一般一般
公司OpenSymphony当当网个人个人个人
高级功能弹性扩容,多种作业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化任务分片, 失效转移,弹性扩容 ,时间规则支持quartz和crontab ,kill任务, 现场执行,查询任务运行状态
使用企业大众化产品,对分布式调度要求不高的公司大面积使用36氪,当当网,国美,金柚网,联想,唯品会,亚信,平安,猪八戒大众点评,运满满,优信二手车,拍拍贷

ElasticJob 是面向互联网生态和海量任务的分布式调度解决方案,由两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。

ElasticJob 已于 2020 年 5 月 28 日成为 Apache ShardingSphereopen in new window 的子项目。

1609819019238

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-servicecom.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-servicecom.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) {

    }
}
上次编辑于:
贡献者: soulballad