当前位置: 首页 > news >正文

基于RabbitMQ,Redis,Redisson,RocketMQ四种技术实现订单延时关闭功能及其相关优缺点介绍(以12306为主题)

目录

1. 延迟关闭订单

1.1 订单延时关闭功能技术选型

1.1.1 定时任务

1.1.2 RabbitMQ

1.1.3 Redis 过期监听

1.1.4 Redisson

1.1.5 RocketMQ

1.2 RocketMQ订单延时关闭发送方实现

1.3 RocketMQ订单延时关闭的消费方实现


1. 延迟关闭订单

用户发起订单后,如果长时间未支付,需要将订单关闭,也就是大家常说的延时关闭订单。12306的PC端的延迟时间是10分钟:

1.1 订单延时关闭功能技术选型

1.1.1 定时任务

通过定时任务是一种常见的订单延迟关闭解决方案。

可以通过调度平台来实现定时任务的执行,具体任务是根据订单创建时间扫描所有到期的订单,并执行关闭订单的操作。

大家常用的定时任务调度平台有以下这些:

  • https://github.com/xuxueli/xxl-job

  • https://github.com/PowerJob/PowerJob

  • https://github.com/apache/shardingsphere-elasticjob

这种方案的优点在于简单易实现,但是,该方案也存在一些问题:

  1. 延迟时间不精确:使用定时任务执行订单关闭逻辑,无法保证订单在十分钟后准确地关闭。如果任务执行器在关闭订单的具体时间点出现问题,可能导致订单关闭的时间延后。

  2. 不适合高并发场景:定时任务执行的频率通常是固定的,无法根据实际订单的情况来灵活调整。在高并发场景下,可能导致大量的定时任务同时执行,造成系统负载过大。

  3. 分库分表问题:拿 12306 来说,订单表按照用户标识和订单号进行了分库分表,那这样的话,和上面说的根据订单创建时间去扫描一批订单进行关闭,自然就行不通。因为根据创建时间查询没有携带分片键,存在读扩散问题。

通常最不推荐的方式是使用定时任务来实现订单关闭。

1.1.2 RabbitMQ

RabbitMQ 是一个功能强大的消息中间件,通过使用 RabbitMQ 的延时消息特性,我们可以轻松实现订单十分钟延时关闭功能。首先,我们需要在 RabbitMQ 服务器上启用延时特性,通常通过安装 rabbitmq_delayed_message_exchange 插件来支持延时消息功能。

接下来,我们创建两个队列:订单队列和死信队列。订单队列用于存储需要延时关闭的订单消息,而死信队列则用于存储延时时间到达后的订单消息。在创建订单队列时,我们要为队列配置延时特性,指定订单消息的延时时间,比如十分钟。这样,当有新的订单需要延时关闭时,我们只需要将订单消息发送到订单队列,并设置消息的延时时间。

在订单队列中设置死信交换机和死信队列,当订单消息的延时时间到达后,消息会自动转发到死信队列,从而触发关闭订单的操作。在死信队列中,我们可以监听消息,并执行关闭订单的逻辑。为了确保消息的可靠性,可以在关闭订单操作前添加适当的幂等性措施,这样即使消息重复处理,也不会对系统产生影响。

通过以上步骤,我们就成功实现了订单的十分钟延时关闭功能。当有新的订单需要延时关闭时,将订单消息发送到订单队列,并设置延时时间。在延时时间到达后,订单消息会自动进入死信队列,从而触发关闭订单的操作。这种方式既简单又可靠,保证了系统的稳定性和可用性。

从整体来说 RabbitMQ 实现延时关闭订单功能是比较合适的,但也存在几个问题:

  1. 延时精度:RabbitMQ 的延时消息特性是基于消息的 TTL(Time-To-Live)来实现的,因此消息的延时时间并不是完全准确的,可能会有一定的误差。在处理订单十分钟延时关闭时,可能会有一些订单的关闭时间略晚于预期时间。

  2. 高并发问题:如果系统中有大量的订单需要延时关闭,而订单关闭操作非常复杂耗时,可能会导致消息队列中的消息堆积。这样就可能导致延时关闭操作无法及时处理,影响订单的实际关闭时间。

  3. 重复消息问题:由于网络原因或其他不可预知的因素,可能会导致消息重复发送到订单队列。如果没有处理好消息的幂等性,可能会导致订单重复关闭的问题,从而造成数据不一致或其他异常情况。

  4. 可靠性问题:RabbitMQ 是一个消息中间件,它是一个独立的系统。如果 RabbitMQ 本身出现故障或宕机,可能会导致订单延时关闭功能失效。因此,在使用 RabbitMQ 实现延时关闭功能时,需要考虑如何保证 RabbitMQ 的高可用性和稳定性。

延时精度和高并发属于一类问题,取决于客户端的消费能力。重复消费问题是所有消息中间件都需要解决,需要通过消息表等幂等解决方案解决。比较难搞定的是可用性问题,RabbitMQ 在可用性方面较弱,部分场景下会存在单点故障问题。

1.1.3 Redis 过期监听

可以借助 Redis 的过期消息监听机制实现延时关闭功能。

首先,在订单创建时,将订单信息存储到 Redis,并设置过期时间为十分钟。同时,在 Redis 中存储一个过期消息监听的键值对,键为订单号,值为待处理订单的标识。

其次,编写一个消息监听器,持续监听 Redis 的过期事件。监听器使用 Redis 的 PSUBSCRIBE 命令订阅过期事件,并在监听到过期事件时触发相应的处理逻辑。

当订单过期时间到达时,Redis 会自动触发过期事件,消息监听器捕获到该事件,并获取到过期的订单号。接着,监听器执行订单关闭的逻辑,如更新订单状态为关闭状态,释放相关资源等,实现订单的十分钟延时关闭功能。

需要注意的是,消息监听器应该是一个长期运行的任务,确保持续监听 Redis 的过期事件。为了保证系统的稳定性和可靠性,可以在实现订单关闭逻辑时添加容错机制,以应对 Redis 可能发生故障或重启的情况。

Redis 过期消息也存在几个问题:

  1. 不够精确:Redis 的过期时间是通过定时器实现的,可能存在一定的误差,导致订单的关闭时间不是精确的十分钟。这对于某些对时间要求较高的场景可能不适用。

  2. Redis 宕机:如果 Redis 宕机或重启,那些已经设置了过期时间但还未过期的订单信息将会丢失,导致这部分订单无法正确关闭。需要考虑如何处理这种异常情况。

  3. 可靠性:依赖 Redis 的过期时间来实现订单关闭功能,需要确保 Redis 的高可用性和稳定性。如果 Redis 发生故障或网络问题,可能导致订单关闭功能失效。

  4. 版本问题:Redis 5.0 之前是不保证延迟消息持久化的,如果客户端消费过程中宕机或者重启,这个消息不会重复投递。5.0 之后推出了 Stream 功能,有了持久化等比较完善的延迟消息功能。

1.1.4 Redisson

通过 Redisson 的 RDelayedQueue 功能可以实现订单十分钟延时关闭的功能。

首先,我们需要创建一个 RDelayedQueue 对象,用于存放需要延时关闭的订单信息。当用户创建订单时,我们将订单信息添加到 RDelayedQueue 中,并设置订单的延时时间为十分钟。

Redisson 提供了监听功能,可以实现对 RDelayedQueue 中订单信息的监听。一旦订单到达设定的延时时间,Redisson 会触发监听事件。在监听到订单的延时事件后,我们可以编写相应的处理逻辑,即关闭对应的订单。

在处理订单关闭时,我们可以根据订单号或订单创建时间等信息,来找到对应的订单进行关闭操作。

不过这种方式也不推荐使用,基本上 Redis 过期监听消息存在的问题,RDelayedQueue 也都会有,因为 RDelayedQueue 本质上也是依赖 Redis 实现。

1.1.5 RocketMQ

在订单生成时,我们将订单关闭消息发送到 RocketMQ,并设置消息的延迟时间为十分钟。RocketMQ 支持设置消息的延迟时间,可以通过设置消息的 delayLevel 来指定延迟级别,每个级别对应一种延迟时间。这样,订单关闭消息将在十分钟后自动被消费者接收到。

需要注意,RocketMQ 5.0 之后已经支持了自定义时间的延迟,而不仅是延迟级别范围内的时间。

为了处理订单关闭消息,我们需要在消费者端创建一个消息监听器。当消息监听器接收到订单关闭消息时,触发订单关闭操作,将订单状态设置为关闭状态。

需要注意的是,RocketMQ 的消息传递机制保证了消息的可靠性传递,因此消息可能会进行多次重试。为了确保订单关闭操作的幂等性,即多次执行不会产生副作用,我们需要在订单关闭逻辑中进行幂等性的处理。

铁路 12306 系统中,使用 RocketMQ 作为延时关闭订单的技术实现,从整体考虑,处理能力以及稳定性相对来说较为合适。

1.2 RocketMQ订单延时关闭发送方实现

在12306中,当订单添加完成之后, 在OrderService方,会延迟10分钟,发送一个关闭订单的消息给到MQ, 而关闭订单的消费方订阅这个消息,进行未及时支付的订单进行取消操作.

1) 我们创建一个延迟关闭订单的事件实体类 DelayCloseOrderEvent

package com.fs.fs12306.biz.orderservice.mq.event;
​
import com.fs.fs12306.biz.orderservice.dto.req.TicketOrderItemCreateReqDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
​
import java.util.List;
​
/*** 延迟关闭订单事件*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DelayCloseOrderEvent {
​/*** 车次 ID*/private String trainId;
​/*** 出发站点*/private String departure;
​/*** 到达站点*/private String arrival;
​/*** 订单号*/private String orderSn;
​/*** 乘车人购票信息*/private List<TicketOrderItemCreateReqDTO> trainPurchaseTicketResults;
}

2) 消息发送事件基础扩充属性实体 BaseSendExtendDTO

package com.fs.fs12306.biz.orderservice.mq.produce;
​
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
​
/*** 消息发送事件基础扩充属性实体**/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public final class BaseSendExtendDTO {
​/*** 事件名称*/private String eventName;
​/*** 主题*/private String topic;
​/*** 标签*/private String tag;
​/*** 业务标识*/private String keys;
​/*** 发送消息超时时间*/private Long sentTimeout;
​/*** 延迟消息*/private Integer delayLevel;
}

3) 把发送消息封装成一个模版类AbstractCommonSendProduceTemplate,目的是为了提供一个抽象的公共类,方便在项目中发送RocketMQ消息。

package com.fs.fs12306.biz.orderservice.mq.produce;
​
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.Message;
​
import java.util.Optional;
​
/*** RocketMQ 抽象公共发送消息组件**/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCommonSendProduceTemplate<T> {
​private final RocketMQTemplate rocketMQTemplate;
​/*** 构建消息发送事件基础扩充属性实体* @param messageSendEvent 消息发送事件* @return 扩充属性实体*/protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);
​/*** 构建消息基本参数,请求头、Keys...** @param messageSendEvent 消息发送事件* @param requestParam     扩充属性实体* @return 消息基本参数*/protected abstract Message<?> buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);
​/*** 消息事件通用发送** @param messageSendEvent 消息发送事件* @return 消息发送返回结果*/public SendResult sendMessage(T messageSendEvent) {//构建基础扩充属性实体BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);SendResult sendResult;try {//构建消息的topic和tag,格式: topic:tagStringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {destinationBuilder.append(":").append(baseSendExtendDTO.getTag());}//同步发送消息sendResult = rocketMQTemplate.syncSend(destinationBuilder.toString(),buildMessage(messageSendEvent, baseSendExtendDTO),baseSendExtendDTO.getSentTimeout(),Optional.ofNullable(baseSendExtendDTO.getDelayLevel()).orElse(0));log.info("[{}] 消息发送结果:{},消息ID:{},消息Keys:{}", baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());} catch (Throwable ex) {log.error("[{}] 消息发送失败,消息体:{}", baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);throw ex;}return sendResult;}
}

AbstractCommonSendProduceTemplate定义了一个抽象的RocketMQ消息发送模板,具体的实现需要子类根据具体需求进行。通过这种方式,可以在不同的场景下复用消息发送的逻辑,提高代码的可维护性和可扩展性。

4) 编写延迟关闭订单生产者 DelayCloseOrderSendProduce

让延迟关闭订单生产者继承AbstractCommonSendProduceTemplate类,复用消息发送的逻辑

package com.fs.fs12306.biz.orderservice.mq.produce;
​
import cn.hutool.core.util.StrUtil;
import com.fs.fs12306.biz.orderservice.common.constant.OrderRocketMQConstant;
import com.fs.fs12306.biz.orderservice.mq.domain.MessageWrapper;
import com.fs.fs12306.biz.orderservice.mq.event.DelayCloseOrderEvent;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageConst;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
​
import java.util.UUID;
​
/*** 延迟关闭订单生产者**/
@Slf4j
@Component
public class DelayCloseOrderSendProduce extends AbstractCommonSendProduceTemplate<DelayCloseOrderEvent> {
​private final ConfigurableEnvironment environment;
​public DelayCloseOrderSendProduce(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {super(rocketMQTemplate);this.environment = environment;}
​@Overrideprotected BaseSendExtendDTO buildBaseSendExtendParam(DelayCloseOrderEvent messageSendEvent) {return BaseSendExtendDTO.builder().eventName("延迟关闭订单").keys(messageSendEvent.getOrderSn()).topic(environment.resolvePlaceholders(OrderRocketMQConstant.ORDER_DELAY_CLOSE_TOPIC_KEY)).tag(environment.resolvePlaceholders(OrderRocketMQConstant.ORDER_DELAY_CLOSE_TAG_KEY)).sentTimeout(2000L)// RocketMQ 延迟消息级别 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h.delayLevel(14).build();}
​@Overrideprotected Message<?> buildMessage(DelayCloseOrderEvent messageSendEvent, BaseSendExtendDTO requestParam) {String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();return MessageBuilder.withPayload(new MessageWrapper(requestParam.getKeys(), messageSendEvent)).setHeader(MessageConst.PROPERTY_KEYS, keys).setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag()).build();}
}

对于消息的负载信息,我们进行再次封装: MessageWrapper

package com.fs.fs12306.biz.orderservice.mq.domain;
​
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
​
import java.io.Serializable;
import java.util.UUID;
​
/*** 消息体包装器**/
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {
​private static final long serialVersionUID = 1L;
​/*** 消息发送 Keys*/@NonNullprivate String keys;
​/*** 消息体*/@NonNullprivate T message;
​/*** 唯一标识,用于客户端幂等验证*/private String uuid = UUID.randomUUID().toString();
​/*** 消息发送时间*/private Long timestamp = System.currentTimeMillis();
}
​

在往数据库插入订单相关的数据之后,我们就使用延时关闭订单的生产者延迟发送一个消息,从而实现延时关闭订单的功能

try {// 发送 RocketMQ 延时消息,指定时间后取消订单DelayCloseOrderEvent delayCloseOrderEvent = DelayCloseOrderEvent.builder().trainId(String.valueOf(requestParam.getTrainId())).departure(requestParam.getDeparture()).arrival(requestParam.getArrival()).orderSn(orderSn).trainPurchaseTicketResults(requestParam.getTicketOrderItems()).build();SendResult sendResult = delayCloseOrderSendProduce.sendMessage(delayCloseOrderEvent);if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {throw new ServiceException("投递延迟关闭订单消息队列失败");}} catch (Throwable ex) {log.error("延迟关闭订单消息队列发送错误,请求参数:{}", JSON.toJSONString(requestParam), ex);throw ex;}

1.3 RocketMQ订单延时关闭的消费方实现

12306中的关闭订单的消费方是在TicketService实现的.可能会有一个疑问,为什么延迟关闭订单发送端在订单服务,消费延迟关闭订单消息却是在购票服务?

如果消息的消费在订单服务中进行,那么订单服务就需要调用购票服务来进行座位的释放或订单状态的更新。这样就形成了一个远程循环依赖:订单服务依赖于购票服务,同时购票服务又依赖于订单服务。这对于微服务涉及中是需要尽可能避免的。所以,最终将消费延迟关闭消息放在了购票服务。

在ticketService服务中,也定义一个延时关闭订单的事件实体类, 与发送方的延时关闭订单事件的实体类是一样,

package com.fs.fs12306.biz.ticketservice.mq.event;
​
import com.fs.fs12306.biz.ticketservice.service.handler.ticket.dto.TrainPurchaseTicketRespDTO;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
​
import java.util.List;
​
/*** 延迟关闭订单事件**/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DelayCloseOrderEvent {
​/*** 车次 ID*/private String trainId;
​/*** 出发站点*/private String departure;
​/*** 到达站点*/private String arrival;
​/*** 订单号*/private String orderSn;
​/*** 乘车人购票信息*/private List<TrainPurchaseTicketRespDTO> trainPurchaseTicketResults;
}
​

封装消息体包装器MessageWrapper

package com.fs.fs12306.biz.ticketservice.mq.domain;
​
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
​
import java.io.Serializable;
import java.util.UUID;
​
/*** 消息体包装器**/
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {
​private static final long serialVersionUID = 1L;
​/*** 消息发送 Keys*/@NonNullprivate String keys;
​/*** 消息体*/@NonNullprivate T message;
​/*** 唯一标识,用于客户端幂等验证*/private String uuid = UUID.randomUUID().toString();
​/*** 消息发送时间*/private Long timestamp = System.currentTimeMillis();
}
​

这里有一个可以优化的地方,我们发现DelayCloseOrderEvent,MessageWrapper在OrderService和ticketService重复编写了, 可以把DelayCloseOrderEvent类抽取出来,放到一个公共模块中.

编写延时关闭订单的消费者DelayCloseOrderConsumer

package com.fs.fs12306.biz.ticketservice.mq.consumer;
​
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.fs.fs12306.biz.ticketservice.common.constant.TicketRocketMQConstant;
import com.fs.fs12306.biz.ticketservice.dto.domain.RouteDTO;
import com.fs.fs12306.biz.ticketservice.dto.req.CancelTicketOrderReqDTO;
import com.fs.fs12306.biz.ticketservice.mq.domain.MessageWrapper;
import com.fs.fs12306.biz.ticketservice.mq.event.DelayCloseOrderEvent;
import com.fs.fs12306.biz.ticketservice.remote.TicketOrderRemoteService;
import com.fs.fs12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO;
import com.fs.fs12306.biz.ticketservice.remote.dto.TicketOrderPassengerDetailRespDTO;
import com.fs.fs12306.biz.ticketservice.service.SeatService;
import com.fs.fs12306.biz.ticketservice.service.TrainStationService;
import com.fs.fs12306.biz.ticketservice.service.handler.ticket.dto.TrainPurchaseTicketRespDTO;
import com.fs.fs12306.biz.ticketservice.service.handler.ticket.tokenbucket.TicketAvailabilityTokenBucket;
import com.fs.fs12306.frameworks.starter.cache.DistributedCache;
import com.fs.fs12306.frameworks.starter.common.util.BeanUtil;
import com.fs.fs12306.frameworks.starter.convention.result.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
​
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
​
import static com.fs.fs12306.biz.ticketservice.common.constant.RedisKeyConstant.TRAIN_STATION_REMAINING_TICKET;
​
/*** 延迟关闭订单消费者**/
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(topic = TicketRocketMQConstant.ORDER_DELAY_CLOSE_TOPIC_KEY,selectorExpression = TicketRocketMQConstant.ORDER_DELAY_CLOSE_TAG_KEY,consumerGroup = TicketRocketMQConstant.TICKET_DELAY_CLOSE_CG_KEY
)
public final class DelayCloseOrderConsumer implements RocketMQListener<MessageWrapper<DelayCloseOrderEvent>> {
​private final SeatService seatService;private final TicketOrderRemoteService ticketOrderRemoteService;private final TrainStationService trainStationService;private final DistributedCache distributedCache;private final TicketAvailabilityTokenBucket ticketAvailabilityTokenBucket;
​@Value("${ticket.availability.cache-update.type:}")private String ticketAvailabilityCacheUpdateType;
​@Overridepublic void onMessage(MessageWrapper<DelayCloseOrderEvent> delayCloseOrderEventMessageWrapper) {log.info("[延迟关闭订单] 开始消费:{}", JSON.toJSONString(delayCloseOrderEventMessageWrapper));DelayCloseOrderEvent delayCloseOrderEvent = delayCloseOrderEventMessageWrapper.getMessage();String orderSn = delayCloseOrderEvent.getOrderSn();Result<Boolean> closedTickOrder;try {//远程调用OrderService的关闭订单的接口closedTickOrder = ticketOrderRemoteService.closeTickOrder(new CancelTicketOrderReqDTO(orderSn));} catch (Throwable ex) {log.error("[延迟关闭订单] 订单号:{} 远程调用订单服务失败", orderSn, ex);throw ex;}if (closedTickOrder.isSuccess() && !StrUtil.equals(ticketAvailabilityCacheUpdateType, "binlog")) {if (!closedTickOrder.getData()) {log.info("[延迟关闭订单] 订单号:{} 用户已支付订单", orderSn);return;}String trainId = delayCloseOrderEvent.getTrainId();String departure = delayCloseOrderEvent.getDeparture();String arrival = delayCloseOrderEvent.getArrival();List<TrainPurchaseTicketRespDTO> trainPurchaseTicketResults = delayCloseOrderEvent.getTrainPurchaseTicketResults();try {//释放锁定的座位seatService.unlock(trainId, departure, arrival, trainPurchaseTicketResults);} catch (Throwable ex) {log.error("[延迟关闭订单] 订单号:{} 回滚列车DB座位状态失败", orderSn, ex);throw ex;}try {StringRedisTemplate stringRedisTemplate = (StringRedisTemplate) distributedCache.getInstance();Map<Integer, List<TrainPurchaseTicketRespDTO>> seatTypeMap = trainPurchaseTicketResults.stream().collect(Collectors.groupingBy(TrainPurchaseTicketRespDTO::getSeatType));List<RouteDTO> routeDTOList = trainStationService.listTakeoutTrainStationRoute(trainId, departure, arrival);//缓存中的余票加routeDTOList.forEach(each -> {String keySuffix = StrUtil.join("_", trainId, each.getStartStation(), each.getEndStation());seatTypeMap.forEach((seatType, trainPurchaseTicketRespDTOList) -> {stringRedisTemplate.opsForHash().increment(TRAIN_STATION_REMAINING_TICKET + keySuffix, String.valueOf(seatType), trainPurchaseTicketRespDTOList.size());});});TicketOrderDetailRespDTO ticketOrderDetail = BeanUtil.convert(delayCloseOrderEvent, TicketOrderDetailRespDTO.class);ticketOrderDetail.setPassengerDetails(BeanUtil.convert(delayCloseOrderEvent.getTrainPurchaseTicketResults(), TicketOrderPassengerDetailRespDTO.class));ticketAvailabilityTokenBucket.rollbackInBucket(ticketOrderDetail);} catch (Throwable ex) {log.error("[延迟关闭订单] 订单号:{} 回滚列车Cache余票失败", orderSn, ex);throw ex;}}}
}
​

OrderService的feign client接口

package com.fs.fs12306.biz.ticketservice.remote;
​
import com.fs.fs12306.biz.ticketservice.dto.req.CancelTicketOrderReqDTO;
import com.fs.fs12306.biz.ticketservice.dto.req.TicketOrderItemQueryReqDTO;
import com.fs.fs12306.biz.ticketservice.remote.dto.TicketOrderCreateRemoteReqDTO;
import com.fs.fs12306.biz.ticketservice.remote.dto.TicketOrderDetailRespDTO;
import com.fs.fs12306.biz.ticketservice.remote.dto.TicketOrderPassengerDetailRespDTO;
import com.fs.fs12306.frameworks.starter.convention.result.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
​
/*** 车票订单远程服务调用**/
@FeignClient(value = "fs12306-order-service", url = "${aggregation.remote-url:}")
public interface TicketOrderRemoteService {/*** 创建车票订单** @param requestParam 创建车票订单请求参数* @return 订单号*/@PostMapping("/api/order-service/order/ticket/create")Result<String> createTicketOrder(@RequestBody TicketOrderCreateRemoteReqDTO requestParam);
​/*** 车票订单关闭** @param requestParam 车票订单关闭入参* @return 关闭订单返回结果*/@PostMapping("/api/order-service/order/ticket/close")Result<Boolean> closeTickOrder(@RequestBody CancelTicketOrderReqDTO requestParam);
​/*** 车票订单取消** @param requestParam 车票订单取消入参* @return 订单取消返回结果*/@PostMapping("/api/order-service/order/ticket/cancel")Result<Void> cancelTicketOrder(@RequestBody CancelTicketOrderReqDTO requestParam);
}
​

接下来需要在OrderService服务中,实现对订单关闭的业务实现

package com.fs.fs12306.biz.orderservice.controller;
​
import cn.crane4j.annotation.AutoOperate;
import com.fs.fs12306.biz.orderservice.dto.req.*;
import com.fs.fs12306.biz.orderservice.dto.resp.TicketOrderDetailRespDTO;
import com.fs.fs12306.biz.orderservice.dto.resp.TicketOrderDetailSelfRespDTO;
import com.fs.fs12306.biz.orderservice.dto.resp.TicketOrderPassengerDetailRespDTO;
import com.fs.fs12306.biz.orderservice.service.OrderItemService;
import com.fs.fs12306.biz.orderservice.service.OrderService;
import com.fs.fs12306.frameworks.starter.convention.page.PageResponse;
import com.fs.fs12306.frameworks.starter.convention.result.Result;
import com.fs.fs12306.frameworks.starter.web.Results;
import lombok.RequiredArgsConstructor;
​
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
​
import java.util.List;
​
/*** 车票订单接口控制层**/
@RestController
@RequiredArgsConstructor
public class TicketOrderController {
​private final OrderService orderService;private final OrderItemService orderItemService;
​/*** 根据订单号查询车票订单*/@GetMapping("/api/order-service/order/ticket/query")public Result<TicketOrderDetailRespDTO> queryTicketOrderByOrderSn(@RequestParam(value = "orderSn") String orderSn) {return Results.success(orderService.queryTicketOrderByOrderSn(orderSn));}
​/*** 根据子订单记录id查询车票子订单详情*/@GetMapping("/api/order-service/order/item/ticket/query")public Result<List<TicketOrderPassengerDetailRespDTO>> queryTicketItemOrderById(TicketOrderItemQueryReqDTO requestParam) {return Results.success(orderItemService.queryTicketItemOrderById(requestParam));}
​/*** 分页查询车票订单*/@AutoOperate(type = TicketOrderDetailRespDTO.class, on = "data.records")@GetMapping("/api/order-service/order/ticket/page")public Result<PageResponse<TicketOrderDetailRespDTO>> pageTicketOrder(TicketOrderPageQueryReqDTO requestParam) {return Results.success(orderService.pageTicketOrder(requestParam));}
​/*** 分页查询本人车票订单*/@GetMapping("/api/order-service/order/ticket/self/page")public Result<PageResponse<TicketOrderDetailSelfRespDTO>> pageSelfTicketOrder(TicketOrderSelfPageQueryReqDTO requestParam) {return Results.success(orderService.pageSelfTicketOrder(requestParam));}
​/*** 车票订单创建*/@PostMapping("/api/order-service/order/ticket/create")public Result<String> createTicketOrder(@RequestBody TicketOrderCreateReqDTO requestParam) {return Results.success(orderService.createTicketOrder(requestParam));}
​/*** 车票订单关闭*/@PostMapping("/api/order-service/order/ticket/close")public Result<Boolean> closeTickOrder(@RequestBody CancelTicketOrderReqDTO requestParam) {return Results.success(orderService.closeTickOrder(requestParam));}
​/*** 车票订单取消*/@PostMapping("/api/order-service/order/ticket/cancel")public Result<Boolean> cancelTickOrder(@RequestBody CancelTicketOrderReqDTO requestParam) {return Results.success(orderService.cancelTickOrder(requestParam));}
}

关闭订单的业务实现:

OrderService接口定义关闭订单的方法:closeTickOrder

/*** 关闭火车票订单** @param requestParam 关闭火车票订单入参*/
boolean closeTickOrder(CancelTicketOrderReqDTO requestParam);
OrderServiceImpl类的具体closeTickOrder方法的实现如下:@Overridepublic boolean closeTickOrder(CancelTicketOrderReqDTO requestParam) {String orderSn = requestParam.getOrderSn();LambdaQueryWrapper<OrderDO> queryWrapper = Wrappers.lambdaQuery(OrderDO.class).eq(OrderDO::getOrderSn, orderSn).select(OrderDO::getStatus);OrderDO orderDO = orderMapper.selectOne(queryWrapper);if (Objects.isNull(orderDO) || orderDO.getStatus() != OrderStatusEnum.PENDING_PAYMENT.getStatus()) {return false;}// 原则上订单关闭和订单取消这两个方法可以复用,为了区分未来考虑到的场景,这里对方法进行拆分但复用逻辑return cancelTickOrder(requestParam);}
​
@Overridepublic boolean cancelTickOrder(CancelTicketOrderReqDTO requestParam) {String orderSn = requestParam.getOrderSn();LambdaQueryWrapper<OrderDO> queryWrapper = Wrappers.lambdaQuery(OrderDO.class).eq(OrderDO::getOrderSn, orderSn);OrderDO orderDO = orderMapper.selectOne(queryWrapper);if (orderDO == null) {throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_CANAL_UNKNOWN_ERROR);} else if (orderDO.getStatus() != OrderStatusEnum.PENDING_PAYMENT.getStatus()) {throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_CANAL_STATUS_ERROR);}RLock lock = redissonClient.getLock(StrBuilder.create("order:canal:order_sn_").append(orderSn).toString());if (!lock.tryLock()) {throw new ClientException(OrderCanalErrorCodeEnum.ORDER_CANAL_REPETITION_ERROR);}try {OrderDO updateOrderDO = new OrderDO();updateOrderDO.setStatus(OrderStatusEnum.CLOSED.getStatus());updateOrderDO.setOrderSn(orderSn);LambdaUpdateWrapper<OrderDO> updateWrapper = Wrappers.lambdaUpdate(OrderDO.class).eq(OrderDO::getOrderSn, orderSn);int updateResult = orderMapper.update(updateOrderDO, updateWrapper);if (updateResult <= 0) {throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_CANAL_ERROR);}OrderItemDO updateOrderItemDO = new OrderItemDO();updateOrderItemDO.setStatus(OrderItemStatusEnum.CLOSED.getStatus());updateOrderItemDO.setOrderSn(orderSn);LambdaUpdateWrapper<OrderItemDO> updateItemWrapper = Wrappers.lambdaUpdate(OrderItemDO.class).eq(OrderItemDO::getOrderSn, orderSn);int updateItemResult = orderItemMapper.update(updateOrderItemDO, updateItemWrapper);if (updateItemResult <= 0) {throw new ServiceException(OrderCanalErrorCodeEnum.ORDER_CANAL_ERROR);}} finally {lock.unlock();}return true;}

相关文章:

基于RabbitMQ,Redis,Redisson,RocketMQ四种技术实现订单延时关闭功能及其相关优缺点介绍(以12306为主题)

目录 1. 延迟关闭订单 1.1 订单延时关闭功能技术选型 1.1.1 定时任务 1.1.2 RabbitMQ 1.1.3 Redis 过期监听 1.1.4 Redisson 1.1.5 RocketMQ 1.2 RocketMQ订单延时关闭发送方实现 1.3 RocketMQ订单延时关闭的消费方实现 1. 延迟关闭订单 用户发起订单后&#xff0c;如…...

HarmonyOS ArkTS与C++数据类型转换

1. HarmonyOS ArkTS与C数据类型转换 本文介绍了C与TS各自数据类型与互相之间的数据类型转换&#xff0c;在需要使用C模块时可以快速上手对各种数据类型进行转换。 1.1. 概述 HarmonyOS的主力开发语言是ArkTS&#xff0c;也提供了C语言的支持&#xff0c;对于一些能力&#xff…...

腾讯云或阿里云centos7安装Redis,并解决端口无法访问的问题

问题背景 最近自建的网站JeecgFlow在云环境安装redis时候&#xff0c;出现端口无法远程进行访问。 浪费好了好久时间进行排查&#xff0c; 记录一下Redis在云环境centos7环境下如何安装&#xff0c;并且远程访问。 Redis安装 //安装c 用于编译redis yum install gcc-c//在/u…...

【小问题】距离估计和频率估计的方差下界推导出距离估计的方差下界

【1】OFDM Radar Algorithms in Mobile Communication Networks pp34 文章目录 1. 频率和距离之间的关系2. 计算 d ^ \hat{d} d^ 对 n ^ \hat{n} n^ 的导数3. 将频率的方差转化为距离的方差4. 从频率的 CRB 获得 var ⁡ [ n ^ ] \operatorname{var}[\hat{n}] var[n^]5. 将 …...

Selenium爬虫技术:如何模拟鼠标悬停抓取动态内容

介绍 在当今数据驱动的世界中&#xff0c;抓取动态网页内容变得越来越重要&#xff0c;尤其是像抖音这样的社交平台&#xff0c;动态加载的评论等内容需要通过特定的方式来获取。传统的静态爬虫方法难以处理这些由JavaScript生成的动态内容&#xff0c;Selenium爬虫技术则是一…...

Z-BlogPHP显示错误Undefined array key 0 (set_error_handler)的解决办法

今天打开博客的时候&#xff0c;意外发现页面&#xff0c;打开均显示错误&#xff1a;Undefined array key 0 (set_error_handler)。 博客程序采用的是Z-BlogPHP。百度了一圈没有找到解决办法&#xff0c;在官方论坛里也没找到解决办法。 于是开始自己排查原因。我服务器采用…...

java-实例化一个List,然后添加数据的方法详解

在Java中&#xff0c;实例化一个 List 并向其中添加数据非常简单。List 是一个接口&#xff0c;因此我们通常使用它的常见实现类 ArrayList 或 LinkedList。以下是一些常见的操作方法&#xff1a; ### 1. 使用 ArrayList 实例化并添加数据 java import java.util.ArrayList; …...

【Linux系统】Ubuntu的简单操作

什么是 Ubuntu&#xff1f; Ubuntu&#xff08;乌帮图&#xff09;是一个非洲词汇&#xff0c;它的意思是“人性对待他人”或“群在故我在”。Ubuntu发行版将Ubuntu精神带到软件世界之中。 目前已有大量各种各样基于GNU/Linux的操作系统&#xff0c;例如:Debian,SuSE,Gentoo,R…...

标准日志插件项目【C/C++】

博客主页&#xff1a;花果山~程序猿-CSDN博客 文章分栏&#xff1a;项目日记_花果山~程序猿的博客-CSDN博客 关注我一起学习&#xff0c;一起进步&#xff0c;一起探索编程的无限可能吧&#xff01;让我们一起努力&#xff0c;一起成长&#xff01; 目录 一&#xff0c;项目介…...

SpingBoot原理

SpingBoot原理 在前面十多天的课程当中&#xff0c;我们学习的都是web开发的技术使用&#xff0c;都是面向应用层面的&#xff0c;我们学会了怎 么样去用。而我们今天所要学习的是web后端开发的最后一个篇章springboot原理篇&#xff0c;主要偏向于底 层原理。 我们今天的课程…...

Cout输出应用举例

Cout输出应用 在main.cpp里输入程序如下&#xff1a; #include <iostream> //使能cin(),cout(); #include <stdlib.h> //使能exit(); #include <sstream> #include <iomanip> //使能setbase(),setfill(),setw(),setprecision(),setiosflags()和res…...

java的无锁编程和锁机制

Java 的并发编程中&#xff0c;为了保证线程安全和高性能&#xff0c;采用了两种主要的同步手段&#xff1a;锁机制和无锁编程。以下是对锁机制、无锁编程、死锁及其避免的详细讲解。 一、无锁编程 无锁编程通过原子操作来避免传统锁&#xff0c;从而减少线程的上下文切换&am…...

vue实现富文本编辑器上传(粘贴)图片 + 文字

vue实现富文本编辑器上传&#xff08;粘贴&#xff09;图片 文字 1.安装插件 npm install vue-quill-editor -s2.在使用vue-quill-editor富文本的时候&#xff0c;对于图片的处理经常是将图片转换成base64&#xff0c;再上传数据库&#xff0c;但是base64不好存储。 原理&a…...

子集和全排列(深度优先遍历)问题

欢迎访问杀马特主页&#xff1a;小小杀马特主页呀&#xff01; 目录 前言&#xff1a; 例题一全排列&#xff1a; 1.题目介绍&#xff1a; 2.思路汇总&#xff1a; 3.代码解答&#xff1a; 例题二子集&#xff1a; 题目叙述&#xff1a; 解法一&#xff1a; 1.思路汇总…...

判断检测框是否在感兴趣区域(ROI)内

判断检测框是否在感兴趣区域&#xff08;ROI&#xff09;内 在计算机视觉和图像处理中&#xff0c;我们经常需要确定一个矩形检测框是否位于一个特定的感兴趣区域&#xff08;Region of Interest, ROI&#xff09;内。这个ROI可以是一个多边形&#xff0c;而检测框则是一个矩形…...

正点原子阿尔法ARM开发板-IMX6ULL(九)——关于SecureCRT连接板子上的ubuntu

文章目录 一、拨码器二、SecureCRT 一、拨码器 emmm,也是好久没学IMX6ULL了&#xff0c;也是忘了拨码器决定了主板的启动方式 一种是直接从TF卡中读取文件&#xff08;注意这里是通过imdownload软件编译好了之后&#xff0c;通过指令放入TF卡&#xff09; 一种是现在这种用串口…...

微信支付Java+uniapp微信小程序

JS&#xff1a; request.post(/vip/pay, {//这是自己写的java支付接口id: this.vipInfo.id,payWay: wechat-mini}).then((res) > {let success (res2) > {//前端的支付成功回调函数this.$refs.popup.close();// 支付成功刷新当前页面setTimeout(() > {this.doGetVipI…...

【NOIP提高组】加分二叉树

【NOIP提高组】加分二叉树 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 设一个n个节点的二叉树tree的中序遍历为&#xff08;l,2,3,…,n&#xff09;&#xff0c;其中数字1,2,3,…,n为节点编号。每个节点都有一个分数&#xff08;均为正整…...

HarmonyOS 相对布局(RelativeContainer)

1. HarmonyOS 相对布局&#xff08;RelativeContainer&#xff09; 文档中心:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-layout-development-relative-layout-V5   RelativeContainer为采用相对布局的容器&#xff0c;支持容器内部的子元素设…...

webpack5搭建react脚手架详细步骤

1. 初始化项目 首先&#xff0c;创建一个新目录并初始化项目&#xff1a; bash mkdir create-react cd create-react pnpm init --y git init 这里使用pnpm作为包管理工具&#xff0c;因为它在处理依赖和速度上表现更好。 2. 安装React和TypeScript 安装React和React-DOM…...

速盾:高防cdn怎么拦截恶意ip?

高防CDN&#xff08;Content Delivery Network&#xff09;是一种用于防御网络攻击和提供高可用性的服务。它通过分发网络流量&#xff0c;将用户的请求导向最近的服务器&#xff0c;从而提高网站的加载速度和稳定性。然而&#xff0c;不可避免地&#xff0c;有些恶意IP地址会试…...

太阳能面板分割系统:训练自动化

太阳能面板分割系统源码&#xff06;数据集分享 [yolov8-seg-EfficientHead&#xff06;yolov8-seg-vanillanet等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Globa…...

C++笔记---位图

1. 位图的概念 位图&#xff08;Bitmap&#xff09;是一种基于位操作的数据结构&#xff0c;用于表示一组元素的集合信息。它通常是一个仅包含0和1的数组&#xff0c;每个元素对应一个二进制位&#xff0c;若该元素存在&#xff0c;则对应的位为1&#xff1b;若不存在&#xff…...

ABC370

## A - Raise Both Hands &#xff08;模拟&#xff09; 题意&#xff1a;输入l&#xff0c;r&#xff0c;如果l1r0输出yes&#xff0c;l0r1输出no&#xff0c;否则输出Invalid 代码&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long ll; vo…...

C语言[求x的y次方]

C语言——求x的y次方 这段 C 代码的目的是从用户输入获取两个整数 x 和 y &#xff0c;然后计算 x 的 y 次幂&#xff08;不过这里有个小错误&#xff0c;实际计算的是 x 的 (y - 1) 次幂&#xff0c;后面会详细说&#xff09;&#xff0c;最后输出结果。 代码如下: #include…...

JavaScript part2

一.前言 前面我们讲了一下js的基础语法&#xff0c;但是这些还是远远不够的&#xff0c;我们要想操作标签&#xff0c;实现一个动态且好看的页面&#xff0c;就得学会BOM和DOM&#xff0c;这些都是浏览器和页面的&#xff0c;这样我们才能实现一个好看的页面 二.BOM对象 BOM…...

HarmonyOS开发 - 本地持久化之实现LocalStorage实例

用户首选项为应用提供Key-Value键值型的数据处理能力&#xff0c;支持应用持久化轻量级数据&#xff0c;并对其修改和查询。数据存储形式为键值对&#xff0c;键的类型为字符串型&#xff0c;值的存储数据类型包括数字型、字符型、布尔型以及这3种类型的数组类型。 说明&#x…...

【C++打怪之路Lv12】-- 模板进阶

#1024程序员节&#xff5c;征文# &#x1f308; 个人主页&#xff1a;白子寰 &#x1f525; 分类专栏&#xff1a;重生之我在学Linux&#xff0c;C打怪之路&#xff0c;python从入门到精通&#xff0c;数据结构&#xff0c;C语言&#xff0c;C语言题集&#x1f448; 希望得到您…...

第23周Java主流框架入门-SpringMVC 2.RESTful开发风格

课程笔记&#xff1a;RESTful 开发风格 课程介绍 本节课程介绍 RESTful 开发风格&#xff0c;以及如何在 Spring MVC 中应用这种开发模式。传统 MVC 开发通过 Servlet、JSP 和 Java Bean 实现前后端交互&#xff0c;而 RESTful 开发提供了一种新的理念&#xff0c;更适合现代…...

QT枚举类型转字符串和使用QDebug<<重载输出私有枚举类型

一 将QT自带的枚举类型转换为QString 需要的头文件&#xff1a; #include <QMetaObject> #include <QMetaEnum> 测试代码 const QMetaObject *metaObject &QImage::staticMetaObject;QMetaEnum metaEnum metaObject->enumerator(metaObject->indexOf…...

南京网站排名/友情链接交换软件

setTimeout 在 js 加载前的问题探究 思思放出一道题目&#xff0c;深究一下发觉很有意思<script>setTimeout(() > {alert(2);}, 0)</script> <script src"https://test.tms-uat.xuebangsoft.net/plugins/jquery-1.10.2.min.js"></script>…...

保定建网站需要多少钱/全网网站推广

php中在做文件下载的时候&#xff0c;其中要加上这么一些header信息&#xff1a; 1 2 3 4 header("Content-type: application/octet-stream"); header("Accept-Ranges: bytes"); header("Accept-Length:".$fileSize); //请用Content-Length he…...

网站建设丿找vx cp5173/宁波seo网络推广渠道介绍

本文有的内容是期刊风格&#xff0c;所以会随着期刊变化而变化。有的内容不属于风格&#xff0c;比如易错的东西&#xff0c;摘要的功能等&#xff0c;所有期刊都一样。 文章目录一篇想被捞的论文的基本要求标题摘要公式 equations单位 units图 graphics交叉引用 cross referen…...

网站建设包括哪些方面/推广之家

适合计算机专业毕业生参考的英文范文。Dear Sir or Madam,Having read your internship position ad from qzzjx.cn, I decide to send you my resume because I know I will learn from you as well as bring values to your company.I am going to graduate at June this yea…...

网站建设南通/海外营销公司

1、大量饮水对尿石都有防治作用&#xff0c;也是比较有效的预防措施。 2、控制蛋白、糖的摄入量&#xff0c;要增加新鲜蔬菜和水果的食量。蔬菜和水果含有维生素B1及维生素C&#xff0c;它们在体内的最后代谢产物是碱性的&#xff0c;尿酸在碱性尿内易于溶解&#xff0c;故有利…...

好的手机端网站模板下载软件/目前推广软件

1.Number.EPSILON是JS表示的最小精度 function add(a,b){if(Math.abs(a-b)<Number.EPSILON){return true;}else{return false} } console.log(add(0.10.2,0.3)) //true 2.Number.isFinite检测一个数值是否为有限数 3.Number.isNaN检测一个数值是否为NaN 4.Number.parse…...