如何应对高并发下的购票压力
为什么有分布式锁了还要加幂等组件?
可能小伙伴会有这样的疑惑,直接使用分布式锁不就行了,为什么还要额外设计出幂等组件?首先直接使用分布式锁是可以实现幂等的,当然业务逻辑验证也要做验证,但其实分布式锁会浪费一些性能
分布式锁的特点是多个请求并发执行,这些请求是来自不同的用户,也就是这些请求虽然要依次等待锁执行,但最终还是要把这些请求都执行完的(执行时间太长超时的异常情况排除),总结起来就是都要获得锁,没有获得锁的请求,也要争取获得锁接着执行
幂等的特点也是多个请求并发执行,但这些请求是来自同一个用户,也就是说这些请求只要保证第一个请求能执行,其余的请求要直接拒绝掉,总结起来就是只有第一个请求获得锁执行就可以,其余的请求看到已经上了锁,那么就要直接结束掉
分布式锁
com.damai.lock.ProgramOrderLock#createV1
@ServiceLock(name = PROGRAM_ORDER_CREATE_V1,keys = {"#programOrderCreateDto.programId"})
/**
* 节目服务订单创建V1
* */
public final static String PROGRAM_ORDER_CREATE_V1 = "d_program_order_create_v1_lock";
分布式锁使用节目id作为锁,关于分布式锁组件的使用和详细的设计可跳转到相应的文档
组合模式的业务验证
com.damai.service.ProgramOrderService#create
//进行业务验证
compositeContainer.execute(CompositeCheckType.PROGRAM_ORDER_CREATE_CHECK.getValue(),programOrderCreateDto);
此组环是使用了组合模式和树形结构来将业务的验证逻辑进行复用并且串联起来按照树形结构执行
用户购票验证逻辑的类型为program_order_create_check
,验证的逻辑有
- 验证座位参数
- 将节目缓存
- 验证缓存是否存在节目数据
- 验证用户是否存在
执行业务逻辑
com.damai.service.ProgramOrderService#create
在经过幂等、分布锁、业务验证后,开始执行真正的业务流程处理环节
public String create(ProgramOrderCreateDto programOrderCreateDto) {
//从多级缓存中查找节目演出时间ProgramShowTime
ProgramShowTime programShowTime =
programShowTimeService.selectProgramShowTimeByProgramIdMultipleCache(programOrderCreateDto.getProgramId());
//查询对应的票档类型
List<TicketCategoryVo> getTicketCategoryList =
getTicketCategoryList(programOrderCreateDto,programShowTime.getShowTime());
//传入的座位总价格
BigDecimal parameterOrderPrice = new BigDecimal("0");
//库中的座位总价格
BigDecimal databaseOrderPrice = new BigDecimal("0");
//要购买的座位
List<SeatVo> purchaseSeatList = new ArrayList<>();
//入参的座位
List<SeatDto> seatDtoList = programOrderCreateDto.getSeatDtoList();
//该节目下所有未售卖的座位
List<SeatVo> seatVoList = new ArrayList<>();
//该节目下的余票数量
Map<String, Long> ticketCategoryRemainNumber = new HashMap<>(16);
//遍历得到的票档
for (TicketCategoryVo ticketCategory : getTicketCategoryList) {
//从缓存中查询座位
List<SeatVo> allSeatVoList =
seatService.selectSeatResolution(programOrderCreateDto.getProgramId(), ticketCategory.getId(),
DateUtils.countBetweenSecond(DateUtils.now(), programShowTime.getShowTime()), TimeUnit.SECONDS);
//将查询到未售卖的座位放入seatVoList
seatVoList.addAll(allSeatVoList.stream().
filter(seatVo -> seatVo.getSellStatus().equals(SellStatus.NO_SOLD.getCode())).toList());
//将查询到的余票数量放入ticketCategoryRemainNumber key:票档id value:余票数量
ticketCategoryRemainNumber.putAll(ticketCategoryService.getRedisRemainNumberResolution(
programOrderCreateDto.getProgramId(),ticketCategory.getId()));
}
//入参座位存在
if (CollectionUtil.isNotEmpty(seatDtoList)) {
//余票数量检测 key:票档id value:票档数量
Map<Long, Long> seatTicketCategoryDtoCount = seatDtoList.stream()
.collect(Collectors.groupingBy(SeatDto::getTicketCategoryId, Collectors.counting()));
for (Entry<Long, Long> entry : seatTicketCategoryDtoCount.entrySet()) {
Long ticketCategoryId = entry.getKey();
Long purchaseCount = entry.getValue();
//余票数量
Long remainNumber = Optional.ofNullable(ticketCategoryRemainNumber.get(String.valueOf(ticketCategoryId)))
.orElseThrow(() -> new DaMaiFrameException(BaseCode.TICKET_CATEGORY_NOT_EXIST_V2));
//如果购买数量大于余票数量,那么提示数量不足
if (purchaseCount > remainNumber) {
throw new DaMaiFrameException(BaseCode.TICKET_REMAIN_NUMBER_NOT_SUFFICIENT);
}
}
//循环入参的座位对象
for (SeatDto seatDto : seatDtoList) {
//验证入参的对象在库中的状态,不存在、已锁、已售卖
Map<String, SeatVo> seatVoMap = seatVoList.stream().collect(Collectors
.toMap(seat -> seat.getRowCode() + "-" + seat.getColCode(), seat -> seat, (v1, v2) -> v2));
SeatVo seatVo = seatVoMap.get(seatDto.getRowCode() + "-" + seatDto.getColCode());
//如果入参的座位在未售卖的座位中不存在,那么直接抛出异常提示
if (Objects.isNull(seatVo)) {
throw new DaMaiFrameException(BaseCode.SEAT_IS_NOT_NOT_SOLD);
}
purchaseSeatList.add(seatVo);
//将入参的座位价格进行累加
parameterOrderPrice = parameterOrderPrice.add(seatDto.getPrice());
//将库中的座位价格进行类型
databaseOrderPrice = databaseOrderPrice.add(seatVo.getPrice());
}
//传入的座位价格累加不能大于存放的相应座位累加价格
if (parameterOrderPrice.compareTo(databaseOrderPrice) > 0) {
throw new DaMaiFrameException(BaseCode.PRICE_ERROR);
}
}else {
//入参座位不存在,利用算法自动根据人数和票档进行分配相邻座位
Long ticketCategoryId = programOrderCreateDto.getTicketCategoryId();
Integer ticketCount = programOrderCreateDto.getTicketCount();
//余票检测
Long remainNumber = Optional.ofNullable(ticketCategoryRemainNumber.get(String.valueOf(ticketCategoryId)))
.orElseThrow(() -> new DaMaiFrameException(BaseCode.TICKET_CATEGORY_NOT_EXIST_V2));
//如果购票的数量大于余票数量,则直接抛出异常提示
if (ticketCount > remainNumber) {
throw new DaMaiFrameException(BaseCode.TICKET_REMAIN_NUMBER_NOT_SUFFICIENT);
}
//用算法匹配座位
purchaseSeatList = SeatMatch.findAdjacentSeatVos(seatVoList.stream().filter(seatVo ->
Objects.equals(seatVo.getTicketCategoryId(), ticketCategoryId)).collect(Collectors.toList()), ticketCount);
//如果匹配出来的座位数量小于要购买的数量,拒绝执行
if (purchaseSeatList.size() < ticketCount) {
throw new DaMaiFrameException(BaseCode.SEAT_OCCUPY);
}
}
//进行操作缓存中的数据
updateProgramCacheDataResolution(programOrderCreateDto.getProgramId(),purchaseSeatList,OrderStatus.NO_PAY);
//将筛选出来的购买的座位信息传入,执行创建订单的操作
return doCreate(programOrderCreateDto,purchaseSeatList);
}
获取节目演出时间
先从缓存中获取获取 节目演出时间ProgramShowTime,此数据在查询节目详情的时候放入了进去,所以这里一定会存在
- 点赞
- 收藏
- 关注作者
评论(0)