文章目录

  • 一、前言
  • 二、工程骨架
    • 2.1 DDD概述
    • 2.2 工程结构
  • 三、源码解读
    • 3.1领域层
      • 3.1.1 领域模型
        • a. 活动
        • b. 活动商品
        • c. 库存扣减流水
        • d. 仓储
      • 3.1.2 领域服务
        • a. 活动配置
        • b. 库存扣减
      • 3.1.3 小结
    • 3.2 应用层
      • 3.2.1 活动应用服务
      • 3.2.2 库存应用服务
      • 3.2.3 小结
    • 3.3 用户界面层
    • 3.4 基础设施层
      • 3.4.1 领域服务实现
      • 3.4.2 仓储实现
  • 四、总结

一、前言

在上一篇《一个极简、高效的秒杀系统-战略设计篇》中,楼主重点讲解了基于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);}

3.1.3 小结

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

3.2 应用层

3.2.1 活动应用服务

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

按图索骥,毫不费力就推导出如下应用服务接口定义:其中配置活动、启用/禁用活动的业务逻辑,可直接借用活动领域服务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 {@Resourceprivate ActivityRepository activityRepository;@Resourceprivate ActivityItemRepository activityItemRepository;@Resourceprivate ItemSalesRepository itemSalesRepository;@Resourceprivate ActivityAssembler activityAssembler;@Resourceprivate ActivityService activityService;/*** 配置活动及商品列表*/@Overridepublic 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();}/*** 启用/禁用活动*/@Overridepublic void changeActivityStatus(UpdateActivityStatusCommand command) {ActivityId activityId = new ActivityId(command.getActivityId());activityService.enableActivity(activityId, command.isEnabled());}/*** 活动列表*/@Overridepublic List<ActivityDTO> activityList() {List<Activity> activityList = activityRepository.listActivity();return activityList.stream().map(act -> activityAssembler.asActivityDTO(act)).collect(Collectors.toList());}/*** 活动详情页(透出活动商品及商品销量)*/@Overridepublic 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();}/*** 活动商品详情*/@Overridepublic 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和多个仓储对象(如: ActivityRepositoryActivityItemRepositoryItemSalesRepository);
  • 配置活动、启用/禁用活动:直接交给领域服务来完成;
  • 查活动列表、活动详情、活动商品详情: 直接利用仓储对象读取数据,并进行数据整合;不需要领域服务参与。

3.2.2 库存应用服务

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

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

  • 接口定义
/*** 库存扣减应用服务*/
public interface StockAppService {/*** 扣库存*/StockReduceResult reduce(ReduceCommand command);/*** 回库存*/StockReduceResult cancelReduce(CancelReduceCommand command);}
  • 接口实现
/*** 库存扣减应用服务*/
@Service
public class StockAppServiceImpl implements StockAppService {@Resourceprivate ActivityRepository activityRepository;@Resourceprivate StockReduceFlowRepository stockReduceFlowRepository;@Resourceprivate StockReduceFlowAssembler stockReduceFlowAssembler;@Resourceprivate StockReduceService stockReduceService;/*** 扣库存*/@Overridepublic 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);}/*** 回库存*/@Overridepublic 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,得到库存扣减流水,然后直接调用库存扣减领域服务进行回库存操作。

3.2.3 小结

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

3.3 用户界面层

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

  • 活动接口
@Api(value = "活动接口")
@RestController
@RequestMapping("/api/v1/activity")
public class ActivityController {@Resourceprivate 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 {@Resourceprivate 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 就会看到如下界面:

3.4 基础设施层

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

3.4.1 领域服务实现

  • 库存扣减领域服务实现类StockReduceServiceImpl
@Slf4j
@Service
public class StockReduceServiceImpl implements StockReduceService {@Resourceprivate Gson gson;@Resourceprivate RedissonClient redissonClient;private String reduceLua;private String cancelReduceLua;@PostConstructpublic 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脚本扣库存*/@Overridepublic 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脚本回库存*/@Overridepublic 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 thenreturn "REPEATED_REQUEST";
end-- 校验商品是否参加了活动
local ACTIVITY_ITEMS_HASH = KEYS[2];
local activityExists = redis.call("HEXISTS", ACTIVITY_ITEMS_HASH, itemId);
if activityExists == 0 thenreturn "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 thenbookedCount = 0;
end
if bookedCount + tonumber(ARGV[4]) > tonumber(payload["quota"]) thenreturn "QUOTA_NOT_ENOUGH";
end-- 商品累计售出数量
local ITEM_SALES_HASH = KEYS[4];
local soldCount = redis.call("HGET", ITEM_SALES_HASH, itemId);
if soldCount == false thensoldCount = 0;
end
if soldCount + tonumber(ARGV[4]) > tonumber(payload["stock"]) thenreturn "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 thenreturn "NO_REDUCE_FLOW";
endlocal 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";

3.4.2 仓储实现

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

  • 仓储实现类ActivityRepositoryImpl: 利用的是Redis的Hash结构,活动信息存储在activity_catalog这个Hash结构中
@Repository
public class ActivityRepositoryImpl implements ActivityRepository {@Resourceprivate Gson gson;@Resourceprivate RedissonClient redissonClient;/*** 活动列表*/@Overridepublic 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查活动*/@Overridepublic 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);}/*** 保存活动*/@Overridepublic 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.ActivityRuleRegistrarcom.cgx.marketing.domain.model.activity.rule.BaseActivityRule,看了不后悔!

  • 工程里面提供了一个单测模拟1000个用户并发10万次扣库存请求,耗时约32秒,即系统扣库存单机Qps达到3000+;读者可以从单测入手,通过调试逐渐加深对代码的理解。单测代码见com.cgx.marketing.application.activity.StockAppServiceTest;最后再次建议参照楼主Github项目的README文档把工程Run起来,上手更快!

写作不易,如果有收获,点个赞呗~

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

  1. Java毕设项目在线交友系统2021计算机(附源码+系统+数据库+LW)

    Java毕设项目在线交友系统2021计算机(附源码+系统+数据库+LW) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ ...

  2. 电脑报价管理系统C语言,C语言笔记本电脑销售系统课设(附源码).doc

    PAGE PAGE 1 C语言笔记本电脑销售系统课设 项目说明 本系统基于C语言开发,适用于刚入门的C语言新手项目课设,开发软件采用VC++6.0开发,VS,DEV C++等均可运行.(书生) 项目运 ...

  3. 计算机毕业设计,vue+springboot的农产品溯源系统,内附源码

    基于springboot前后端分离的农产品溯源系统 计算机毕业设计--基于springboot前后端分离的农产品溯源系统 文章目录 基于springboot前后端分离的农产品溯源系统 前言 一.关键技 ...

  4. 手把手教你使用FineUI开发一个b/s结构的取送货管理信息系统(附源码+视频教程(第9节))...

    一 本系列随笔概览及产生的背景 近阶段接到一些b/s类型的软件项目,但是团队成员之前大部分没有这方面的开发经验,于是自己选择了一套目前网上比较容易上手的开发框架(FineUI),计划录制一套视频讲座, ...

  5. 手把手教你使用FineUI开发一个b/s结构的取送货管理信息系统(附源码+视频教程(第6节))...

    一 本系列随笔概览及产生的背景 近阶段接到一些b/s类型的软件项目,但是团队成员之前大部分没有这方面的开发经验,于是自己选择了一套目前网上比较容易上手的开发框架(FineUI),计划录制一套视频讲座, ...

  6. 取消预约的c语言代码大全,C语言机房机位预约系统课设(附源码).doc

    PAGE PAGE 1 C语言机房机位预约系统课设 项目说明 本系统基于C语言开发,适用于刚入门的C语言新手项目课设,开发软件采用VC++6.0开发,VS,DEV C++等均可运行.(书生) 项目运行 ...

  7. 一个快速测试PlayCanvas Demo 的工程(内附源码)

    PlayCanvas Paoject 一个快速测试PlayCanvas Demo 的工程. 源码下载: PlayCanvas Paoject下载地址 操作说明: 1.安装依赖 npm install ...

  8. jsp+ssm计算机毕业设计中医药系统论文2022【附源码】

    项目运行 环境配置: Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclis ...

  9. 基于matlab仿真相控天线阵列在波束成形MIMO-OFDM系统中的使用(附源码)

    一.前言 本例显示了相控阵在采用波束成形的MIMO-OFDM通信系统中的使用.它使用通信工具箱和相控阵系统工具箱中的组件,对组成发射器和前端接收器组件的辐射元件进行建模,用于MIMO-OFDM通信系统 ...

最新文章

  1. 源码资本张宏江:只有算法和技术,那你一定挣不到钱
  2. 【Java】判断字符串是否含字母
  3. 最佳实践系列:前端代码标准和最佳实践
  4. linux 静态配置多个ip,linux 配置静态IP
  5. OpenGL学习笔记以及其它学习思考
  6. js 日期星期 带农历
  7. 搜狐被SEC列入“预摘牌名单”!回应:不打算提出异议
  8. presto 使用 部署_探秘Presto+Alluxio高效云端SQL查询
  9. Linux内核4.17再获捷报
  10. Sqlserver 特殊字符替换
  11. 如何使用迭代器Iterator与增强for循环遍历Map集合?
  12. PAT MOOC期终成绩(map+结构体)
  13. PPT转换成图片及合成长图
  14. 样本T检验、方差分析(ANOVA)、wilcoxon秩和检验、KW秩和检验详解和操作步骤
  15. 【西语】【6】el amor es 什么是爱
  16. excel服务器 微信设置密码,如何用vba给excel工作簿批量设置添加打开密码? - EXCEL VBA - ExcelOffice【微信公众号:水星Excel】...
  17. 【Appium】手机滑动swipe方法及如何进行坐标定位
  18. 分辨率1080P、2K、4K、8K的含义和区别
  19. 电影QQ群怎么引流?电影的社群营销要怎么做?
  20. Neural Ordinary Differential Equations

热门文章

  1. Flutter封装 AppBar
  2. 安卓游戏帧数如何测试软件,Android如何测试微信小游戏和小程序?
  3. Android 系统-进入recovery的问题集
  4. 语音合成论文优选:音素韵律控制Prosodic Clustering for Phoneme-level Prosody Control in End-to-End Speech Synthesis
  5. unity模型黑色的问题
  6. 世界最优秀的b2b网站汇总 世界b2b网站排名
  7. Drools 将DSLR转化为DRL
  8. DSLR(digital single lens reflex)
  9. Android中SQLite数据库查看及导入导出
  10. Python如何启动windows本地程序