• 问:嗯,先来写个题,求一个二叉树里面两个节点之间的路径的最大值,不能走回头路啊。比如这个数里面四到三的路径是最大的,那就是四二一三啊,五到三可以啊,然后返回这个路径上的节点个数啊,比如说四二一三啊就是四。嗯,解释一下你这段代码吧。

我用的是深度优先遍历,要算整个树里面两个节点的最大路径啊,那可以先算左子树的深度啊,然后再算右子树的深度啊,然后再加上根结点啊,就是整个数里面两个节点的最大路径了。然后递归一直往下拆啊,拆到叶子节点啊,然后递归返回的时候,就计算各个子数的最大路径啊,用max 记录一下就可以了。

  • 问:先聊聊你之前做过的项目吧。我看你之前做过订单的模块,你这一天的订单量大概有多少呢?

大概一天十万左右的单吧,具体多少我忘记了。

  • 问:嗯,当时用的什么存储呢?

主要用的是mysql。

  • 问:你们数据库有没有进行分库分表呢?

订单表的话,当时是分了十个库,每个库里面存三十天的订单,然后每天分一百张表。

  • 问:是根据哪个字段进行分库的呢?

按照订单号进行分库分表的,订单号分了三个部分,包含了用户的userId,一个小时级别的时间戳。我记得是距离二零一五年十二月一号的小时数,还有一个趋势递增的i d。我们是先按照userId 呃算个哈希值确定库。然后一个库里面每天都对应一百张表,存了三十天的量,也就是三千张表儿。然后再根据订单号里面的时间戳,就知道去哪找这一百张表了。最后就根据userId模一百就能找到往哪个表写了。

  • 问:为什么要这么设计啊?

主要是为了把一个订单的信息都放到一个库里面。订单库里面除了订单表,还有一些关联表,分到同一个库上就可以走本地事务了,就不用搞什么分布式事务了。还有就是把一个userId的订单信息都放到一个表里面。这样的话就可以支持单个userId的批量订单查询了,然后还能同时支持订单号的检查。所以才考虑把userId还有订单号到一起。然后就是按照时间切表,是为了降低每个表的存储量。历史订单全部存着的话,那跑不了多久就把mysql 打满了。我们这个订单服务毕竟也是个在线的服务,存那么多历史订单也没什么意义啊,也没几个人查,也没修改的,所以只存三十天的。历史的订单数据都会存Hive之类的离线存储啊。去支持离线分析之类的业务。

  • 问:你的订单里边有哪些字段呢?

分几块吧,一块是订单本身的信息。比如说呃订单号,订单状态,下单时间,userId,商家编号,金额,订单明细,运费,金额,收货地址id之类的字段。还有一些支付信息。支付流水号,支付时间之类的,还有一块是订单明细的信息。比如商品名称、单价之类的信息。还有一些优惠活动的信息,比如使用优惠券的类别,i d 数量之类的信息。

  • 问:那你这个两个够宽的啊

不是这样,我们没有把这些列全部都放到订单表里面。我们进行了垂直拆分。你比如说订单明细的信息会单独拆出来,一张订单的明细表,然后通过订单号与订单表关联。然后优惠券的那些字段是放到折扣表里面的。刚才说了,一个订单号的全部关联信息都会放在一个库里啊,我们直接就可以用本地事务来保证这些数据的一致性。

  • 问:那有没有根据订单号之外的字段进行查询的需求呢?

呃,有的,我们是自己实现了mysqlToES的同步工具。大概的原理就是监听blog,然后发消息出去,然后自己消费消息写ES。然后我们后台的的命就通过ES做查询。在ES里面也是按照天去分的索引。然后每个表分别对应不同的索引前缀。然后加个定时脚本啊,每天滚动创建新索引。然后admin 在查询的时候,也会指定查询的时间范围啊,就可以确定查询的索引的范围了。然后查多个索引汇总排序就行了。

  • 问:除了后台admin查,还有没有别的服务会使用你的订单数据呢?

嗯,有,下游服务还有很多,比如售后服务,推荐服务,商家服务,都会消费我们的订单消息。你比如说商家侧的服务,收到这个订单的消息之后呢啊就可以看到这个订单。在商家服务里面呢就不是按照那个订单号去分表了啊,就是按照商家的id去进行分表。我记得商家侧里面有个配送的子服务,可以自动发货。就是商家提前配置好发快递的规则,收到订单就批量跑延迟任务。查订单里面的收货地址,调快递的三方接口下单。

  • 问:那订单那边收到订单信息会不会有延迟呢?

呃,会有一定延迟。这个延迟基本上就是在秒级别的,一般都是可以忍受的。

  • 问:你们配送服务是怎么设置的?

下单成功之后,快递的信息会记录到商家测的表里面,呃,也会往我们的订单服务这边发消息。呃,然后我们也会更新快递单号之类的。后面商家和用户都可以通过这个快递单号呃查我们的配送服务,呃,展示配送状态。呃,然后呃我们的配送服务接各个第三方的openApi的时候,呃,会做一个缓存和回调。那个第三方的openApi会需要我们加回调接口,快递发生状态变更的时候,就会来回调我们这个呃服务接口。然后我们会收到这个状态变更啊,然后在那个配送服务里面就会更新状态。用户查的时候就可以直接查这个配送的状态了。还有就是物流轨迹,物流轨迹我们也是在本地去做了一个缓存,然后用户查的时候,不是实时去查三方的Api。主要是第三方平台可能会贵啊,所以我们自己缓存了这些数据,而且用户频繁来刷配送信息的时候也没必要实时更新。我们只要保证三个小时或者是半天去主动更新这个轨迹和状态就行。那这个订单签收之后会发个消息出来,订单服务就会变更订单状态。然后往定时服务里面加条定时任务。要是用户没有主动点确认收货的话啊,就会通过定时任务结束订单。结束订单的时候就会触发确认支付的逻辑。

  • 问:那你们结束订单的这个定时服务有了解过吗?

呃,我们是搞了个任务调度,然后分片到多个服务实例上扫表,每个实例处理自己分片里面的订单。比如说我有十个实例啊,每个实例负责处理十个表啊,然后一小时执行一次。每次执行都根据我们订单的创建时间批量去查那个到期的订单,查到了就去把这个订单号发给支付的服务。然后走确认支付的流程,然后我们自己的订单状态也会改成结束的状态。

  • 问:扫表来触发定时任务。还会有什么 问题吗?

呃,这个场景下问题不大,用户不主动点结束订单,基本上也不会在意什么时间呃结束订单了。出现延迟了或者是故障了。我们能在小时级别处理的就行了。这就类似于一个兜底而已。

  • 问:那支付是你这边做的吗?

不是呃,我们有专门的支付团队去搞这个支付。我们接的就是他们的支付网关。我对支付里面的逻辑也就是大概了解一点,还挺复杂的。他们是有个应用管理模块,我们去接他们支付服务的时候就会先去走审批。他们给我们分配个支付i d 这种标识。除了我们团购的业务啊,还有其他的ToC的业务啊,还有ToB的业务啊,都会接支付的网关。后面就用这个支付id去摊成本和收益,他们也会根据这个去做隔离。他们当时是接了支付宝、微信,还有银联啊,然后每个支付方式都还有对账。当时好像是每月定时跑hive 表和各个支付厂商对账。还有就是他们在那个真正支付之前,还会过一下风控,防止那种刷单的。除了支付啊,还支持提现,充值之类的功能。有用户账户变更完了之后,还会跟我们自己的财务系统之类的交互。然后商家入驻之后啊,他们在商家后台里面也有对账功能啊,也是支付团队支持的。

  • 问:这个问题先到这,那你有了解过支付是怎么保证幂等性的吗

大概知道方案。他们就是在创建订单的时候,往redis 里面记一个键值对。呃,key 是订单号啊,value 就是支付的流水号,对一个订单进行重复支付的话啊,在重复支付的时候就能查到啊,然后就不再发起新的支付了,而是去查询一下这个支付的情况。然后提醒用户已经支付成功了。

  • 问:现在用户付完钱之后,用户这边是怎么知道支付完成了呢?

呃,是这样的,在创建支付订单的时候会给个回调地址。用户支付完了之后,微信和支付宝会回调我们的接口啊,支付服务就知道订单支付情况了。然后这个支付服务就会再发个消息出来啊,或者是回调我们业务侧的RPC接口通知到我们。然后我们改订单状态,然后push服务会给客户端发push消息通知到用户。

  • 问:要是微信没有回调你们的接口呢,那比如说微信故障或者是你们域名访问有问题之类的。

支付服务里面应该是会有定时任务去请求微信的接口查相应订单的状态。

  • 问:这个定时任务也是走用掉我们的扫表的方案吗?

呃,不是的,是走的RockeMQ延迟队列啊,这个场景对时间比较敏感。走定时任务扫表的话要是延迟了啊,就会导致用户进行无意义的重试。

  • 问:嗯,了解RockeMQ延迟队列的实现机制吗?

RockeMQ延迟消息分很多等级啊,每个等级对应一个延迟时间。RockeMQ在收到延迟消息的时候会先放到一个特定的topic 里面去。这个topic 里面啊每个延迟等级对应一个延迟队列,然后RockeMQ会启动一个timer 定时器定时去检查消息是不是到期了。要是到期了的话,就转正常的topic 里面去了。这个timer 定时器的实现一直被喷。然后现在RockeMQ就改成了线程池的实现了。然后还加了异步转发的功能,就是性能提高了很多。

  • 问:异步转发是什么?介绍一下吧。

就是扫到到期消息的时候,先发到一个LinkedBlockingQueue里面,然后有个线程池消费这个LinkedBlockingQueue,再转发到 正常topic 里面,这个地方就需要限制LinkedBlockingQueue的长度啊,要不就OOM了。我们公司用的RockeMQ定时任务被改造过,改成了时间轮加RocksDB实现。然后也不用指定延迟消息的等级了,直接指定延迟时间就可以了。这还挺好的。还加了延迟消息的类型,在存储上做了隔离。那这样的话,支付这种非常重要的延迟消息啊,就不会被其他低优先级的延迟消息影响。

  • 问:好,那你们的支付接口有没有被刷过?

有,是对参数做了加密,验签之类的处理。后面还有风控拦截,安全性应该挺高的。

  • 问:你了解过他们的加密算法吗?

嗯,嗯,没有深入了解过,估计也是用的AES之类的加密。我记得他们做过密钥轮转之类的优化啊,就隔一段时间生成新密钥。当时他们内部推广支付网关的时候,说是一个密钥加密的数据量越多啊,密钥就越不安全。所以要定时换啊,然后要是密钥被盗了,还能手动换新的密钥。

  • 问:那更换密钥之后,之前加密的数据能兼容吗?

哦,那肯定呢,估计是有版本或者是密钥上做了什么兼容吧。这个我没了解过。

  • 问:行,下面先问到这里吧,感觉时间不太够了。这个JVM调优了解过吗?

了解过一点。

  • 问:好,我这边有一个需求,我们在发微博的时候都会生成一个地址。这个地址大概的格式是国家-省-市 这种三级地址,现在出现了一个热点事件。比如说某些明星宣恋爱了。然后海量用户在转发微博啊,这是需求的背景。然后我们发现线上服务出现了OOM,dump下来之后呢,发现很多地点字符串。比如“中国-北京-海淀”这种字符串。想想可能是什么原因呢?

客户端请求里面带的都是国家-省-市这种字符串吗?

  • 问:假设是这种情况吧,你怎么办呢?

呃,那能在客户端先对这些字符串编码,然后再发到服务端吗?

  • 问:嗯,不太行,一个是客户端需要用户更新。还有就是这个编码表比较大,会导致客户端变大,然后就会导致下载量低啊,增加卸载量之类的。

呃,那就在说到这个地址的时候,先进行切分吧。然后分别对国家,省,市这些字符串调一下intern() 方法。这样的话字符串就进常量池了,全局就只有一个字符串。

  • 问:那要是常用池里的字符串放多了会有什么问题呢?

呃,会触发GC的,

  • 问:了解JVM常量池是怎么维护常量字符串呢?

呃,这个我不记得了

  • 问:提示一下啊,它是用Map维护的。那intern()多了之后会发生什么?

哦,懂了哦,会导致那个哈希冲突。然后HashMap会扩容啊,intern()变慢,那需要提前把map 的长度指定的大一点才行。呃,

  • 问:如果内存存不下呢?

我会考虑给这些地址进行编码吧。比如北京对应0001,上海对应0002呃,这样然后存到mysql 里面去,然后服务里面只存这个编号,需要展示这些地点名称的时候,再查mysql

  • 问:这个方案理论上能实现的。但是请求量大的话,每一条微博都要国家省市的话就要查三次mysql 对数据库的压力比较。

呃,是有点儿。那我在服务里面加个本地缓存吧,比如说LoadingCache之类的实现啊,这样的话就可以减小mysql 压力。

  • 问:那我发微博的时候是怎么考虑把这个地点进行编码呢?

呃,我们也是通过前面说的那个表,先查国家编码,然后再查省份编码,然后再查城市编码啊,最后组成一个完整的编码。用Long值应该就可以了。然后存的时候就存这个Long值。然后读取的时候再解这个编码,查表得到具体的地址字符串展示给用户。

  • 问:发微博的地点肯定是来自全国各地。那这个会不会导致你这个LoadingCache的数热点数据被淘汰出去啊?

呃,可能会有,缓存淘汰之后就会再查库,可能就造成抖动了。你是这个意思吧?

  • 问:嗯。那怎么办呢?

我想想哈。不行我们就上redis 缓存。直接把全量的省市信息放到缓存里面,反正也是静态数据啊,即使丢了也可以恢复。

  • 问:想想啊,重复的静态数据,可以再想想啊,我们能怎么处理呢?

哦哦,那个共享内存的方式感觉也行,

  • 问:考虑放到本地磁盘上吗?

呃,也可以的,再加一个内存映射读起来更快。

  • 问:行。时间不太够了,今天先到这里吧。