自学的马哥的项目,自己拆分记录学习笔记💕💕💕
首先是通过责任链模式看前端发来的请求参数是否合理
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 中:
业务梳理:我们需要根据上面查到的城市信息来构建查询条件,也就是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();