14

一个极简、高效的秒杀系统(战术实践篇)

 4 years ago
source link: http://dockone.io/article/9976
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

工程骨架

DDD概述

在看具体代码前,先窥一幅DDD工程骨架图,该图是楼主根据《领域驱动设计:软件核心复杂性应对之道》这本书的示例工程代码( Github地址戳这里 )进行绘制的,图中每个框,代表一个包,整个工程代码组织结构就是该图的层次结构。如果对DDD感兴趣,也建议把代码下载下来,后续对照着书本进行阅读。对于新手,推荐先粗略看一遍《领域驱动设计:软件核心复杂性应对之道》了解DDD相关专业术语和概念,再精读几遍《实现领域驱动设计》。

Jn6VFnQ.png!web

DDD楼主认识有限,这里也不细说,简单概述下DDD的四个层次,从上到下依次是用户界面层(User Interface)、应用层(Application)、领域层(Domain)、基础设施层(Infrastructure)。

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

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

uQfiMbb.png!web

工程结构

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

ZzQFZnr.png!web

源码解读

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

IN7Vvq2.png!web

领域层

领域模型

1、活动

活动对象的设计比较简单:

  • 活动对象的属性包含:活动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();

}



} 

2、活动商品

商品本身依附于活动对象,没什么业务方法,持有商品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;



} 

3、库存扣减流水

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

/**

* 库存扣减流水:记录在哪个活动(activityId)、哪个用户(buyerId)、在何时下了哪笔订单、拍下哪个商品多少个(orderInfo)

*/

@Builder

@Data

@NoArgsConstructor

@AllArgsConstructor

public class StockReduceFlow {



/** 活动id */

private ActivityId activityId;



/** 买家id */

private BuyerId buyerId;



/** 订单信息 */

private OrderInfo orderInfo;



} 

4、仓储

看到这里,可能会纳闷,上面这些领域对象都怎么构建出来的,它们存在哪里?这就不得不提到仓储(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);



} 

领域服务

1、活动配置

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

public interface ActivityService {



/**

 * 配置活动及活动商品

 */

void saveActivity(Activity activity, List<ActivityItem> activityItems);



/**

 * 启用/禁用活动

 */

void enableActivity(ActivityId activityId, boolean enabled);



} 

2、库存扣减

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

扣库存:入参为库存扣减流水,通过流水信息知道扣哪个活动ActivityId哪个用户BuyerId抢购资格,以及订单上的信息(商品id、购买数量、订单id、下单时间)指导扣哪个商品的活动库存;

回库存:入参为库存扣减流水,通过流水信息知道具体怎么把商品活动库存、用户抢购资格给加回去;本质就是扣库存的逆向操作。

/**

* 库存扣减服务

*/

public interface StockReduceService {



/**

 * 扣库存(同步调用)

 */

StockReduceResult reduce(StockReduceFlow flow);



/**

 * 回库存

 */

StockReduceResult cancelReduce(StockReduceFlow flow);



} 

小结

这一章,我们完成了领域对象的定义,并赋予了领域对象部分方法用来封装相应的职责;定义了两个领域服务:1、通过ActivityService,可以做到配置活动、启用/禁用活动;2、通过StockReduceService,可以做到扣库存、回库存;至此,秒杀系统的两大核心业务流程: 「创建秒杀活动」(配活动、配商品)、「参与秒杀活动」(扣库存、回库存)的实现骨架已搭建起来!

i6vYzea.png!web

应用层

活动应用服务

在DDD四层概念中,提到过应用层就是定义系统需要对外提供的能力。说白了,就是近似定义对外提供的接口集合。对照之前的设计方案,系统需要具备的接口有:配置活动、启用/禁用活动、查看活动列表、查看活动详情、查看活动商品详情。

6FZJbef.png!web

按图索骥,毫不费力就推导出如下应用服务接口定义:其中配置活动、启用/禁用活动的业务逻辑,可直接借用活动领域服务ActivityService能力;剩下的几个纯粹查询(查看活动列表、查看活动详情、查看活动商品详情)属于「查看秒杀活动」业务流程,没啥业务规则,通过直接调用仓储的数据读取能力就能搞定。

接口定义:

public interface ActivityAppService {



/**

 * 配置活动及商品列表

 */

Long saveActivity(SaveActivityCommand command);



/**

 * 启用/禁用活动

 */

void changeActivityStatus(UpdateActivityStatusCommand command);



/**

 * 活动列表

 */

List<ActivityDTO> activityList();



/**

 * 活动详情页(透出活动商品及商品销量)

 */

ActivityDetailDTO activityDetail(Long activityId);



/**

 * 活动商品详情

 */

ActivityItemDetailDTO activityItemDetail(ActivityId activityId, ItemId itemId);



} 

接口实现:

@Service

public class ActivityAppServiceImpl implements ActivityAppService {



@Resource

private ActivityRepository activityRepository;



@Resource

private ActivityItemRepository activityItemRepository;



@Resource

private ItemSalesRepository itemSalesRepository;



@Resource

private ActivityAssembler activityAssembler;



@Resource

private ActivityService activityService;



/**

 * 配置活动及商品列表

 */

@Override

public Long saveActivity(SaveActivityCommand command) {

    // 活动

    Activity activity = activityAssembler.assembleActivity(command);



    // 活动商品

    ActivityId activityId = activity.getActivityId();

    List<ActivityItem> activityItems = activityAssembler.assembleActivityItem(activityId, command);



    activityService.saveActivity(activity, activityItems);

    return activityId.getId();

}



/**

 * 启用/禁用活动

 */

@Override

public void changeActivityStatus(UpdateActivityStatusCommand command) {

    ActivityId activityId = new ActivityId(command.getActivityId());

    activityService.enableActivity(activityId, command.isEnabled());

}



/**

 * 活动列表

 */

@Override

public List<ActivityDTO> activityList() {

    List<Activity> activityList = activityRepository.listActivity();

    return activityList.stream()

                       .map(act -> activityAssembler.asActivityDTO(act))

                       .collect(Collectors.toList());

}



/**

 * 活动详情页(透出活动商品及商品销量)

 */

@Override

public ActivityDetailDTO activityDetail(Long activityId) {

    // 活动信息

    ActivityId aid = new ActivityId(activityId);

    Activity act = activityRepository.findActivity(aid);

    Assert.notNull(act, "活动不存在:activityId=" + activityId);



    // 活动商品

    List<ActivityItem> activityItems = activityItemRepository.queryActivityItems(aid);



    // 商品销量

    Map<Long, Integer> itemId2Sales = itemSalesRepository.queryActivityItemSales(aid);



    List<ActivityItemDTO> itemDTOList = activityItems.stream().map(item -> {

        int itemSales = itemId2Sales.getOrDefault(item.getItemId().getId(), 0);

        return activityAssembler.assembleActivityItemDTO(item, itemSales);

    }).collect(Collectors.toList());



    return ActivityDetailDTO.builder()

                            .activityId(aid.getId())

                            .activityName(act.getActivityName())

                            .startTime(act.getStartTime())

                            .endTime(act.getEndTime())

                            .enabled(act.isEnabled())

                            .items(itemDTOList)

                            .build();

}



/**

 * 活动商品详情

 */

@Override

public ActivityItemDetailDTO activityItemDetail(ActivityId activityId, ItemId itemId) {

    // 活动商品

    Optional<ActivityItem> optItem = activityItemRepository.findActivityItem(activityId, itemId);

    Assert.isTrue(optItem.isPresent(), "商品不存在:itemId=" + itemId.getId());

    ActivityItem item = optItem.get();



    // 商品销量

    ItemSales itemSales = itemSalesRepository.queryItemSales(activityId, itemId);



    // 活动信息

    Activity act = activityRepository.findActivity(activityId);

    Assert.notNull(act, "活动不存在:activityId=" + activityId.getId());



    ActivityDTO activityDTO = ActivityDTO.builder()

                                         .activityId(act.getActivityId().getId())

                                         .activityName(act.getActivityName())

                                         .startTime(act.getStartTime())

                                         .endTime(act.getEndTime())

                                         .enabled(act.isEnabled())

                                         .build();



    return ActivityItemDetailDTO.builder()

                                .itemId(item.getItemId().getId())

                                .itemTitle(item.getItemTitle())

                                .subTitle(item.getSubTitle())

                                .itemImage(item.getItemImage())

                                .itemPrice(item.getItemPrice())

                                .activityPrice(item.getActivityPrice())

                                .quota(item.getQuota())

                                .stock(item.getStock())

                                .sold(itemSales.getSold())

                                .activity(activityDTO)

                                .build();

}



} 

说明:

  • 应用服务直接持有领域服务ActivityService和多个仓储对象(如: ActivityRepository、ActivityItemRepository、ItemSalesRepository);
  • 配置活动、启用/禁用活动:直接交给领域服务来完成;
  • 查活动列表、活动详情、活动商品详情:直接利用仓储对象读取数据,并进行数据整合;不需要领域服务参与。

库存应用服务

参与秒杀活动对外暴露的能力就是扣库存、回库存,这个应该没什么疑问。

aqqMjev.png!web

同上,应用层我们面向交易系统提供两个API,来定义库存扣减交互协议;其中库存扣减这一核心能力,前面我们在领域服务已进行了封装。故库存扣减这个应用服务的业务逻辑也应该非常薄!

接口定义:

/**

* 库存扣减应用服务

*/

public interface StockAppService {



/**

 * 扣库存

 */

StockReduceResult reduce(ReduceCommand command);



/**

 * 回库存

 */

StockReduceResult cancelReduce(CancelReduceCommand command);



} 

接口实现:

/**

* 库存扣减应用服务

*/

@Service

public class StockAppServiceImpl implements StockAppService {



@Resource

private ActivityRepository activityRepository;



@Resource

private StockReduceFlowRepository stockReduceFlowRepository;



@Resource

private StockReduceFlowAssembler stockReduceFlowAssembler;



@Resource

private StockReduceService stockReduceService;



/**

 * 扣库存

 */

@Override

public StockReduceResult reduce(@NonNull ReduceCommand command) {

    ActivityId activityId = new ActivityId(command.getActivityId());

    StockReduceFlow reduceFlow = stockReduceFlowAssembler.assembleStockReduceFlow(command);



    // 前置校验:活动是否存在

    Activity activity = activityRepository.findActivity(activityId);

    if (Objects.isNull(activity)) {

        return StockReduceResult.error(BizStatusCode.ACTIVITY_NOT_EXISTS, activityId.getId());

    }



    // 前置校验:活动是否进行中

    OrderInfo orderInfo = reduceFlow.getOrderInfo();

    if (!activity.onSale(orderInfo.getOrderTime())) {

        return StockReduceResult.error(BizStatusCode.ACTIVITY_OFFLINE, activityId.getId());

    }



    // 活动准入规则校验

    ActivityAccessContext accessContext = stockReduceFlowAssembler.assembleActivityAccessContext(command);

    ActivityRuleCheckResult activityRuleCheckResult = activity.canPass(accessContext);

    if (!activityRuleCheckResult.isPass()) {

        return StockReduceResult.error(activityRuleCheckResult.getErrmsg());

    }



    // 扣库存

    return stockReduceService.reduce(reduceFlow);

}



/**

 * 回库存

 */

@Override

public StockReduceResult cancelReduce(@NonNull CancelReduceCommand command) {

    ActivityId activityId = new ActivityId(command.getActivityId());

    OrderId orderId = new OrderId(command.getOrderId());



    Optional<StockReduceFlow> optFlow = stockReduceFlowRepository.queryStockReduceFlow(activityId, orderId);

    if (optFlow.isPresent()) {

        StockReduceFlow reduceFlow = optFlow.get();

        return stockReduceService.cancelReduce(reduceFlow);

    }

    return StockReduceResult.ok();

}



} 

说明:

  • 扣库存:在调用库存扣减领域服务之前,还做了一些前置校验工作,比如校验活动是否存在、活动是否进行中、以及当前请求是否符合活动准入规则;
  • 回库存:通过活动id和订单id,得到库存扣减流水,然后直接调用库存扣减领域服务进行回库存操作。

小结

这一章,我们完成了两个应用服务的定义:1、通过ActivityAppService,我们完成了面向用户(买家、运营)的查看秒杀活动、配置秒杀活动的接口定义;2、通过StockAppService,完成了面向用户(交易系统)的扣库存、回库存接口定义。至此,配置秒杀活动、查看秒杀活动、参与秒杀活动三大业务流程,所需的接口能力都已定义清楚。

用户界面层

用户界面层的职责就是向用户展示信息(读请求),以及将用户的命令(写请求)传递到应用层、领域层甚至基础设施层(比如直接写数据库)。这一层毫无业务逻辑可言。搞笑点说,就是面向视觉稿编程,用户需要什么就给什么。有了应用服务的接口定义,用户界面层也不费神,通常就是直接利用应用服务的能力!

活动接口:

@Api(value = "活动接口")

@RestController

@RequestMapping("/api/v1/activity")

public class ActivityController {



@Resource

private ActivityAppService activityAppService;



@ApiOperation(value = "配置活动")

@PostMapping("/save")

public Response<Long> saveActivity(@RequestBody SaveActivityCommand command) {

    Long activityId = activityAppService.saveActivity(command);

    return ResponseBuilder.ok(activityId);

}



@ApiOperation(value = "启用/禁用活动")

@PostMapping("/changeStatus")

public Response<Void> changeActivityStatus(@RequestBody UpdateActivityStatusCommand command) {

    activityAppService.changeActivityStatus(command);

    return ResponseBuilder.ok();

}



@ApiOperation(value = "活动列表")

@GetMapping("/list")

public Response<List<ActivityDTO>> activityList() {

    List<ActivityDTO> dto = activityAppService.activityList();

    return ResponseBuilder.ok(dto);

}



@ApiOperation(value = "活动详情(透出活动商品及商品销量)")

@GetMapping("/detail")

public Response<ActivityDetailDTO> activityDetail(

        @ApiParam(value = "activityId", defaultValue = "1") @RequestParam("activityId") Long activityId) {

    ActivityDetailDTO activityDetailDTO = activityAppService.activityDetail(activityId);

    return ResponseBuilder.ok(activityDetailDTO);

}



@ApiOperation(value = "活动商品详情(透出商品销量)")

@GetMapping("/itemDetail")

public Response<ActivityItemDetailDTO> activityItemDetail(

        @ApiParam(value = "activityId", defaultValue = "1") @RequestParam("activityId") Long activityId,

        @ApiParam(value = "itemId", defaultValue = "53724") @RequestParam("itemId") Long itemId) {

    ActivityId curActivityId = new ActivityId(activityId);

    ItemId curItemId = new ItemId(itemId);



    ActivityItemDetailDTO activityDetailDTO = activityAppService.activityItemDetail(curActivityId, curItemId);

    return ResponseBuilder.ok(activityDetailDTO);

}



} 

库存扣减接口:

@Api(value = "库存扣减接口")

@RestController

@RequestMapping("/api/v1/stock")

public class StockController {



@Resource

private StockAppService stockAppService;



@ApiOperation(value = "减库存")

@PostMapping("/reduce")

public Response<StockReduceResult> tryReduce(@RequestBody ReduceCommand command) {

    StockReduceResult res = stockAppService.reduce(command);

    return ResponseBuilder.ok(res);

}



@ApiOperation(value = "回库存")

@PostMapping("/cancelReduce")

public Response<StockReduceResult> cancelReduce(@RequestBody CancelReduceCommand command) {

    StockReduceResult res = stockAppService.cancelReduce(command);

    return ResponseBuilder.ok(res);

}



} 

如果熟悉swagger-ui使用方式,启动工程访问 http://localhost:8080/swagger-ui.html ,就会看到如下界面:

qYfq6fV.jpg!web

基础设施层

前面噼里啪啦一大堆,看到的大多是接口定义,好奇宝宝们还是想知道到底怎样做库存扣减的,接下来我们就讲讲库存扣减的实现。在领域层我们定义了领域服务,但仅仅只是一个接口,领域服务的实现是在基础设施层。 本文的基调就是Redis+Lua脚本实现秒杀,库存扣减服务的实现StockReduceServiceImpl就是完全依赖Lua脚本。

领域服务实现

库存扣减领域服务实现类StockReduceServiceImpl:

@Slf4j

@Service

public class StockReduceServiceImpl implements StockReduceService {



@Resource

private Gson gson;



@Resource

private RedissonClient redissonClient;



private String reduceLua;



private String cancelReduceLua;



@PostConstruct

public void scriptLoading() {

    try {

        reduceLua = LuaScriptHelper.readScript(LuaScriptConstant.Seckill.REDUCE_LUA);

        cancelReduceLua = LuaScriptHelper.readScript(LuaScriptConstant.Seckill.CANCEL_REDUCE_LUA);

    } catch (IOException ioe) {

        throw new IllegalStateException("Script not found!", ioe);

    }

}



/**

 * 执行Lua脚本扣库存

 */

@Override

public StockReduceResult reduce(StockReduceFlow flow) {

    Long activityId = flow.getActivityId().getId();

    Long itemId = flow.getOrderInfo().getItemId().getId();



    /* KEYS[1] 库存扣减流水,KEYS[2] 活动商品,KEYS[3] 买家已购,KEYS[4] 商品销量 */

    String stockReduceFlowHash = SeckillNamespace.stockReduceFlowHash(activityId);

    String activityItemHash = SeckillNamespace.activityItemsHash(activityId);

    String buyerHoldHash = SeckillNamespace.buyerHoldHash(activityId, itemId);

    String itemSalesHash = SeckillNamespace.itemSalesHash(activityId);

    List<Object> keys = Lists.newArrayList(stockReduceFlowHash, activityItemHash, buyerHoldHash, itemSalesHash);



    /* ARGV[1] 订单id,ARGV[2] 买家id,ARGV[3] 商品id,ARGV[4] 抢购数量,ARGV[5] json化库存扣减流水 */

    StockReduceFlowDO reduceFlowDO = StockReduceFlowConverter.toDO(flow);

    String reduceFlowJson = gson.toJson(reduceFlowDO);

    Object[] values = {

            reduceFlowDO.getOrderId(),

            reduceFlowDO.getBuyerId(),

            reduceFlowDO.getItemId(),

            reduceFlowDO.getQuantity(),

            reduceFlowJson

    };



    // 执行减库存Lua脚本

    String resultCode = LuaScriptHelper.create(redissonClient)

                                       .evalLuaScript(keys, values, reduceLua);



    if (!LuaResultDictionary.SUCCESS_RESULT.equals(resultCode)) {

        Status status = LuaResultDictionary.mapping(resultCode);

        String errmsg = status.getMsg(reduceFlowDO.getOrderId());

        log.error("reduce_exception||reduceFlow={}||errmsg={}", reduceFlowJson, errmsg);

        return StockReduceResult.error(errmsg);

    }



    log.info("reduce_success||reduceFlow={}", reduceFlowJson);

    return StockReduceResult.ok();

}



/**

 * 执行Lua脚本回库存

 */

@Override

public StockReduceResult cancelReduce(StockReduceFlow stockReduceFlow) {

    ActivityId activityId = stockReduceFlow.getActivityId();

    OrderInfo orderInfo = stockReduceFlow.getOrderInfo();

    OrderId orderId = orderInfo.getOrderId();

    ItemId itemId = orderInfo.getItemId();



    Long aid = activityId.getId();

    String stockReduceFlowHash = SeckillNamespace.stockReduceFlowHash(aid);

    String buyerHoldHash = SeckillNamespace.buyerHoldHash(aid, itemId.getId());

    String itemSalesHash = SeckillNamespace.itemSalesHash(aid);



    /* KEYS[1] 库存扣减流水,KEYS[2] 买家已购,KEYS[3] 商品销量 */

    List<Object> keys = Lists.newArrayList(stockReduceFlowHash, buyerHoldHash, itemSalesHash);



     /* ARGV[1] 订单id */

    Object[] values = {orderId.getId()};



    // 执行回库存Lua脚本

    String resultCode = LuaScriptHelper.create(redissonClient)

                                       .evalLuaScript(keys, values, cancelReduceLua);



    if (!LuaResultDictionary.SUCCESS_RESULT.equals(resultCode)) {

        Status status = LuaResultDictionary.mapping(resultCode);

        String errmsg = status.getMsg(orderId.getId());

        log.error("cancel_reduce_exception||activityId={}||orderId={}||itemId={}||errmsg={}",

                aid, orderId.getId(), itemId.getId(), errmsg);



        return StockReduceResult.error(errmsg);

    }



    log.info("cancel_reduce_success||activityId={}||orderId={}||itemId={}", aid, orderId.getId(), itemId.getId());

    return StockReduceResult.ok();

}



} 

StockReduceServiceImpl唯一有意义的事就是在启动时加载Lua脚本,剩下干的事就是传参给Lua脚本。所以,真相就在Lua脚本。看看Lua脚本都在干啥?

扣库存Lua脚本:

1、通过校验库存扣减流水是否已存在,来判断是否重复请求;

2、加载商品活动配置,得到商品每人限购数量及活动库存,进而判断用户抢购资格以及商品库存是否充足;

3、真正做扣库存该干的事:记录库存扣减流水、增买家已购计数、增商品销量计数。

--[[

KEYS[1] 库存扣减流水, KEYS[2] 活动商品, KEYS[3] 买家已购, KEYS[4] 商品销量

ARGV[1] 订单id, ARGV[2] 买家id, ARGV[3] 商品id, ARGV[4] 抢购数量, ARGV[5] json化库存扣减流水

--]]

local orderId = ARGV[1];

local buyerId = ARGV[2];

local itemId = ARGV[3];



-- 防重判断

local STOCK_REDUCE_FLOW_HASH = KEYS[1];

local flowExists = redis.call("HEXISTS", STOCK_REDUCE_FLOW_HASH, orderId);

if flowExists == 1 then

return "REPEATED_REQUEST";

end



-- 校验商品是否参加了活动

local ACTIVITY_ITEMS_HASH = KEYS[2];

local activityExists = redis.call("HEXISTS", ACTIVITY_ITEMS_HASH, itemId);

if activityExists == 0 then

return "ITEM_ACTIVITY_ABSENT";

end



-- 加载活动商品配置

local config = redis.call("HGET", ACTIVITY_ITEMS_HASH, itemId);

local payload = cjson.decode(config);



-- 用户已购数量

local BUYER_HOLD_HASH = KEYS[3];

local bookedCount = redis.call("HGET", BUYER_HOLD_HASH, buyerId);

if bookedCount == false then

bookedCount = 0;

end

if bookedCount + tonumber(ARGV[4]) > tonumber(payload["quota"]) then

return "QUOTA_NOT_ENOUGH";

end



-- 商品累计售出数量

local ITEM_SALES_HASH = KEYS[4];

local soldCount = redis.call("HGET", ITEM_SALES_HASH, itemId);

if soldCount == false then

soldCount = 0;

end

if soldCount + tonumber(ARGV[4]) > tonumber(payload["stock"]) then

return "STOCK_NOT_ENOUGH";

end



-- 记录库存扣减流水、增买家已购、增商品销量

redis.call("HSET", STOCK_REDUCE_FLOW_HASH, orderId, ARGV[5]);

redis.call("HINCRBY", BUYER_HOLD_HASH, buyerId, ARGV[4]);

redis.call("HINCRBY", ITEM_SALES_HASH, itemId, ARGV[4]);

return "OK";

回库存Lua脚本:

1、通过校验库存扣减流水是否已存在,来判断是否重复请求;

2、加载商品库存扣减流水,从流水得到用户需要回滚的已购数量以及商品销量需要回滚的数量;

3、真正做回库存该干的事: 减买家已购计数、减商品销量计数、删除库存扣减流水。

--[[

KEYS[1] 库存扣减流水, KEYS[2] 买家已购, KEYS[3] 商品销量

ARGV[1] 订单id

--]]

local STOCK_REDUCE_FLOW_HASH = KEYS[1];

local BUYER_HOLD_HASH = KEYS[2];

local ITEM_SALES_HASH = KEYS[3];

local orderId = ARGV[1];



-- 幂等控制

local flowExists = redis.call("HEXISTS", STOCK_REDUCE_FLOW_HASH, orderId);

if flowExists == 0 then

return "NO_REDUCE_FLOW";

end



local flow = redis.call("HGET", STOCK_REDUCE_FLOW_HASH, orderId);

local payload = cjson.decode(flow);



-- 减商品销量、减买家已购

redis.call("HINCRBY", BUYER_HOLD_HASH, payload["buyerId"], -1 * tonumber(payload["quantity"]));

redis.call("HINCRBY", ITEM_SALES_HASH, payload["itemId"], -1 * tonumber(payload["quantity"]));



-- 删除库存扣减流水

redis.call("HDEL", STOCK_REDUCE_FLOW_HASH, orderId);

return "OK";

仓储实现

本文的存储介质百分百为Redis,故仓储的实现,完全就是利用Redis的数据结构来做纯CRUD,没有任何业务逻辑和技术含量,故仅以活动仓储ActivityRepositoryImpl来示例。

仓储实现类ActivityRepositoryImpl: 利用的是Redis的Hash结构,活动信息存储在activity_catalog这个Hash结构中:

@Repository

public class ActivityRepositoryImpl implements ActivityRepository {



@Resource

private Gson gson;



@Resource

private RedissonClient redissonClient;



/**

 * 活动列表

 */

@Override

public List<Activity> listActivity() {

    String activityCatalogHash = SeckillNamespace.activityCatalogHash();



    RMap<String, String> activityMap = redissonClient.getMap(activityCatalogHash);

    List<ActivityDO> activityList = activityMap.values().stream()

                                               .map(activity -> gson.fromJson(activity, ActivityDO.class))

                                               .collect(Collectors.toList());

    return activityList.stream()

                       .map(ActivityConverter::fromDO)

                       .collect(Collectors.toList());

}



/**

 * 根据活动id查活动

 */

@Override

public Activity findActivity(ActivityId activityId) {

    String activityCatalogHash = SeckillNamespace.activityCatalogHash();



    Map<String, String> activityMap = redissonClient.getMap(activityCatalogHash);

    String activity = activityMap.get(String.valueOf(activityId.getId()));

    Assert.hasText(activity, "活动不存在:activityId=" + activityId.getId());



    ActivityDO activityDO = gson.fromJson(activity, ActivityDO.class);

    return ActivityConverter.fromDO(activityDO);

}



/**

 * 保存活动

 */

@Override

public void saveActivity(Activity activity) {

    ActivityId activityId = activity.getActivityId();

    ActivityDO activityDO = ActivityConverter.toDO(activity);



    String activityCatalogHash = SeckillNamespace.activityCatalogHash();

    redissonClient.getMap(activityCatalogHash)

                  .put(String.valueOf(activityId.getId()), gson.toJson(activityDO));

}



} 

总结

本文遵循DDD领域建模思想,从领域层、应用层、用户界面层逐层复原秒杀系统设计方案的落地全过程,并结合源码进行了分析阐述。其实,代码不重要,建模过程中的思想才是最有价值的,希望读者能领略到DDD建模的魅力,并在实际工作中进行运用实践!学习都是从模仿开始,感兴趣的读者可以将《领域驱动设计:软件核心复杂性应对之道》示例代码工程源码或楼主的工程代码码下载下来参考。

其它未讲到的点:

  • 活动准入规则:活动规则的实现比较有技巧性,楼主未曾展开讲。挑战点就是如何将一段字符串转换为成Java类,核心代码见com.cgx.marketing.domain.model.activity.rule.ActivityRuleRegistrar 和 com.cgx.marketing.domain.model.activity.rule.BaseActivityRule,看了不后悔!
  • 工程里面提供了一个单测模拟1000个用户并发10万次扣库存请求,耗时约32秒,即系统扣库存单机Qps达到3000+;读者可以从单测入手,通过调试逐渐加深对代码的理解。单测代码见com.cgx.marketing.application.activity.StockAppServiceTest;最后再次建议参照楼主Github项目的README文档把工程Run起来,上手更快!

原文链接: https://blog.csdn.net/caiguoxi ... 34759 ,作者:温柔一cai刀


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK