12306梳理-列车检索

515 阅读4分钟

自学的马哥的项目,自己拆分记录学习笔记💕💕💕

首先是通过责任链模式看前端发来的请求参数是否合理

 ticketPageQueryAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);

加载城市数据

例如我们搜索了北京南到杭州东的搜索条件,它会帮你列出北京到杭州所有的列车车次。

这个很好实现,直接通过站点关联到城市,通过城市查询列车即可

在缓存中,有一个 Hash 结构数据,专门负责保存列车站点 Code 值与城市之间的关联关系。

业务梳理:就是首先我们搜索了北京南到杭州东,我们会先在缓存中找是否有对应的城市信息,如果存在为空的情况,就重新加载对应的城市数据

源码:

 // 通过批量查询方式获取出发站点和到达站点对应的城市集合
 List<Object> stationDetails = stringRedisTemplate.opsForHash()
         .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
 // 判断为空问题,同上验证查询数据
 long count = stationDetails.stream().filter(Objects::isNull).count();
 // 如果城市数据为空,代表缓存中没有这个值,需要从数据库加载
 if (count > 0) {
     // 避免缓存击穿,通过分布式锁方式解决该问题
     RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION_MAPPING);
     lock.lock();
     try {
         // 双重判定锁,规避数据库无效请求
         stationDetails = stringRedisTemplate.opsForHash()
                 .multiGet(REGION_TRAIN_STATION_MAPPING, Lists.newArrayList(requestParam.getFromStation(), requestParam.getToStation()));
         count = stationDetails.stream().filter(Objects::isNull).count();
         if (count > 0) {
             // 查询列车站点数据与城市信息
             List<StationDO> stationDOList = stationMapper.selectList(Wrappers.emptyWrapper());
             Map<String, String> regionTrainStationMap = new HashMap<>();
             stationDOList.forEach(each -> regionTrainStationMap.put(each.getCode(), each.getRegionName()));
             // 通过 putAll 批量保存方式存入 Redis,避免多次 put 网络 IO 消耗
             stringRedisTemplate.opsForHash().putAll(REGION_TRAIN_STATION_MAPPING, regionTrainStationMap);
             stationDetails = new ArrayList<>();
             stationDetails.add(regionTrainStationMap.get(requestParam.getFromStation()));
             stationDetails.add(regionTrainStationMap.get(requestParam.getToStation()));
         }
     } finally {
         lock.unlock();
     }
 }

查询列车的站点信息

我们的设计是将列车站点数据存入 Redis 中:

image.png

业务梳理:我们需要根据上面查到的城市信息来构建查询条件,也就是Redis的key,在缓存中来查到对应的列车信息,存到一个Map里面,其中Key是站点查询的Key,Value是这个Hash数据,如果缓存中没有查到的话,就去数据库根据上面查到的城市信息在查询列车信息并构建出这个Map,最终得到的是一个Map数据

源码:

 List<TicketListDTO> seatResults = new ArrayList<>();
 // 构建查询 Redis 中 Hash 结构 Key,Key前缀 + 出发城市 + 到达城市
 String buildRegionTrainStationHashKey = String.format(REGION_TRAIN_STATION, stationDetails.get(0), stationDetails.get(1));
 Map<Object, Object> regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
 // 如果为空,兜底查询数据库再放入缓存
 if (MapUtil.isEmpty(regionTrainStationAllMap)) {
     // 老规矩上分布式锁
     RLock lock = redissonClient.getLock(LOCK_REGION_TRAIN_STATION);
     lock.lock();
     try {
         // 老规矩双重判定锁
         regionTrainStationAllMap = stringRedisTemplate.opsForHash().entries(buildRegionTrainStationHashKey);
         if (MapUtil.isEmpty(regionTrainStationAllMap)) {
             // 加载数据库列车相关信息,并构建出一趟列车详细记录
             LambdaQueryWrapper<TrainStationRelationDO> queryWrapper = Wrappers.lambdaQuery(TrainStationRelationDO.class)
                     .eq(TrainStationRelationDO::getStartRegion, stationDetails.get(0))
                     .eq(TrainStationRelationDO::getEndRegion, stationDetails.get(1));
             List<TrainStationRelationDO> trainStationRelationList = trainStationRelationMapper.selectList(queryWrapper);
             for (TrainStationRelationDO each : trainStationRelationList) {
                 TrainDO trainDO = distributedCache.safeGet(
                         TRAIN_INFO + each.getTrainId(),
                         TrainDO.class,
                         () -> trainMapper.selectById(each.getTrainId()),
                         ADVANCE_TICKET_DAY,
                         TimeUnit.DAYS);
                 TicketListDTO result = new TicketListDTO();
                 result.setTrainId(String.valueOf(trainDO.getId()));
                 result.setTrainNumber(trainDO.getTrainNumber());
                 result.setDepartureTime(convertDateToLocalTime(each.getDepartureTime(), "HH:mm"));
                 result.setArrivalTime(convertDateToLocalTime(each.getArrivalTime(), "HH:mm"));
                 result.setDuration(DateUtil.calculateHourDifference(each.getDepartureTime(), each.getArrivalTime()));
                 result.setDeparture(each.getDeparture());
                 result.setArrival(each.getArrival());
                 result.setDepartureFlag(each.getDepartureFlag());
                 result.setArrivalFlag(each.getArrivalFlag());
                 result.setTrainType(trainDO.getTrainType());
                 result.setTrainBrand(trainDO.getTrainBrand());
                 if (StrUtil.isNotBlank(trainDO.getTrainTag())) {
                     result.setTrainTags(StrUtil.split(trainDO.getTrainTag(), ","));
                 }
                 long betweenDay = cn.hutool.core.date.DateUtil.betweenDay(each.getDepartureTime(), each.getArrivalTime(), false);
                 result.setDaysArrived((int) betweenDay);
                 result.setSaleStatus(new Date().after(trainDO.getSaleTime()) ? 0 : 1);
                 result.setSaleTime(convertDateToLocalTime(trainDO.getSaleTime(), "MM-dd HH:mm"));
                 seatResults.add(result);
                 regionTrainStationAllMap.put(CacheUtil.buildKey(String.valueOf(each.getTrainId()), each.getDeparture(), each.getArrival()), JSON.toJSONString(result));
             }
             // 全部加载完,批量保存到 Redis Hash 结构中
             stringRedisTemplate.opsForHash().putAll(buildRegionTrainStationHashKey, regionTrainStationAllMap);
         }
     } finally {
         lock.unlock();
     }
 }

查询列车余票信息

注意:列车详细信息中是没有列车余票数据的,因为这个数据是实时变更的,存入这里无法更新。

列车基本信息已经全部填充完毕了,接下来就是查询列车余票信息并填充到基本信息中。

上面讲过,列车余票数据是实时变更的,如果在存储到基本信息中,就没办法变更了,所以单独存储。

上面的Map里面取Value集合,然后对于每一个Value集合找到对于的余票数量构建

 for (TicketListDTO each : seatResults) {
     // 加载列车对应的座位价格数据
     // safeGet 就是安全获取缓存方案,底层加了分布式锁和双重判定锁
     // 如果查询 TRAIN_STATION_PRICE 数据为空,则加载数据库,并放入缓存
     String trainStationPriceStr = distributedCache.safeGet(
             String.format(TRAIN_STATION_PRICE, each.getTrainId(), each.getDeparture(), each.getArrival()),
             String.class,
             () -> {
                 LambdaQueryWrapper<TrainStationPriceDO> trainStationPriceQueryWrapper = Wrappers.lambdaQuery(TrainStationPriceDO.class)
                         .eq(TrainStationPriceDO::getDeparture, each.getDeparture())
                         .eq(TrainStationPriceDO::getArrival, each.getArrival())
                         .eq(TrainStationPriceDO::getTrainId, each.getTrainId());
                 return JSON.toJSONString(trainStationPriceMapper.selectList(trainStationPriceQueryWrapper));
             },
             ADVANCE_TICKET_DAY,
             TimeUnit.DAYS
     );
     List<TrainStationPriceDO> trainStationPriceDOList = JSON.parseArray(trainStationPriceStr, TrainStationPriceDO.class);
     List<SeatClassDTO> seatClassList = new ArrayList<>();
     // 循环遍历座位价格数据,获取到座位对应的余票,并最终放入到列车基本信息中
     trainStationPriceDOList.forEach(item -> {
         String seatType = String.valueOf(item.getSeatType());
         String keySuffix = StrUtil.join("_", each.getTrainId(), item.getDeparture(), item.getArrival());
         Object quantityObj = stringRedisTemplate.opsForHash().get(TRAIN_STATION_REMAINING_TICKET + keySuffix, seatType);
         int quantity = Optional.ofNullable(quantityObj)
                 .map(Object::toString)
                 .map(Integer::parseInt)
                 .orElseGet(() -> {
                     Map<String, String> seatMarginMap = seatMarginCacheLoader.load(String.valueOf(each.getTrainId()), seatType, item.getDeparture(), item.getArrival());
                     return Optional.ofNullable(seatMarginMap.get(String.valueOf(item.getSeatType()))).map(Integer::parseInt).orElse(0);
                 });
         seatClassList.add(new SeatClassDTO(item.getSeatType(), quantity, new BigDecimal(item.getPrice()).divide(new BigDecimal("100"), 1, RoundingMode.HALF_UP), false));
     });
     each.setSeatClassList(seatClassList);
 }

返回响应

 return TicketPageQueryRespDTO.builder()
         .trainList(seatResults)
         .departureStationList(buildDepartureStationList(seatResults))
         .arrivalStationList(buildArrivalStationList(seatResults))
         .trainBrandList(buildTrainBrandList(seatResults))
         .seatClassTypeList(buildSeatClassList(seatResults))
         .build();