一个极简、高效的秒杀系统-战术实践篇(内附源码)

一、前言

在上一篇《一个极简、高效的秒杀系统-战略设计篇》中,楼主重点讲解了基于Redis + Lua脚本的秒杀系统设计方案,如果没看过的同学,请花十分钟复习下。在这一篇中,楼主会结合代码,来探讨如何将设计方案落地。

提前剧透,工程源代码地址见楼主Github: https://github.com/Heroicai0101/seckill ,可下载到本地对照本篇看。

二、工程骨架

2.1 DDD概述

在看具体代码前,先窥一幅DDD工程骨架图,该图是楼主根据《领域驱动设计:软件核心复杂性应对之道》这本书的示例工程代码(Github地址戳这里进行绘制的, 图中每个框,代表一个包,整个工程代码组织结构就是该图的层次结构。如果对DDD感兴趣,也建议把代码下载下来,后续对照着书本进行阅读。对于新手,推荐先粗略看一遍《领域驱动设计:软件核心复杂性应对之道》了解DDD相关专业术语和概念,再精读几遍《实现领域驱动设计》。
在这里插入图片描述
DDD楼主认识有限,这里也不细说,简单概述下DDD的四个层次,从上到下依次是用户界面层(User Interface)、应用层(Application)、领域层(Domain)、基础设施层(Infrastructure)

  • 用户界面层: 负责向用户展示信息和解释用户命令。这里的用户是广义概念,既可以是用户界面的使用者,还可以是与当前系统交互的其他应用。
  • 应用层: 定义系统需要对外提供的能力,应用层通常不包含业务规则,主要是通过编排领域服务来完成能力建设。
  • 领域层: 提炼并抽象业务概念、业务规则,实现细节在基础设施层。DDD核心思想就是围绕领域对象来建模,故领域层是业务系统的核心。
  • 基础设施层:向其他层提供底层技术能力,如消息发送、数据库持久化等。基础设施层也不包含业务规则,可简单理解为对数据进行存/取的资源库。

这四个层次调用关系如下图(红色箭头代表调用方向): 可以看出用户界面层权力比较大,可以直接调用应用层、领域层、基础设施层;应用层可以调用领域层和基础设施层;
在这里插入图片描述

2.2 工程结构

有了上面各层整体认识后,再对照看下我们这个秒杀工程结构就容易理解了,结构基本是一样的!急不可耐的同学,如果想快速run起来的话,强烈建议参照楼主Github项目的README文档来起飞!
在这里插入图片描述

三、源码解读

在《一个极简、高效的秒杀系统-战略设计篇》这篇E-R图中提到了几个重要的领域模型:活动、活动准入规则、活动商品。既然DDD是围绕领域对象来建模的,所以在系统实现上,首要任务就是建立领域对象,并围绕领域对象来建模。So,我们先从领域层开始吧!
在这里插入图片描述

3.1领域层

3.1.1 领域模型

a. 活动

活动对象的设计比较简单

  • 活动对象的属性包含: 活动id、活动名称、活动开始/结束时间、活动是否启用状态,以及一个活动规则列表;
  • 活动对象的方法有三个:判断活动是否进行中onSale()、启用/禁用活动enableActivity() 以及判断当前请求是否符合活动准入条件canPass();这三个方法,其实就是活动这个领域对象应该具备的业务知识,只有活动对象才拥有完备的知识知道如何判断活动是否进行中、怎么启用/禁用活动、以及请求是否符合活动准入条件。假如这些业务逻辑按我们通常写法,把方法丢到某些Service对象中,就会出现本该由领域对象管理的业务逻辑散落到系统各处,造成只有属性没有方法的贫血模型。
/**
 * 活动信息
 */
@Data
public class Activity {
   

    /** 活动id */
    private ActivityId activityId;

    /** 活动名称 */
    private String activityName;

    /** 活动开始时间 */
    private Long startTime;

    /** 活动结束时间 */
    private Long endTime;

    /** 活动是否启用 */
    private boolean enabled;

    /** 活动准入规则 */
    private List<ActivityRule> activityRules;

    /**
     * 活动进行中
     */
    public boolean onSale(Long orderTime) {
   
        return enabled && (orderTime >= startTime && orderTime < endTime);
    }

    /**
     * 启用/禁用活动
     */
    public void enableActivity(boolean enabled) {
   
        this.enabled = enabled;
    }

    /**
     * 活动准入规则校验
     * 1、活动未配置规则, 则无需校验
     * 2、活动若配置了规则, 则逐一进行校验
     */
    public ActivityRuleCheckResult canPass(ActivityAccessContext context) {
   
        if (CollectionUtils.isEmpty(activityRules)) {
   
            return ActivityRuleCheckResult.ok();
        }

        Assert.notNull(context, "活动准入条件为空");
        for (ActivityRule activityRule : activityRules) {
   
            ActivityRuleCheckResult result = activityRule.satisfy(context);
            if (!result.isPass()) {
   
                return result;
            }
        }
        return ActivityRuleCheckResult.ok();
    }

}
b. 活动商品

商品本身依附于活动对象,没什么业务方法,持有商品id、商品标题、图片链接、原价、活动价、活动库存、限购数量等属性;值得注意的是,E-R图上我们看到活动跟商品有联系,但在Activity这个对象一点都没体现出来。在这里,我们看到是通过ActivityItem持有活动id来建立二者联系的。

/**
 * 活动商品
 */
@Data
public class ActivityItem {
   

    /** 商品id */
    private ItemId itemId;

    /** 活动id */
    private ActivityId activityId;

    /** 商品标题 */
    private String itemTitle;

    /** 商品副标题 */
    private String subTitle;

    /** 商品图片链接 */
    private String itemImage;

    /** 商品原价 */
    private Long itemPrice;

    /** 商品活动价 */
    private Long activityPrice;

    /** 每人限购件数 */
    private Integer quota;

    /** 商品活动库存 */
    private Integer stock;

}
c. 库存扣减流水

库存扣减流水:记录在哪个活动(activityId)、哪个用户(buyerId)、在何时下了哪笔订单、拍下哪个商品多少个(orderInfo); 扣库存、回库存操作强依赖这一对象

/**
 * 库存扣减流水:记录在哪个活动(activityId)、哪个用户(buyerId)、在何时下了哪笔订单、拍下哪个商品多少个(orderInfo)
 */
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class StockReduceFlow {
   

    /** 活动id */
    private ActivityId activityId;

    /** 买家id */
    private BuyerId buyerId;

    /** 订单信息 */
    private OrderInfo orderInfo;

}
d. 仓储

看到这里,可能会纳闷,上面这些领域对象都怎么构建出来的,它们存在哪里?这就不得不提到仓储(Repository)这个概念。在DDD理念里,仓储负责领域对象的存储,但仓储本身并没规定存储介质。也就是说仓储只负责定义领域对象的读/写协议,至于具体用内存、MySQL还是Oracle,它不关心。在《领域驱动设计:软件核心复杂性应对之道》中提到,一般只对聚合根建立仓储,但在我们秒杀系统中,活动对象Activity可算一个聚合根,至于活动商品、商品销量其实都不是聚合根,但楼主还是为这几个对象定义了仓储。实在是仓储这个概念的度拿捏不好,没办法严格照搬书上的做法。如果大家有独到的见解,欢迎和楼主交流!

  • 活动仓储(ActivityRepository): 查活动、保存活动
/**
 * 活动
 */
public interface ActivityRepository {
   

    /**
     * 查活动列表
     */
    List<Activity> listActivity();

    /**
     * 查单个活动
     */
    Activity findActivity(ActivityId activityId);

    /**
     * 保存活动
     */
    void saveActivity(Activity activity);

}
  • 活动商品仓储(ActivityItemRepository): 查商品、保存商品
/**
 * 活动商品
 */
public interface ActivityItemRepository {
   

    /**
     * 保存指定活动的商品配置
     */
    void saveActivityItem(ActivityId activityId, List<ActivityItem> activityItems);

    /**
     * 查指定活动的指定商品
     */
    Optional<ActivityItem> findActivityItem(ActivityId activityId, ItemId itemId);

    /**
     * 查活动商品(缺商品销量)
     */
    List<ActivityItem> queryActivityItems(ActivityId activityId);

}
  • 库存扣减流水仓储(StockReduceFlowRepository): 查库存扣减流水
/**
 * 库存扣减流水
 */
public interface StockReduceFlowRepository {
   

    Optional<StockReduceFlow> queryStockReduceFlow(ActivityId activityId, OrderId orderId);

}
  • 商品销量仓储(ItemSalesRepository): 查单个、全部商品销量
/**
 * 商品销量
 */
public interface ItemSalesRepository {
   

    /**
     * 查活动全部商品销量
     */
    Map<Long, Integer> queryActivityItemSales(ActivityId activityId);

    /**
     * 查商品指定活动的销量
     */
    ItemSales queryItemSales(ActivityId activityId, ItemId itemId);

}

3.1.2 领域服务

a. 活动配置

完整的活动配置操作,涉及活动及活动商品多个领域对象,并且还需要进行数据持久化。这些职责显然不能放在单个活动或者活动商品对象上,这时我们就需要提炼出一个领域服务。ActivityService这个领域服务就是用来完成活动、活动商品配置,以及活动启用/禁用能力的。

public interface ActivityService {
   

    /**
     * 配置活动及活动商品
     */
    void saveActivity(Activity activity, List<ActivityItem> activityItems);

    /**
     * 启用/禁用活动
     */
    void enableActivity(ActivityId activityId, boolean enabled);

}
b. 库存扣减

秒杀系统的核心就是正确执行库存扣减,这里定义了库存扣减服务的两大核心方法:扣库存reduce()cancelReduce()

  • 扣库存: 入参为库存扣减流水,通过流水信息知道扣哪个活动ActivityId哪个用户BuyerId抢购资格,以及订单上的信息(商品id、购买数量、订单id、下单时间)指导扣哪个商品的活动库存;
  • 回库存: 入参为库存扣减流水,通过流水信息知道具体怎么把商品活动库存、用户抢购资格给加回去;本质就是扣库存的逆向操作。
/**
 * 库存扣减服务
 */
public interface StockReduceService {
   

    /**
     * 扣库存(同步调用)
     */
    StockReduceResult reduce(StockReduceFlow flow);

    /**
     * 回库存
     */
    StockReduceResult cancelReduce(StockReduceFlow flow
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值