登录流程和登录状态保存

  • 乘客登录:首先根据前端微信小程序传来的code进行乘客openid的解析,根据这个openid进行落库。在我们的项目中不会对外暴露这个openid,使用的是自己系统的自增id。如果没有注册就insert一条。接着用redis存储登录状态,key是UUID,value是自己数据库的自增id。我们这里用的aop+threadlocal+注解类似网关的prehandle,处理前端每次都携带的token字段,然后在redis里根据这个key进行查询,如果是没有这个key就是没有登录过期了,如果是key为空就是没有登录。然后根据redis里的这个value存储到threadlocal中,后续就可以从threadlocal中get这个字段进行操作。
  • 司机登录:流程与上面的类似,只不过多了阿里云的身份校验。

threadLocal的原理

threadlocal是根据线程独立的变量,也就是说每一个线程可以存储不同的threadlocal,彼此是隔离的。其原理是每一个Thread类都有一个threadLocalMap,这个map相当于一个hashmap,key是threadLocal对象,value是存储的值。threadLocal查询的时候就会先获取当前线程,根据这个线程找到threadLocalmap,再找到其中的threadLocal对应的值。

threadLocal的内存泄露问题

threadLocalmap的key是弱引用,value是强引用,也就是说如果threadlocal没有外部强引用就会被回收,而value不会被回收。这个时候就会有内存泄露问题了。解决方法是不再使用threadLocal就手动remove

搜寻周围司机和订单推送

在司机开始接单之后会把自己的位置上传到redis,然后进行等待,在这个过程中司机会不断轮询自己redis的list是否有数据。同时乘客端下单之后会新增一个定时任务,使用redis的gredius命令查询方圆5km内的符合要求的司机。并且为这些司机的list都添加该订单号。

为什么要用定时任务

因为执行一次并不一定就能及时发现司机,轮询又太消耗cpu,所以采用了定时任务。使用xxl-job可以管理这些定时任务。

异常怎么处理的

有一个兜底策略就是超时15分钟自动取消,如果在运行过程中出现失败首先xxl-job会显示执行失败,同时记录日志。

如果周围只有一个司机,但是一直不接单,如何保证他的list中消息不重复

这里使用了redis的set集合,key是订单的id,每当任务调度搜索到最近的司机之后首先会校验司机的id是否在set中,如果不在才可以加入自己的list。如果在的话说明是重复提交了。

任务调度何时终止

在查询周围司机之前都会先检查当前订单的状态,通过另一个微服务的调用。如果这个订单的状态发生改变说明已经有司机接单了,这个任务就会提前终止。

为什么不用mq实现这一部分逻辑

redis的速度较快,毕竟用户的规模不会很大,list的规模也不会很大,暂时不会存在大key问题。而且也需要批量处理这些消息和频繁的删除,如果为每一个司机都建立一个queue有一点奢侈,综合下来还是redis中list能够简化操作。

司机抢单

每个司机在接单的过程中都会轮询自己在redis中的list是否有数据,如果有前端就会显示并且可以选择订单进行抢单。主要逻辑是首先会检查是否还有订单,这个是通过在redis的一个标识实现的,再下单的时候会给redis一个标识。目的是不用去数据库查找当前订单的状态节约数据库开销。为了防止超卖要用分布式锁,为当前订单id加锁。加锁之后再次校验是否有标识,进行接单操作后删除这个标识和释放锁,完成抢单逻辑。

分布式锁用的是什么

基于redision的分布式锁,锁住的是司机list中的订单id。本质还是基于setnx的,只不过redission有看门狗机制能够延长执行时间,保证任务能够进行完成而不会出现删除别人的锁的情况。这里可以优化的地方可以做成lua脚本,反正标识也是在redis里面的。逻辑是查询标识然后异步下单给mq。也会缩短司机抢单的响应时间,

司乘同显

在司机正在前往起始点和进行服务的时候,乘客也需要知道司机在哪里。司机会定时将自己的数据上传到mongodb中,乘客端会找到最近时间的司机点坐标,然后调用map微服务渲染出路径。

为什么使用mongodb,用redis或者mysql存储呢

  • 路径数据是需要持久化保存的,用redis毕竟不是落库,而且存储的数据量大且只需要找出最近的一个点,大部分的数据没有用,会浪费珍贵的缓存空间,所以最好不用redis。
  • mysql也不太适合,存储的点数据量很大而mysql超过1kw查询效率就会很低,也会存在深度分页问题,如果多个订单同时进行并发也是mysql的问题。多一个少一个坐标点不会有很大的问题,所以使用另一个数据库进行存储,经过调研mongo能够满足。

基于rocketmq的延迟队列完成批量写是怎么实现的

这里真是犯了一个大错误。

为了实现高效率写可以合并多次写请求,实现批量写。具体是设置rocketmq的批量处理功能。但是会增加代码的复杂度,更加轻量级的做法是线程池和阻塞队列。

异步编排

在完成订单后会有一系列的计算过程,例如防刷单校验,分账计算,推送账单等。

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
[Start]
|
+--> [获取订单信息] ----+
| |
+--> [获取司机位置] ------+--> [等待两个任务完成]
|
v
[计算距离]
|
+-----------------+-----------------+
| |
[距离 > 2公里] [距离 <= 2公里]
| |
[抛出异常] +----+----+
| |
[计算实际里程] [获取订单数量]
| |
[计算费用] [计算奖励]
\ /
\ /
[计算分账信息]
|
[封装并更新账单]
|
[End]

考虑将获取订单信息和获取目的地距离进行并行处理,使用supplyAsync实现两个线程,最后合并一起进行计算得到结果;再校验距离是否再可以接收的范围内,如果是不是刷单就并行获取订单数量和计算实际里程,这个是根据mongodb中的数据计算的,得到这两个信息进行账单计算和推送账单。主要是将几个不相关的微服务通过多线程并行获取了。

如何测试的40%提升

测试的该方法的执行时间发现比原来快了40%。

分布式事务

TCC的流程

try-confirm-cancel,是一种业务入侵的分布式事务方法。try阶段会预留资源,confirm阶段确定分布式事务下所有的执行器都已经成功预留,如果全部成功就执行confirm提交事务,如果失败了就执行补偿措施cancel。这种方式比较灵活,可以自己控制事务,但是也入侵了业务,会造成一些别的问题,例如幂等性问题,空回滚和资源倒挂。

解释一下这三个问题

  • 幂等性:由于网络抖动等问题导致在confirm阶段可能出现超时重试,但是最终两个请求都收到了。在seata的解决方式是用一个事务状态表,在confirm/cancel的时候提交给事务状态表,在开始的时候又去检查字段状态,如果是已经confirm了的,第二个请求就会被忽略,从而达到了幂等性
  • 空回滚:在try阶段没有成功的微服务仍然执行了回滚,需要对失败的微服务进行区别。在seata里仍然是用事务状态表,在try成功了才会有status,回滚阶段有status的才能回滚
  • 资源倒挂:由于网络原因try阻塞了,结果回滚了,但是后续网络恢复了仍然会预留资源,使得资源被倒挂。解决方案仍然是事务状态表,在回滚的时候插入一条suspend=4,在try的时候先校验是不是倒挂了,如果=4就不用预留资源。

具体在代码里是怎么做的

例如我们的支付模块,需要修改金额和商品数量,然后下单。这个时候就要用到分布式事务了。每一个微服务模块都要实现tcc三个接口,在try的接口上使用@TwoPhaseBusinessAction,指定在seata服务中的id以及回滚和提交的方法名,接着在代码实现响应逻辑。对于数据库而言增加一个frozen字段,try阶段首先减少数量,frozen增加,cancel了就做相反操作,如果confirm了就减少frozen实际提交。

规则引擎

用规则引擎做了一些计算

  • 例如根据距离和实际预估出价格,例如在7点之前起步价是多少多少钱,每公里的价格计算。使用规则引擎防止将规则和价格硬编码在代码里,方便实际部署动态变更。
  • 计算账单也使用到了规则引擎,例如按照什么比例分成,根据订单多少给予奖励

但是实际工作里面都不太会用到这个你知道是为什么吗

我觉得首先是学习成本的问题,这个东西语法确实也很冗杂,虽然在java代码里面不用硬编码了,但是几个ifelse却要用更长的代码表示。某些时候这个服务也会成为瓶颈。

实习项目

业务是什么

实现人工智能检测平台和主平台的接口,具体是定时发送指令到视频流平台,截图入库后通过这个接口的一系列定时任务提交给人工智能检测平台返回结果进行统计。统计完成后用kafka异步落库并返回给前端。除此之外还有一些文档和增删改查的工作。

xxl-job是怎么用的,你的项目里面有哪些定时任务会需要用到这个

xxl-job是一个开箱即用的分布式调度平台,与传统的quataz相比可以动态改变cron表达式,还具有可视化的界面。由于实习单位后端有很多的定时任务,写死cron表达式不利于管理

  1. 首先是截图指令,会传递几个参数通过http控制视频流平台,其中参数会包含间隔时间,视频流平台也会根据这个进行定时落库。
  2. 接着是定时给人工智能平台传输,因为平台有很多的检测算法,接口都不是很统一。这里有一些很长的if判定过程。例如未佩戴安全帽 未佩戴绝缘手套 靠近变压器告警、工作服,或者是画面无人。这里用了一个线程池来并发完成这些图片的校验和传输,还用了策略模式把这些校验的很长的ifelse给简化了。具体是用一个公用的策略接口,然后上下文类中替换这个接口的实现类来完成策略选择。最后在策略工厂里面用哈希表来保存类型和具体策略的映射关系。
  3. 还有一个定时任务是每天早上1点钟删除保存了3天以上的照片
  4. 人脸注册每分钟从数据库抽200张给人工智能平台,人工智能平台自己也有数据库,在这个系统里面也有,这个时候虽然是通过http传输的,但是也可以看作一个分布式的事务,由于这个系统业务范围不是我们的,所以用分布式事务也不太合理。所以就想到用kafka保证最终一致性。

kafka怎么保证最终一致性的

主要依赖kafka的exactly once语义。要实现只提交一次且只能消费一次需要三方都协调。对于生产者也就是我们这个代码里面,要使用kafka的幂等和事务,配置kafka的幂等性可以使生产者的消息只会被kafka接收一次,配置事务使得消费者在消费消息的时候只能看到提交事务的,对于kafka而言,由于我们公司也没配置kafka集群,所以只设置了acks=1,也就是写入成功后返回回调函数,这样能保证kafka不丢消息。消费者为了不重复消费消息也需要做到幂等,这里的幂等校验是redis进行去重的,然后再手动提交ack。这样能保证如果前面的平台执行失败了,这里的接口也不会落库;如果执行成功,通过kafka的这个exactly once就可以只落库一次。