Q1.使用rabbitmq怎么样可以保证消息100%投递?原理是什么?
    A1:消息一定是先要去落库,就是你的业务数据要做落库。业务数据做落库的同时其实你要把消息能够发到MQ上。
    这两者应该保证一个原子性,就是相当于我的消息要落库,并且落库的这条消息一定要发到MQ上,这是一个原子性的操作。
    那这样的话就有一个问题,因为数据库和你的MQ根本就是两个数据,相当于两个数据源。他们肯定不能去用Spring的事务或者其他的事务去保证这个原子性。
    那怎么办呢?一般来讲是这样的,我们可以在消息落库的同时插一条日志消息。
    当然这条日志的消息记录要跟你的业务数据库绑定在一起,相当于两条insert。要是一个原子性。然后你再把消息发出去,等他给你回这个ACK,就是broker。收到了你的消息之后,会给你ACK。那你ACK的时候呢,你同时在ACK的回调函数里面,去更新一下这个日志的记录,比如说叫做已完成。当然有一些极限情况,比如说broker没有给你回ACK,可能是网络闪断等等原因造成的。
    那这个时候那个日志记录里边就是一个中间状态,需要用定时任务去扫描的。扫描完了之后再把一些超时的消息再重新拉出来,然后再推过去。就是这么一个过程。但这个过程有一个问题,可能会造成消息的多发。消息多发的时候消费端去做幂等性就可以了,这是一个最简单最原始最通用的解决方案。
    当然这种方案现在在主流的互联网公司大家都在用。你说有没有更高端的呢?其实也有。
    比如说你去做一个统一的消息,这个proxy就是代理平台。当然,这种代理平台还要做什么事?
    首先它的大前提是一定要有一条消息。就是有一个备份数据是一定要保存成功的。如果你的备份数据保存不成功,肯定实现不了100%的可靠性投递,那什么时候备份数据呢?就像我刚才说的,你的业务入库了之后,即使是一条记录也应该成功。
    老师公司现在做的这种方式就是有一个统一的消息代理平台。第一件事情就是把备份的数据同步发到我们的一个中台服务里边。为什么同步发送?就是为了保证它成功,但如果同步发送失败,那肯定会去做一些balance,轮询让它去再尝试去发整个中台服务的其他节点,只要同步发成功了之后,我们回来再去执行本地事务。本地事务成功之后,再去给它发一个应答。
    主要核心思路我简单说一下,第一次是我要发送同步的请求到我的中台服务,把我的消息做一个备份。备份成功了之后,执行本地事务。本地事务执行成功了,之后再去回一次性的。那回这次应答,可能成功,也可能失败。如果成功的话,就是让我们的中台服务帮我去发这条消息。如果失败的话,就回滚这条记录。
    这样其实还会存在问题,因为我的应答也可能收不到。收不到的时候,就需要一个主动的轮询回查的接口。其实也是Rabbit MQ事务消息的实现。只不过老师把这个Rabbit MQ事务消息的实现去拆开了,就是单独用的一个中台去做存储了,这个有机会后面会跟大家去详细的去分享一下。
    当然,我们现在做的东西叫做MQPP,就是Message Queue Proxy Platform。无论是生产端还是消费端,都做了一层代理。这个目的其实很简单,就是为了统一方式,无论是你的生产端,还是消费端,都走我的代理。那我的代理是可制定化的,可配置化的,帮你注册你的消费服务、生产服务。
    Q2:老师,我想问下重启服务器会导致kafka offset偏移导致重复消费吗?
    A2:对于刚才同学说的这个MQ的失败,其实任何MQ都会出现这个失败的情况。就是我的消息重复消费,这个是很常见的一个问题。那怎么去解决?无非就是你要做好幂等,做好接口的幂等性。即使这条消息重复发了十次,那对于你而言你的业务只能是成功一次,其他的九次都应该去失败。其实各种各样的MQ都会出现消息重复的情况,为什么呢?因为你消费的过程中,你会回ACK给broker。也就是说消费成功了以后你其实要更新这个offset的。
    很多MQ都是批量的机制。批量的机制的意思就是说可能消费端一次拉取10条数据。那10条数据,可能我是2 ,3,4,5,7,8,9,10都消费成功了,但是我的第1条跟我的第6条没有消费成功。那会造成一个现象是第1条跟第6条卡死了。那其实我的offset应该变成什么呢?这个时候我的offset绝对它还会保持原来的位置。这种情况会把这10条消息重新给你推过来。然后会继续重复消费,无论是Rabbit MQ还是其他MQ,他都是去用offset去做记录的。
    我再举个例子,比如说一共10条消息。410,消费成功了,但是呢,1,2,3。1成功了,2成功了,3没成功。那我的offset下次更新的时候,我只能更新到2。因为3没有消费成功,所以说我offset只能更新到2,那下一次再拉取的时候310,你可能都需要重新再消费一次,那这个时候需要你消费端做好对它们的幂等性了。
    当然这种情况也有很多主流的解决方案,像Rabbit MQ其实更新offset的时候,对于一批数据里边,如果说其中有一条处理的特别慢,导致整个消费的进度会卡到这里的时候,它会有一个机制。比如说你这个消息一直反反复复总是卡着,或者说你这个消息一直消费不完,超时,它会自己做记录,然后去做丢弃,把它放到一些死信队列或者会去跳过,就是跳过这条消费记录,然后让业务方感知到它会有这么一个机制,这个是Rabbit MQ在4.0以后做了一个兜底。
    同学们在遇到这个问题的时候,都感觉很棘手。想怎么去解决?可能就是其中一条消息,死活消费不完,反反复复的。可能是数据的问题。那么它多了一个skip跳过的机制,然后把消息传到死信队列里。然后让开发同学去人工的去针对于这一条消息去做处理。也就是说,这个思想相当于不能因为一条臭鱼熏了一锅汤。
    其实很多设计都源于我们的生活。有的时候小伙伴们再去学软件的时候,再去想这个设计思路的时候,你按照正常人的思考就可以了,就按照正常的流程去走。其实很多生活上的实例,一些经典的典故,都是你在设计的时候的一些灵感。
    Q3:老师,我想问一下,目前基于消息队列的主流分布式事务的解决方案有哪些?主要用了哪些技术和工具,能展开说说吗?
    A3:现在目前的主流的这种分布式的事务,分布式的消息的事务的解决方案其实有很多种。包括阿里其实也开源了,当然它主要是解决分布式事务的问题。之前我们的姚半仙老师也分享了阿里的那个事务的框架。但其实就目前我了解到而言,任何互联网的大公司,在应对这种高并发的场景下(当然我说的是高并发的产品下啊),它都不会去选择事务去做操作的。
    一般来讲,我们都去做这种最终一致性。为什么这么说?其实就好像回答Q1问题:对于这个消息100%可靠性投递的解决方案也是一样的。Q1我回答的是Rabbit MQ,我怎么去保证可靠性投递。就是说要业务落库,同时还要记一条记录。那我要保证这个业务的数据,跟我记得这条日志的记录,两个要保证一个原子性。怎么保证原子性就意味着要加事务。
    在高并发的场景下加事务,是对你整个Spring框架,包括对底层的这个存储数据库是非常不友好的,尤其是在高并发的场景下。
    所以它的性能,它的吞吐量,它的承载能力会下降几个数量级,这都是有可能的。有的时候会导致我的数据库直接崩溃。因为你的事务过多,它会排队阻塞。
    所以一般来讲,对于分布式事务,其实能不用就不用,因为业界现在没有任何一种能够非常非常好的落地方案。当然,我说的是高性能去解决这种高并发下的分布式事务的问题。所以对于分布式事务这个问题也是比较老生常谈的一个问题了。
    一般的情况下我们都会去做最终一致性。就是像我刚才说的,我第一次一定要保证我的数据的备份。其实跟数据库一样的,数据库在去做存储的时候,它肯定要去做这个log日志的一些写。我举个例子,不论是说Mysql ,Oracle,甚至说一些hbase,它都会有这个一些redo log,然后去对你的commit先做一个日志的记录。其实Rabbit MQ也有,Hbase里边有一个hlog都会有这些备份。所以说它会把这个日志记录的很清亮,在微秒级别的就能提交了。
    所以其实这个问题很简单,想要去解决,万变不离其宗,最重要的一点就是要做这个操作,你要能保证你的记录一定是成功的,然后你完全就可以不加事务了。最终就靠比如说定时的轮询,或者是主动的检查,或者是一些定时任务等等,去做数据的回查工作,去做效验,最终把这个数据去补齐。
    当然其实类似于这种分布式的场景,就是分布式的这种一致性的场景它一定是不及时的。比如说一些即时性的操作,其实很简单。比如说调这个接口,同步的RPC或异步的RPC。我们先说同步的。那同步的RPC过去了之后就应答结果,如果没有结果,我就直接快速失败。比如说像下单支付的场景,除非你自己能够内部的去保证这个完整性,不然就是一个快速失败的概念。
    那如果我下订单成功了有后面的过程,之后我要去做一些后续的处理,比如说发送物流,像这种东西都是可以去做最终一致性的。因为我最开始有一个下单操作。当然我在这里简单跟大家说一下,其实对于下订单这种场景一定是一个单一性的数据库入库操作。
    就好像老师之前在美团,这个订单信息除了主订单之外,还有一些附属信息,关于骑手等一些附属表操作。但是我们会把主信息,主表的信息一定是最关键的信息,把它落库一定要成功落全。然后附带着比如说类似一些非结构化的数据,或者是压缩等数据,这些数据可能是全量的数据了,就存到了主库里边,但是它不是结构化的。其它的四个操作都是异步去做的,就是四个这个附属表都是异步的。那这样的话就可能会导致数据的不一致,可能后面都不完整,那就靠主表去做补偿就可以了。
    Q4:老师,生产环境mysql目前有许多大表,这些数据大部分都是用作不是高并发的查询和报表,不做过重分库分表的话,可否将历史数据迁移到TokuDB的表?
    A4:这个问题其实是一个很通用的问题。用一句比较俗套的话讲,很多东西都是分久必合,合久必分的,为什么这么说呢?我简单跟大家解释一下。
    就像我们架构课程一样,开始是一个单体架构,所有东西都搞到一起,都是一套数据库表,一个application应用跑着。然后跑了一段时间业务量膨胀发展起来,那这样的话就会做服务的拆分和治理,比如做一些分库分表,再去横向拆分,纵向拆分。这个时候我的业务量再做到特别庞大的时候,那会怎么办?举个例子,我可能有交易的这个数据库。其实我做完了订单的数据库,然后交易的数据库,之后还有一些履约中心、物流中心,我把之前的一个垂直化的系统,拆分成了好多个中心。
    这样相当于微服务拆分了,但是也会产生一个比较大的弊端。交易的数据可能一个订单的全量数据存到订单中心,然后交易还有一些支付的信息,存到支付中心,再往后就是履约,去做一些拆单合单的逻辑,接着又到了物流这边,会有一些物流拣货单等,这些所有的全流程其实都是依赖于第一次提交的数据。
    说白了就是,订单数据就是用户加入购物车,下单支付成功的时候,一定要把这些数完全的去留转到它的下游,下下游,下下下游,从订单开始就会把这些数据去留转到支付留转到履约中心留转到物流中心。这样一种方式也不是不好,但是当量大的时候会发现一个问题。
    你的数据库整个订单的业务线,可能就是RDS实例,存储了所有的订单数据。履约中心除了有订单的一些信息之外,还有自己的支付的信息,甚至还有自己履约的信息。再到物流的时候,肯定会有订单的信息,有支付的信息。还有履约的信息,再加上自己物流的信息。这个就是一个增量的过程。
    你的数据库,可能分了好多个系统,每个系统可能都存了很多冗余的数据。而且你要访问不同的数据源,其实面临着一个过程,你要把整个全链路的线条打通。我就想看一个用户的一个全链路行为,就是这个用户从最开始下单到支付成功,到它做履约,拆了几笔单到物流发配送等有一个全面的订单流水信息,就是一个用户的一个场景的一个行为这样一个全量的数据。
    那你会感觉很费劲。为什么费劲?因为你要通过不同的接口去调,然后把它们整合在一起,基于这种情况,数据要做数据同步。我们把数据都同步在ES里,方便去做聚合,去做查询。但是开始的时候可能每个中心都是一套自己的ES,这样数据还是打通不了。其实我们想要的效果是数据最终是打通的。我把服务都拆完了之后,最终还想看到单体能看到的结果,就是说能从开始下单到最后的全站的流程都想跟踪到。
    这个时候要做相互的数据同步的工作,导致分布式系统就比较复杂了。所以就提出了中台的概念。这种中台是说,做订单,外卖也算订单,购买东西也算订单,订机票,订酒店,这些全都算订单。这种是纵向的中台,就是说,我就是一个订单的服务,对外部所有的边缘的周边的服务,提供同样的统一的一种能力,订单就分各种各样的类型,这种是一种做法。
    还一种做法就是一些存储的中台。我把数据都存到一个地方,我不关注底层到底是用什么存储的。但是这个中台就会提供一个聚合的能力,把全链路的存储的过程都聚合到一起,就变成一个真正的这种存储的中台。说白了有了这一套存储的中台,我们的数据都是共享的,大家看到的都是一致的。
    我再回到这个主题,就像刚才说的,这个数据库的这个字段特别多,或者表特别大。这是你现在公司所经历的必然的一个过程,所以就默默享受吧。
    Q5.kafka的数据目录如果磁盘空间大小不足了,想迁移到大一点的磁盘,有哪些注意项?
    A5:这个也是一个比较常见的问题,就是关于kafka数据的迁移。关于数据迁移,大体上可以分为两大类。第一种情况是同集群,第二种情况是多集群,就是不同的机器。
    同集群的话,比如现在kafka有三个节点,然后我新增了一个broker,kafka的broker。就相当于在同一个集群内,又加了一个节点。在有些节点磁盘空间不足的情况下,可能加了一个比较大的磁盘的一个broker,那这个时候你可以采用kafka,他其实都有命令的,还有一些shall命令,在kafka的bin下面,都会有一些迁移的工具,一些命令直接可以去做。
    有一个bin目录下的kafka-reassign-partitions.sh,它可以去做同级群内的topic的迁移。这种方式需要你手工的去做迁移,它不可能去自己帮你去做像负载均衡这样的。知道你的这个数据量多大,然后帮你去做负载,这个它不会去做的,需要你自己人工的去做。这个就是第一种同集群内的。
    第二种情况就是不同的集群。我现在有两个集群,想把其中一个原有的集群里某些topic就某些这个主题。想要去迁移到一个新的集群中,怎么去做?也是一样的。就kafka的闭幕下,还给你提供了一个脚本——kafka-mirror-marker.sh(不同的集群的迁移方式)。只需要指定你连的集群的配置即可。
    当然还有一种你可以在kafka启动的时候就指定好那个集群的配置,即动态的挂载。因为现在都是容器化了,你的一台服务器上动态的可以挂载多块磁盘,它本身也支持多块磁盘,kafka其实他的这个数据就是log。我记得他有一个log那个配置项就是在kafka的配置文件里面是可以指定多个这个地址的,也就是说你可以指定多块磁盘的。当然他可能不支持动态的,你需要重启一下。几种方式大家都可以去了解一下,这个是你要做运维的时候很常见的一个需求了。
    Q6:RabbitMQ监听死信队列的线程可否配置将里面消费不了的消息发送到我们程序里指定的接口,然后再修改数据库逻辑数据状态再重新由生产者到?
    这个问题其实是多种多样的解决方案。你消息就是没有消费成功,入到死信队列里了,你可以重新发。但是你不能无限制的重新发,毕竟你重新发了之后,他可能还是处理不完,还要放到死信队列里,不能让他无限的去循环。所有消息都有一个时效,这样的话你要好好去设计一下你的时效性。比如说你认为他发了三次,如果失败了三次,他就不能从事了,我就得人工补偿了。
    所以一般来讲,有些消息如果真的是到死信队列了,那我就是推荐人工补偿。因为这个概率都不是很大的,消费失败了,你的消费端去记录一下日志就可以了,记录完日志之后,你能补偿就补偿一下,通过自己实现一些业务逻辑,不能补偿的只能是手工去做了。其实呢,在这里我简单想说一句话。
    就软件领域其实你千万不要把很多东西妖魔化。很多东西,包括设计,其实是越简单越好的,而不是说我非得搞很多补偿机制,兜底机制,当然必要的一定要有,但是你千万不要陷入这个坑里,陷入这个坑里就无法自拔了。当然每个人都要有这个过程。最开始我们去想可靠性投递这个事情的时候,当我第一次接触可靠性投递,那我肯定容易陷入坑里了。数据库要提交日志,要保证同源的,那我必须要加事务,不加事务怎么做?不加事务会有很多种情况,比如说第一次消息落库成功,但是跟着那个日志没有落成功,那就不一致了。就算他们两个都成功了,但是我的MQ又不成功,发失败了,或者干脆没发过去。其实这种是必须要把它思考清楚的过程,因为你要保证可靠性。所以说有些东西是需要你去完善,去做优化,去思考的,但是有些东西其实你没必要去做100%。这个是不太可取的。
    举个例子,比如支付宝,我记得开始的时候提出一个TCC概念,就是柔性事务。柔性事务也有一些弊端,就是不能保证100%能把这个TCC分布式搞定,可能一千万订单里边就一两单,甚至三四单会失败。那失败怎么办呢?难道就为了千万分之一千分之三重新做一个很复杂的补偿逻辑,兜底逻辑吗?其实没必要,可能一两个月都不会发生的事情。那你只需要做一件事情,就是把消费失败,错误的东西记录一下,然后去做人工的补偿,就可以了。就是最简单的最有效的一个逻辑。很多东西就不要钻牛角尖儿,你一钻牛角尖儿,整个研发的成本就太高了。
    Q7:老师,我想问下拓展spring actautor实现应用监控的可行性
    这个其实比较适合中小型公司去做,它是一种业务的监控性能,就是监控服务器的资源,监控springboot服务的一些健康状态。其实对于springcloud而言,还不是特别成熟。只是相对更适合中小规模的这个公司去做开发。如果是规模大一点儿的公司,它不仅仅是要监控服务器的指标。
    比如说我们现在做的这个matrix。首先基础设施的指标都要采集,比如硬件软件资源中间件缓存ES等,这些基础指标都需要采集。这样可能说到的这个spring提供的插件儿就不是特别适合,因为不满足你的业务需求,很多指标没有,比如说IOPS,一些磁盘的情况。还有业务的指标肯定采不上来,因为他根本就不支持。
    所以actautor我个人觉得,如果你的公司量级不是很大,其实你用springboot,cloud快速开发一个东西都是可以的。如果量级大一些,像我们其实没有用这个springboot,这一套都是自己去做的,包括前端的gatetway,这个gatetway评测出来其实相比要强很多。
    我觉得不是因为他们做的不好,而是不够契合我们的业务,还有没有做到完整的义务化功能。其实对于gateway这个项目,在我们公司里面我是寄予很大的期望的,包括整个核心代码都是我去写的。就是从这个入口级别的怎么样提升?其实网关这个事情很简单,你就需要做两件事情就足够了。
    当然我说的是怎么样提升网关的性能。第一件事情,你外面过来的请求,怎么能够让他吞吐量能够更高,就是能够承载更多的请求?这个很简单,请求过来,我不去做串行化处理。我都去做异步化处理,比如请求过来了之后,他就是一个HTTP for request 对象。那我就把这个对象拿去扔到一个队列里,我可以选择很多高性能的无锁队列。然后再做后续的处理。在处理的过程中,比如说网关,最重要的就是面临协议转换,比如说你前面HTPP过来转成double,HTPP再转HTPP等,其实这些就相当于转换的过程。
    转换过程就相当于你的网关跟下游的服务做一个通信,你要把这个通信都变成异步化的。当然,其实double 2.7.4.1已经支持了completable future,就是异步化的方式。在double其实很多早几年,他就有这个future,但是一直没实现。现在实现了。其实早期在263的时候,我们自己就已经实现了这个功能了,当然不是用completable future去做的。其实包括RocketMQ,他的主从同步之前是用单个的thread去wait和notifiled。但是在4.7之后,它也采用了这个completable future,其实就比较类似于异步的并行, 类似于future模式,是一个增强的框架。
    Q8:老师 还想问下 工作上技术因为接触的技术,团队水平等原因提升有限,同时年龄都在增长,时间焦虑,那么如何在业余时间更好更快的提高自己的水平和学习新技术 以便能够跳槽或升职到更高职位 有哪些途径和建议吗?
    我个人觉得抓住一点核心,就是你的基础一定要过关。如果基础不过关,其他的即使你学习,其实也没什么提高。只有你基础打牢靠以后,然后再去看其他的东西,才会相对来讲更容易更简单。我举个例子,假设我刚才提到的并发编程,你开始学会用了,只是一个使用的级别。如果你去面试的话,别人问你,肯定会有一些你答不上来的。
    所以其实做什么事情都要从基础开始。第一步先走好,然后再去想着怎么去走第二步。再去想着怎么搞第三步,如果你不是这样去做的,那你肯定学的任何知识都不是那么牢靠。比如说,我现在想精通double,或者精通rocketmq。那我先怎么去做?第一步,你应该先学会这个框架怎么去使用,在工作中应用。然后遇到问题了之后能够debug,跟一下这个代码。然后接下来去看一看相关的博客,或者是自己有能力的话梳理一下整个的这个设计思想,先了解一下。你知道这个设计思想之后,再去看一部分一部分的代码,这样的话就会有头绪。
    分享一下学习的方式,小伙伴们只有在实践中总结,思考问题,才能进步更快,而不是单纯的学习,期望小伙伴在学习课程的过程中,把课程里讲的都用到自己的实际工作中,这样才会进步飞快。
    Q9:老师您好,我想问下,高并发的情况,针对缓存,要注意的事项?
    并发的情况下,还是要看多高的并发,这个要看具体的场景,但是大体上思路是这样的。首先都有这么一个周期,比如开始我的数据库压力扛不住了,那就把一部分数据放到堆内内存里。然后堆内内存可能存储量太大了怎么办?那我就放到缓存里,就是用这种远程缓存,类似于redis。
    然后你会发现高并发的时候,就是很高的地方的时候,比如说正常的这种高峰期,流量,可能不属于秒杀。这种高峰期订单,爆款或者商品,在做促销采购的时候,跟秒杀可能有些不同。因为秒杀可以专门去做独立的服务器,也会做一些限流的手段等,现在这个秒杀的话,其实都不算真正的秒杀了,因为都会有一些流量的控制限流策略。
    我个人觉得针对高并发的挑战在于真正的业务促销,流量的洪峰。像双11,或者是一些爆款热销,或者滴滴早高峰午高峰还有晚高峰,或者像外卖,在中午吃饭,晚上吃饭的时候,这种才是高并发。真正的这个挑战其实是我刚才说的,实际场景中的这种高并发,因为他不允许做流控,一般来讲是不允许做流量的控制或者代码的特殊处理的。如果做一些技巧,去扛住这种压力什么的,这个没什么技术难度。
    刚才我说到缓存,缓存之后你会发现像这种业务的高并发,它的缓存也是扛不住的。当然是针对于你具体的实际的缓存节点数,还有每个节点能扛住多少个并发。比如首页是每个人都去浏览,都去看,好多东西加载,你如果用缓存会特别特别消耗性能。首页如果用缓存的话,这么多人去看就会把你的缓存打满的,这样你后面所有的服务用到缓存的地方肯定都是不可用的。缓存如果不可用,那可能就降低到DB,DB就会有压力,然后就会把DB干死,干宕机。
    所以要合理的去用远程缓存,类似于redis。还有你可以用一些静态缓存,有些后端数据你可以用本地缓存。很多东西你发现做回来之后,又变成堆内缓存了。好多东西都要放到堆内的,如果堆内放不了的话就放到堆外。因为堆内的话,如果商品的数据或者有些东西总更新会影响应用的稳定性。可能有一些特殊的场景就需要做堆外的缓存。其实缓存根据你不同的场景,他会有不同的解决办法和方案。