本项目为开源项目

先了解相关包,对项目有个整体的了解。
包相关说明

配置相关看 官方环境搭建 就行了

站点查询相关

因为 查询大多都类似 ,比较复杂的就是首页进行车票查询,所以再这里只写一个

接口 /api/ticket-service/ticket/query ,查询车票(默认访问查询 北京到杭州的车票 )访问首页即可触发

大致流程: 请求首先会在网关中被拦截,然后通过黑白名单进行过滤,请求再到相应服务端进行处理,对于上述车票的查询,网关路由到服务端后首先查询缓存,查询不到就查询数据库,并将相关数据存储在缓存中

具体流程如下

1
2
3
4
5
6
7
8
//服务端接口
@GetMapping("/api/ticket-service/ticket/query")
public Result<TicketPageQueryRespDTO> pageListTicketQuery
// 参数为 两站台缩写
(TicketPageQueryReqDTO requestParam) {
return Results.success(
ticketService.pageListTicketQueryV1(requestParam));
}

进入到实现层后,第一行代码就是利用 责任链模式,首先 对参数进行校验

1
2
3
4
5
// 责任链模式 验证城市名称是否存在、不存在加载缓存以及出发日期不能小于当前日期等等
ticketPageQueryAbstractChainContext.handler
// 第一个参数是标记,用于区分不同的责任链
// 第二个是要进行校验的参数
(TicketChainMarkEnum.TRAIN_QUERY_FILTER.name(), requestParam);

再进入到 ticketPageQueryAbstractChainContext 时,首先我们要看一个接口

1
2
3
4
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}

这是一个单接口, 文中用了大量的 @FunctionalInterface 来标识接口,我能想到的目的就是规范开发, 只定义一个接口增加灵活性,方便扩展 ?(泛型接口,不用具体实现,函数式编程直接传入具体实现)

接下来再进入 ticketPageQueryAbstractChainContext 查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

public final class AbstractChainContext<T> implements CommandLineRunner {

private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer
= new HashMap<>();

/**
* 责任链组件执行
*
* @param mark 责任链组件标识
* @param requestParam 请求参数
*/
public void handler(String mark, T requestParam) {
List<AbstractChainHandler> abstractChainHandlers
= abstractChainHandlerContainer.get(mark);
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
throw new RuntimeException(String
.format("[%s] Chain of Responsibility ID is undefined.", mark));
}
abstractChainHandlers.
forEach(each -> each.handler(requestParam));
}

// 在初始化时就进行装载, 我给忘了在哪里加载来着。chao TODO
@Override
public void run(String... args) throws Exception {
Map<String, AbstractChainHandler> chainFilterMap = ApplicationContextHolder
.getBeansOfType(AbstractChainHandler.class);
chainFilterMap.forEach((beanName, bean) -> {
List<AbstractChainHandler> abstractChainHandlers =
abstractChainHandlerContainer.get(bean.mark());
if (CollectionUtils.isEmpty(abstractChainHandlers)) {
abstractChainHandlers = new ArrayList();
}
abstractChainHandlers.add(bean);
List<AbstractChainHandler> actualAbstractChainHandlers =
abstractChainHandlers.stream()
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
});
}
}

DO: 上述初始化是因为实现了 CommandLineRunner 接口,这个接口只有一个方法就是 run() ,专门用于程序启动时进行初始化 ,大概逻辑如下

  1. 引入本模块
  2. 定义接口,继承 接口 AbstractChainHandler ,重写进行标注 mark,用于不同过滤器链
  3. 实现所定义的接口,实现方法 handler() ,处理逻辑
  4. 当该模块启动时,会自动调用 run() 方法加载到 abstractChainHandlerContainer 容器中,这样就完成了 责任链模式根据mark()标记进行不同流程验证

上述最重要的是 abstractChainHandlers.forEach(each -> each.handler(requestParam));
将参数依次传到责任链进行相关校验

之后基本上都是查询数据库写入缓存的操作,我只写逻辑,没怎么给注释

感觉代码很美,可以多看看写法,很帅

校验完成后尝试从 Redis 缓存中进行获取两地信息,如果获取不到就加锁用 双重校验锁(锁前查询一次,锁后再查询一次) 进行 数据库获取 所有站台缩写与城市之间的映射关系 查询并插入 缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()));
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();
}
}

获取到数据后,再通过同样的方式,获取到 所有北京到杭州站点的车票

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
List<TicketListDTO> seatResults = new ArrayList<>();
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));
}
stringRedisTemplate.opsForHash().putAll(buildRegionTrainStationHashKey, regionTrainStationAllMap);
}
} finally {
lock.unlock();
}
}

再获取到 车票余额、车座次相对应的价格 并进行缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
seatResults = CollUtil.isEmpty(seatResults)
? regionTrainStationAllMap.values().stream().map(each -> JSON.parseObject(each.toString(), TicketListDTO.class)).toList()
: seatResults;
seatResults = seatResults.stream().sorted(new TimeStringComparator()).toList();
for (TicketListDTO each : seatResults) {
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);
}

到这里,基本上就结束了,中间三个代码段都是 对车票部分进行查询、缓存,还有一部分不好的地方,我找个时间补上。

总结一下

这个项目查询车票流程基本这样,对于 网关 实现方式多,本项目是通过继承 AbstractGatewayFilterFactory ,主要重写 apply 方法实现。

有很多代码写得很美,
像一开始的责任链模式进行参数校验,责任链实现在包frameworks.designpattern(前几太看的时候我还记得是如何加载的,现在我已经忘了T_T)
然后就是用 @FunctionalInterface只写一个接口,文中用了很多,举个例子

1
2
3
4
5
6
7
8
9
10
@Override
public List<StationQueryRespDTO> listAllStation() {
return distributedCache.safeGet(
STATION_ALL,
List.class,
() -> BeanUtil.convert(stationMapper.selectList(Wrappers.emptyWrapper()), StationQueryRespDTO.class),
ADVANCE_TICKET_DAY,
TimeUnit.DAYS
);
}

其中 () -> BeanUtil.convert(stationMapper.selectList(Wrappers.emptyWrapper()), StationQueryRespDTO.class) 是实现 CacheLoaderload() 接口

1
2
3
4
5
6
7
8
@FunctionalInterface
public interface CacheLoader<T> {

/**
* 加载缓存
*/
T load();
}

这有什么好处呢?

我们写这样一个接口,通过函数式编程方式,直接写上实现。不用再依次写具体实现,然后再调用,极大增强复用性,查这个写一个实现,查那个写一个实现

雅,太雅了!