广州积分入学网站,公司自有网站工信备案,网络广告的形式有哪些,网站的推广方案怎么写#x1f3af; 本文档介绍了场馆预订系统接口V2的设计与实现#xff0c;旨在解决V1版本中库存数据不一致及性能瓶颈的问题。通过引入令牌机制确保缓存和数据库库存的最终一致性#xff0c;避免因服务器故障导致的库存错误占用问题。同时#xff0c;采用消息队列异步处理库存… 本文档介绍了场馆预订系统接口V2的设计与实现旨在解决V1版本中库存数据不一致及性能瓶颈的问题。通过引入令牌机制确保缓存和数据库库存的最终一致性避免因服务器故障导致的库存错误占用问题。同时采用消息队列异步处理库存扣减和订单创建显著提升了接口的吞吐量和响应速度。测试结果显示新版接口在高并发场景下表现优异平均响应时间为1801毫秒吞吐量达到了每秒1045.8次请求异常率仅为0.22%极大改善了用户体验。 ️ HelloDam/场快订场馆预定 SaaS 平台 文章目录 说明避免空场无法预订接口性能提升ControllerServiceMQ生产者消费者 测试结果说明 说明
在阅读此文之前建议先阅读预订接口V1实现https://hellodam.blog.csdn.net/article/details/144950335
接口 V2 主要是解决 V1 存在的一些问题
问题一接口 V1 中存在如下问题假如说 lua 脚本执行完成缓存中的库存已经扣减结果突然服务器宕机了没有执行后续的数据库库存扣减和创建订单流程就会出现库存被错误占用导致缓存中库存小于实际库存。对应于现实就是有的场空着用户预定不到问题二接口 V1 中因为库存扣减和订单创建是同步的预订接口吞吐量较低。为了进一步提升接口性能可以使用消息队列来异步执行库存扣减和订单创建逻辑
避免空场无法预订
缓存扣减完成之后由于发生故障导致没有更新数据库。这个问题本身是无法避免的只能通过一些机制来兜底。本文通过使用令牌机制来解决空场无法预订问题。
在接口 V1 中用户请求预定接口先查看 Redis 缓存中的库存是否大于 0 大于 0 才进行后面的操作。令牌是什么其实也是这个缓存但是我们并不完全相信它我们知道它可能和数据库的数据不一致。当用户获取不到令牌的时候我们不是直接返回时间段售罄错误而是先查询一下数据库看看是不是真的售罄了如果数据库中还有库存就删除令牌缓存。这样下一个用户再发起预订时就会重新刷新令牌缓存这样令牌的数据就和数据库保持一致就不会出现空场无法预订的问题。
为了实现这个思路我们还需要考虑一个问题难道每个用户看到没有令牌都去查数据库吗那肯定不行这样并发高的话数据库很容易被打崩。可以通过分布式锁让同一时刻只有一个用户查询数据库但是光是添加分布式锁还是不行用户请求多时可能出现不同时间点连续查询数据库刷新token的情况其实不必如此频繁查询。还有一个问题高并发时大量任务等着数据库响应数据库更新不会那么快。如果是立刻刷新token可能出现数据库没来得及扣减库存就被刷新到token中了这样会导致时间段超卖因为令牌数量大于库存。为了解决上述问题可以先延时10秒再刷新token在这10秒内其他用户访问预定接口因为拿不到分布式锁也不会重复执行token刷新。
/*** 查询数据库是否还有库存如果还有的话删除令牌让下一个用户重新加载令牌缓存** param timePeriodId*/
private void refreshTokenByCheckDatabase(Long timePeriodId) {RLock lock redissonClient.getLock(String.format(RedisCacheConstant.VENUE_LOCK_TIME_PERIOD_REFRESH_TOKEN_KEY, timePeriodId));// 尝试获取分布式锁获取不成功直接返回if (!lock.tryLock()) {return;}// 延迟 10 秒之后去检查数据库和令牌是否一致// 为啥要延迟如果不延迟的话可能高峰期时大量请求过来数据库还没来得及更新就触发令牌刷新导致超卖tokenRefreshExecutor.schedule(() - {try {TimePeriodDO timePeriodDO this.getById(timePeriodId);if (timePeriodDO.getStock() 0) {// --if-- 数据库中还有库存说明数据库中的库存和令牌中的库存不一致删除缓存让下一个用户重新获取stringRedisTemplate.delete(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY);stringRedisTemplate.delete(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);}} finally {lock.unlock();}}, 10, TimeUnit.SECONDS);
}接口性能提升
Controller
/*** 预定时间段*/
GetMapping(/v2/reserve)
Idempotent(uniqueKeyPrefix vrs-venue:lock_reserve:,// 让用户同时最多只能预定一个时间段根据用户名来加锁// key T(com.vrs.common.context.UserContext).getUsername(),// 让用户同时最多只能预定该时间段一次但是可以同时预定其他时间段根据用户名时间段ID来加锁key T(com.vrs.common.context.UserContext).getUsername()_#timePeriodId,message 正在执行场馆预定流程请勿重复预定...,scene IdempotentSceneEnum.RESTAPI
)
Operation(summary 预定时间段V2)
public Result reserve2(RequestParam(timePeriodId) Long timePeriodId) {timePeriodService.reserve2(timePeriodId);return Results.success();
}Service
【预订流程】
参数检验获取令牌 能获取到执行下一步获取不到查询数据库刷新令牌缓存 发送消息异步更新库存并生成订单
/*** 尝试获取令牌令牌获取成功之后发送消息异步执行库存扣减和订单生成* 注意令牌在极端情况下如扣减令牌之后服务宕机了此时令牌的库存是小于真实库存的* 如果查询令牌发现库存为0尝试去数据库中加载数据加载之后库存还是0说明时间段确实售罄了* 使用消息队列异步 扣减库存更新缓存生成订单** param timePeriodId*/
Override
public void reserve2(Long timePeriodId) { 参数校验使用责任链模式校验数据是否正确TimePeriodReserveReqDTO timePeriodReserveReqDTO new TimePeriodReserveReqDTO(timePeriodId);chainContext.handler(ChainConstant.RESERVE_CHAIN_NAME, timePeriodReserveReqDTO);TimePeriodDO timePeriodDO timePeriodReserveReqDTO.getTimePeriodDO();Long venueId timePeriodReserveReqDTO.getVenueId();VenueDO venueDO timePeriodReserveReqDTO.getVenueDO();PartitionDO partitionDO partitionService.getPartitionDOById(timePeriodDO.getPartitionId()); 使用lua脚本获取一个空场地对应的索引并扣除相应的库存同时在里面进行用户的查重// 首先检测空闲场号缓存有没有加载好没有的话进行加载this.checkBitMapCache(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY, timePeriodReserveReqDTO.getTimePeriodId()),timePeriodId,partitionDO.getNum());// 其次检测时间段库存有没有加载好没有的话进行加载this.getStockByTimePeriodId(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY, timePeriodReserveReqDTO.getTimePeriodId());// 执行lua脚本Long freeCourtIndex executeStockReduceByLua(timePeriodReserveReqDTO,venueDO,RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);if (freeCourtIndex -2L) {// --if-- 用户已经购买过该时间段throw new ClientException(BaseErrorCode.TIME_PERIOD_HAVE_BOUGHT_ERROR);} else if (freeCourtIndex -1L) {// --if-- 没有空闲的场号查询数据库如果数据库中有库存删除缓存下一个用户预定时重新加载令牌this.refreshTokenByCheckDatabase(timePeriodId);throw new ServiceException(BaseErrorCode.TIME_PERIOD_SELL_OUT_ERROR);} 发送消息异步更新库存并生成订单SendResult sendResult executeReserveProducer.sendMessage(ExecuteReserveMqDTO.builder().timePeriodId(timePeriodId).freeCourtIndex(freeCourtIndex).venueId(venueId).userId(UserContext.getUserId()).userName(UserContext.getUsername()).build());if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {log.error(消息发送失败: sendResult.getSendStatus());// 恢复令牌缓存this.restoreStockAndBookedSlotsCache(timePeriodId,UserContext.getUserId(),freeCourtIndex,RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);throw new ServiceException(BaseErrorCode.MQ_SEND_ERROR);}
}【获取令牌】
获取令牌的过程其实就是 检验用户是否重新预订、库存数量检查、场号分配、库存扣减、场号占用 这里和接口V1的实现是一样的
/*** 使用lua脚本进行缓存中的库存扣减并分配空闲场号** param timePeriodReserveReqDTO* param venueDO* param stockKey* param freeIndexBitMapKey* return*/
private Long executeStockReduceByLua(TimePeriodReserveReqDTO timePeriodReserveReqDTO, VenueDO venueDO,String stockKey, String freeIndexBitMapKey) {// 使用 Hutool 的单例管理容器 管理lua脚本的加载保证其只被加载一次String luaScriptPath lua/free_court_index_allocate_by_bitmap.lua;DefaultRedisScriptLong luaScript Singleton.get(luaScriptPath, () - {DefaultRedisScriptLong redisScript new DefaultRedisScript();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(luaScriptPath)));redisScript.setResultType(Long.class);return redisScript;});// 执行用户重复预定校验、库存扣减、场号分配Long freeCourtIndex stringRedisTemplate.execute(luaScript,Lists.newArrayList(String.format(stockKey, timePeriodReserveReqDTO.getTimePeriodId()),String.format(freeIndexBitMapKey, timePeriodReserveReqDTO.getTimePeriodId()),String.format(RedisCacheConstant.VENUE_IS_USER_BOUGHT_TIME_PERIOD_KEY, timePeriodReserveReqDTO.getTimePeriodId())),UserContext.getUserId().toString(),String.valueOf(venueDO.getAdvanceBookingDay() * 86400));return freeCourtIndex;
}lua
-- 定义脚本参数
local stock_key KEYS[1]
local free_index_bitmap_key KEYS[2]
-- 用来存储已购买用户的set
local set_name KEYS[3]-- 用户ID
local user_id ARGV[1]
-- 过期时间 (秒)
local expire_time tonumber(ARGV[2])-- 检查用户是否已经购买过
if redis.call(SISMEMBER, set_name, user_id) 1 then-- 用户已经购买过返回 -2 表示失败return -2
end-- 获取库存
local current_inventory tonumber(redis.call(GET, stock_key) or 0)-- 尝试消耗库存
if current_inventory 1 then-- 库存不够了返回-1代表分配空场号失败return -1 -- 失败
end-- 查找第一个空闲的场地位图中第一个为 0 的位
local free_court_bit redis.call(BITPOS, free_index_bitmap_key, 0)if not free_court_bit or free_court_bit -1 then-- 没有空闲的场号return -1 -- 失败
end-- 占用该场地将对应位设置为 1
redis.call(SETBIT, free_index_bitmap_key, free_court_bit, 1)
-- 更新库存
redis.call(DECRBY, stock_key, 1)
-- 添加用户到已购买集合
redis.call(SADD, set_name, user_id)
-- 设置过期时间
if expire_time 0 thenredis.call(EXPIRE, set_name, expire_time)
end-- 返回分配的场地索引注意位图的位索引从0开始如果你需要从1开始这里加1
return tonumber(free_court_bit)【更新缓存中库存】
大家可能会疑问为啥有了令牌还要更新缓存中的库存和空闲场号。因为我们在前端展示的信息需要是真实的库存信息为了加速查询需要将库存缓存起来这里的缓存数据需要和数据库一致。为了保证缓存和数据库的最终一致性可以开启 binlog 然后使用 Canal 进行监听。如果数据库中的数据更新了就发送消息到消息队列中消费消息时再更新缓存中的库存。
-- 定义脚本参数
local stock_key KEYS[1]
local free_index_bitmap_key KEYS[2]-- 预订场号
local free_court_bit ARGV[1]-- 占用该场地将对应位设置为 1
redis.call(SETBIT, free_index_bitmap_key, free_court_bit, 1)
-- 更新库存
redis.call(DECRBY, stock_key, 1)return 0【检测和加载位图缓存】
/*** 检测位图缓存是否加载好没有的话执行加载操作** param freeIndexBitmapKey* param timePeriodId* param initStock*/
Override
public void checkBitMapCache(String freeIndexBitmapKey, Long timePeriodId, int initStock) {String cache stringRedisTemplate.opsForValue().get(freeIndexBitmapKey);if (StringUtils.isBlank(cache)) {// --if-- 如果缓存中的位图为空RLock lock redissonClient.getLock(String.format(RedisCacheConstant.VENUE_LOCK_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, timePeriodId));lock.lock();try {// 双重判定一下避免其他线程已经加载数据到缓存中了cache stringRedisTemplate.opsForValue().get(freeIndexBitmapKey);if (StringUtils.isBlank(cache)) {// --if-- 如果缓存中的位图还是空到数据库中加载位图TimePeriodDO timePeriodDO this.getById(timePeriodId);if (timePeriodDO null) {throw new ServiceException(timePeriodId 对应的时间段为null, BaseErrorCode.SERVICE_ERROR);}// 将位图信息设置到缓存中this.initializeFreeIndexBitmap(freeIndexBitmapKey, initStock, timePeriodDO.getBookedSlots(), 24 * 3600);}} finally {// 解锁lock.unlock();}}
}/*** 初始化Redis中的位图并设置key的过期时间** param freeIndexBitmapKey 位图的键名* param longValue 用于初始化位图的 long 类型数据* param expireSecond key的过期时间秒*/
public void initializeFreeIndexBitmap(String freeIndexBitmapKey, int initStock, long longValue, long expireSecond) {// 将 long 转换为64位的二进制字符串String binaryString Long.toBinaryString(longValue);// 确保字符串长度为64位不足的部分用0补齐binaryString String.format(%64s, binaryString).replace( , 0);// 从低位到高位遍历二进制字符串设置位图中的对应位for (int i 0; i 64 initStock-- 0; i) {// 注意long的最低位对应位图的第0位if (binaryString.charAt(63 - i) 1) {stringRedisTemplate.opsForValue().setBit(freeIndexBitmapKey, i, true).booleanValue();} else {stringRedisTemplate.opsForValue().setBit(freeIndexBitmapKey, i, false).booleanValue();}}// 设置过期时间仅当expireTime大于0时进行设置if (expireSecond 0) {stringRedisTemplate.expire(freeIndexBitmapKey, expireSecond, TimeUnit.SECONDS);}
}【检验和加载库存缓存】
这里使用了封装的缓存组件需要去仓库查看详细代码
/*** 获取指定时间段的库存** param timePeriodId* return*/
Override
public Integer getStockByTimePeriodId(Long timePeriodId) {return (Integer) distributedCache.safeGet(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_KEY, timePeriodId),new TypeReferenceInteger() {},() - {TimePeriodDO timePeriodDO this.getById(timePeriodId);return timePeriodDO.getStock();},1,TimeUnit.DAYS);
}【消费消息执行预订流程】
和接口 V1 不同的是V1 时同步创建订单创建完成之后直接访问给用户订单数据。但是在 V2 中将任务交给消息队列之后就要返回成功了。用户需要在前端等待订单创建结果。那前端如何感知订单是否创建成功呢
方式一前端轮询查询后端如每隔一秒问一下后端订单创建好没有创建好了就返回给前端这样前端就可以进行支付了方式二使用前后端双向通讯技术如WebSocket。前后端一开始先建立好连接等后端消费消息创建订单成功之后直接将订单信息推送给前端
/*** 通过消息队列执行 时间段预定 逻辑* param executeReserveMqDTO*/
Override
public void mqExecutePreserve(ExecuteReserveMqDTO executeReserveMqDTO) {TimePeriodDO timePeriodDO this.getTimePeriodDOById(executeReserveMqDTO.getTimePeriodId());// 编程式开启事务减少事务粒度避免长事务的发生transactionTemplate.executeWithoutResult(status - {try {// 扣减当前时间段的库存修改空闲场信息baseMapper.updateStockAndBookedSlots(timePeriodDO.getId(), timePeriodDO.getPartitionId(), executeReserveMqDTO.getFreeCourtIndex());// 更新缓存中的库存、位图if (!isUseBinlog) {// --if-- 如果不使用binlog需要手动更新缓存// 首先检测空闲场号缓存有没有加载好没有的话进行加载this.checkBitMapCache(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, executeReserveMqDTO.getTimePeriodId()),executeReserveMqDTO.getTimePeriodId(),partitionService.getPartitionDOById(timePeriodDO.getPartitionId()).getNum());// 其次检测时间段库存有没有加载好没有的话进行加载this.getStockByTimePeriodId(executeReserveMqDTO.getTimePeriodId());// 使用 Hutool 的单例管理容器 管理lua脚本的加载保证其只被加载一次String luaScriptPath lua/inventory_update.lua;DefaultRedisScriptLong luaScript Singleton.get(luaScriptPath, () - {DefaultRedisScriptLong redisScript new DefaultRedisScript();redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(luaScriptPath)));redisScript.setResultType(Long.class);return redisScript;});// 库存扣减、场号占用stringRedisTemplate.execute(luaScript,Lists.newArrayList(String.format(RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_KEY, executeReserveMqDTO.getTimePeriodId()),String.format(RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_KEY, executeReserveMqDTO.getTimePeriodId())),executeReserveMqDTO.getFreeCourtIndex().toString());}// todo 需要实现binlog版本// 调用远程服务创建订单OrderGenerateReqDTO orderGenerateReqDTO OrderGenerateReqDTO.builder().timePeriodId(timePeriodDO.getId()).partitionId(timePeriodDO.getPartitionId()).periodDate(timePeriodDO.getPeriodDate()).beginTime(timePeriodDO.getBeginTime()).endTime(timePeriodDO.getEndTime()).courtIndex(executeReserveMqDTO.getFreeCourtIndex()).userId(executeReserveMqDTO.getUserId()).userName(executeReserveMqDTO.getUserName()).venueId(executeReserveMqDTO.getVenueId()).payAmount(timePeriodDO.getPrice()).build();ResultOrderDO result;try {result orderFeignService.generateOrder(orderGenerateReqDTO);if (result null || !result.isSuccess()) {// --if-- 订单生成失败抛出异常上面的库存扣减也会回退throw new ServiceException(BaseErrorCode.ORDER_GENERATE_ERROR);}} catch (Exception e) {// --if-- 订单生成服务调用失败// 恢复缓存中的信息this.restoreStockAndBookedSlotsCache(timePeriodDO.getId(),1L,executeReserveMqDTO.getFreeCourtIndex(),RedisCacheConstant.VENUE_TIME_PERIOD_STOCK_TOKEN_KEY,RedisCacheConstant.VENUE_TIME_PERIOD_FREE_INDEX_BIT_MAP_TOKEN_KEY);// todo 如果说由于网络原因实际上订单已经创建成功了但是因为超时访问失败这里库存却回滚了此时需要将订单置为废弃状态即删除// 发送一个短暂的延时消息时间过长用户可能已经支付去检查订单是否生成如果生成将其删除// 打印错误堆栈信息e.printStackTrace();// 把错误返回到前端throw new ServiceException(e.getMessage());}OrderDO orderDO result.getData();// todo 使用 WebSocket 通知前端订单生成成功} catch (Exception ex) {status.setRollbackOnly();throw ex;}});
}MQ
生产者
import cn.hutool.core.util.StrUtil;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.ExecuteReserveMqDTO;
import com.vrs.templateMethod.AbstractCommonSendProduceTemplate;
import com.vrs.templateMethod.BaseSendExtendDTO;
import com.vrs.templateMethod.MessageWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageConst;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;import java.util.UUID;/*** 执行预订流程 生产者** Author dam* create 2024/9/20 16:00*/
Slf4j
Component
public class ExecuteReserveProducer extends AbstractCommonSendProduceTemplateExecuteReserveMqDTO {Overrideprotected BaseSendExtendDTO buildBaseSendExtendParam(ExecuteReserveMqDTO messageSendEvent) {return BaseSendExtendDTO.builder().eventName(执行时间段预定).keys(String.valueOf(messageSendEvent.getTimePeriodId())).topic(RocketMqConstant.VENUE_TOPIC).tag(RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG).sentTimeout(2000L).build();}Overrideprotected Message? buildMessage(ExecuteReserveMqDTO messageSendEvent, BaseSendExtendDTO requestParam) {String keys StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();return MessageBuilder.withPayload(new MessageWrapper(keys, messageSendEvent)).setHeader(MessageConst.PROPERTY_KEYS, keys).setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag()).build();}
}消费者
import com.vrs.annotation.Idempotent;
import com.vrs.constant.RocketMqConstant;
import com.vrs.domain.dto.mq.ExecuteReserveMqDTO;
import com.vrs.enums.IdempotentSceneEnum;
import com.vrs.service.TimePeriodService;
import com.vrs.templateMethod.MessageWrapper;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.annotation.SelectorType;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;/*** 执行预订流程 消费者* Author dam* create 2024/9/20 21:30*/
Slf4j(topic RocketMqConstant.VENUE_TOPIC)
Component
RocketMQMessageListener(topic RocketMqConstant.VENUE_TOPIC,consumerGroup RocketMqConstant.VENUE_CONSUMER_GROUP - RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG,messageModel MessageModel.CLUSTERING,// 监听tagselectorType SelectorType.TAG,selectorExpression RocketMqConstant.TIME_PERIOD_EXECUTE_RESERVE_TAG
)
RequiredArgsConstructor
public class ExecuteReserveListener implements RocketMQListenerMessageWrapperExecuteReserveMqDTO {private final TimePeriodService timePeriodService;/*** 消费消息的方法* 方法报错就会拒收消息** param messageWrapper 消息内容类型和上面的泛型一致。如果泛型指定了固定的类型消息体就是我们的参数*/Idempotent(uniqueKeyPrefix time_period_execute_reserve:,key #messageWrapper.getMessage().getTimePeriodId(),scene IdempotentSceneEnum.MQ,keyTimeout 3600L)SneakyThrowsOverridepublic void onMessage(MessageWrapperExecuteReserveMqDTO messageWrapper) {// 开头打印日志平常可 Debug 看任务参数线上可报平安比如消息是否消费重新投递时获取参数等log.info([消费者] 执行时间段预定时间段ID{}, messageWrapper.getMessage().getTimePeriodId());timePeriodService.mqExecutePreserve(messageWrapper.getMessage());}
}测试结果 样本数量共有40,000个样本这表示在测试期间进行了40,000次操作或请求。响应时间 平均值1801毫秒表示所有请求的平均响应时间。中位数1346毫秒表示50%的请求响应时间低于这个值。90%百分位2048毫秒表示90%的请求响应时间低于这个值。95%百分位3410毫秒表示95%的请求响应时间低于这个值。99%百分位15133毫秒表示99%的请求响应时间低于这个值。最小值15毫秒表示最快的请求响应时间。最大值22121毫秒表示最慢的请求响应时间。 异常率0.22%表示在所有请求中有0.22%的请求出现了异常。吞吐量每秒可以处理1045.8个请求网络流量 接收速率221.51 KB/sec表示系统每秒接收的数据量。发送速率509.96 KB/sec表示系统每秒发送的数据量。
总结
系统的平均响应时间为1801毫秒中位数为1346毫秒表明大多数请求的响应时间在可接受范围内。99%的请求响应时间在15133毫秒以内但有少数请求的响应时间较长最大值达到了22121毫秒。系统的吞吐量为1045.8次请求/秒处理能力较高相较于接口V1性能强了一倍
说明
文章内容并非最新代码实现若需要知道最新实现麻烦移步开源仓库 HelloDam/场快订场馆预定 SaaS 平台 文章转载自: http://www.morning.csznh.cn.gov.cn.csznh.cn http://www.morning.prgyd.cn.gov.cn.prgyd.cn http://www.morning.dxgt.cn.gov.cn.dxgt.cn http://www.morning.tgydf.cn.gov.cn.tgydf.cn http://www.morning.csxlm.cn.gov.cn.csxlm.cn http://www.morning.ailvturv.com.gov.cn.ailvturv.com http://www.morning.qcbhb.cn.gov.cn.qcbhb.cn http://www.morning.glswq.cn.gov.cn.glswq.cn http://www.morning.blqsr.cn.gov.cn.blqsr.cn http://www.morning.yxgqr.cn.gov.cn.yxgqr.cn http://www.morning.nchsz.cn.gov.cn.nchsz.cn http://www.morning.trwkz.cn.gov.cn.trwkz.cn http://www.morning.yrddl.cn.gov.cn.yrddl.cn http://www.morning.yhxhq.cn.gov.cn.yhxhq.cn http://www.morning.xjtnp.cn.gov.cn.xjtnp.cn http://www.morning.pymff.cn.gov.cn.pymff.cn http://www.morning.tgtwy.cn.gov.cn.tgtwy.cn http://www.morning.gyzfp.cn.gov.cn.gyzfp.cn http://www.morning.ydrml.cn.gov.cn.ydrml.cn http://www.morning.gmgyt.cn.gov.cn.gmgyt.cn http://www.morning.flqkp.cn.gov.cn.flqkp.cn http://www.morning.kkdbz.cn.gov.cn.kkdbz.cn http://www.morning.wcjgg.cn.gov.cn.wcjgg.cn http://www.morning.thbqp.cn.gov.cn.thbqp.cn http://www.morning.gbkkt.cn.gov.cn.gbkkt.cn http://www.morning.rxlk.cn.gov.cn.rxlk.cn http://www.morning.bfsqz.cn.gov.cn.bfsqz.cn http://www.morning.pcngq.cn.gov.cn.pcngq.cn http://www.morning.cnwpb.cn.gov.cn.cnwpb.cn http://www.morning.tndhm.cn.gov.cn.tndhm.cn http://www.morning.xhfky.cn.gov.cn.xhfky.cn http://www.morning.yrjxr.cn.gov.cn.yrjxr.cn http://www.morning.vattx.cn.gov.cn.vattx.cn http://www.morning.lptjt.cn.gov.cn.lptjt.cn http://www.morning.nwljj.cn.gov.cn.nwljj.cn http://www.morning.cfybl.cn.gov.cn.cfybl.cn http://www.morning.xrwbc.cn.gov.cn.xrwbc.cn http://www.morning.qmzwl.cn.gov.cn.qmzwl.cn http://www.morning.wlqll.cn.gov.cn.wlqll.cn http://www.morning.rqfnl.cn.gov.cn.rqfnl.cn http://www.morning.stsnf.cn.gov.cn.stsnf.cn http://www.morning.ttvtv.cn.gov.cn.ttvtv.cn http://www.morning.fkrzx.cn.gov.cn.fkrzx.cn http://www.morning.qqzdr.cn.gov.cn.qqzdr.cn http://www.morning.tymwx.cn.gov.cn.tymwx.cn http://www.morning.ydnxm.cn.gov.cn.ydnxm.cn http://www.morning.lywys.cn.gov.cn.lywys.cn http://www.morning.nykzl.cn.gov.cn.nykzl.cn http://www.morning.tdwjj.cn.gov.cn.tdwjj.cn http://www.morning.tqpnf.cn.gov.cn.tqpnf.cn http://www.morning.rshkh.cn.gov.cn.rshkh.cn http://www.morning.bklhx.cn.gov.cn.bklhx.cn http://www.morning.yhpl.cn.gov.cn.yhpl.cn http://www.morning.rlbc.cn.gov.cn.rlbc.cn http://www.morning.tlfyb.cn.gov.cn.tlfyb.cn http://www.morning.rbkl.cn.gov.cn.rbkl.cn http://www.morning.hmxb.cn.gov.cn.hmxb.cn http://www.morning.cgntj.cn.gov.cn.cgntj.cn http://www.morning.gktds.cn.gov.cn.gktds.cn http://www.morning.bjjrtcsl.com.gov.cn.bjjrtcsl.com http://www.morning.yjprj.cn.gov.cn.yjprj.cn http://www.morning.tqsmg.cn.gov.cn.tqsmg.cn http://www.morning.ypcbm.cn.gov.cn.ypcbm.cn http://www.morning.nllst.cn.gov.cn.nllst.cn http://www.morning.nclps.cn.gov.cn.nclps.cn http://www.morning.wdlg.cn.gov.cn.wdlg.cn http://www.morning.nicetj.com.gov.cn.nicetj.com http://www.morning.nlkjq.cn.gov.cn.nlkjq.cn http://www.morning.qbwbs.cn.gov.cn.qbwbs.cn http://www.morning.dhckp.cn.gov.cn.dhckp.cn http://www.morning.wkpfm.cn.gov.cn.wkpfm.cn http://www.morning.gqjqf.cn.gov.cn.gqjqf.cn http://www.morning.djpgc.cn.gov.cn.djpgc.cn http://www.morning.rwnx.cn.gov.cn.rwnx.cn http://www.morning.kndyz.cn.gov.cn.kndyz.cn http://www.morning.pqwrg.cn.gov.cn.pqwrg.cn http://www.morning.rmdwp.cn.gov.cn.rmdwp.cn http://www.morning.rfycj.cn.gov.cn.rfycj.cn http://www.morning.ymhjb.cn.gov.cn.ymhjb.cn http://www.morning.hxxwq.cn.gov.cn.hxxwq.cn