六、RocketMQ
6.1 消息队列用过哪些?消息队列的作用?
常用的消息队列大概有这四种
- Kafka:吞吐量大概在10 万级,高吞吐,支持topic。一般配合大数据类的系统来进行实时数据计算、日志采集等场景。延迟在 ms 级以内。它的可用性非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用。可以做到0丢失
- RocketMQ:10 万级,支撑高吞吐,支持topic。延迟在ms 级,可用性非常高,分布式架构。 经过参数优化配置,可以做到 0 丢失
- ActiveMQ:吞吐量大概在万级,延迟在ms级。可用性高,基于主从架构实现高可用。有较低的概率丢失数据。
- RabbitMQ:吞吐量大概在万级,延迟在微秒级,这是 RabbitMQ 的一大特点,延迟最低。可用性高,基于主从架构实现高可用。基本不丢数据
优点与作用
- 解耦:以前都是接入系统都是改代码调用。现在直接MQ的形式,一个只管发,一个只管接。
- 异步:程序处理完本职工作之后,把数据写入MQ就可以让他在后台自动执行,就不需要等他执行完再返回了。
- 削峰:和异步差不多,源源不断的请求堆积在这里,就可以把所有的请求都打到MQ,由MQ按顺序执行即可。
缺点
- 系统可用性降低: 俗话说,写的越多,出错的几率越大。
- 系统复杂度提高:原本调用就够了,现在要接入MQ,还要做一些参数的配置,性能调优配置。加大的开发难度。
- 一致性问题: 宕机后MQ未处理的消息以及已处理的消息,和数据库,和Redis的数据一致性问题
我的技术选型是RocketMQ,主要原因如下
- 吞吐量在10万级,延迟低,可以做到0丢失
- 可用性很高,体量到了一定级别,可以采用分布式架构进行扩展
- 底层是用Java开发的,故障排查,二次开发也是非常友好的(Java程序员)
- 阿里开源的,社区较为活跃,出现问题也方便解决
6.2 如何保证消息不丢失?
聊到消息一致性,可靠性传输,我们可以从问题的根源入手。我先列举一些容易出问题的故障点
- **生产阶段:**在这个阶段,从消息在 Producer 创建出来,经过网络传输发送到 Broker 端。
- **存储阶段:**在这个阶段,消息在 Broker 端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。
- **消费阶段:**在这个阶段,Consumer 从 Broker 上拉取消息,经过网络传输发送到 Consumer 上。
生产阶段
在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递:当你的代码调用发消息方法时,消息队列的客户端会把消息发送到 Broker,Broker 收到消息后,会给客户端返回一个确认响应,表明消息已经收到了。客户端收到响应后,完成了一次正常消息的发送。
只要 Producer 收到了 Broker 的确认响应,就可以保证消息在生产阶段不会丢失。有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,就会以返回值或者异常的方式告知用户。
你在编写发送消息代码时,需要注意,正确处理返回值或者捕获异常,就可以保证这个阶段的消息不会丢失
存储阶段
在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。
如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。
对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。例如,在 RocketMQ 中,需要将刷盘方式 flushDiskType 配置为 SYNC_FLUSH 同步刷盘。
集群我不会,后续再更新。
消费阶段
消费阶段采用和生产阶段类似的确认机制来保证消息的可靠传递,客户端从 Broker 拉取消息后,执行用户的消费业务逻辑,成功后,才会给 Broker 发送消费确认响应。如果 Broker 没有收到消费确认响应,下次拉消息的时候还会返回同一条消息,确保消息不会在网络传输过程中丢失,也不会因为客户端在执行消费逻辑中出错导致丢失。
你在编写消费代码时需要注意的是,不要在收到消息后就立即发送消费确认,而是应该在执行完所有消费业务逻辑之后,再发送消费确认。
消息丢失检测
前期代码健壮性不友好的情况,可以在拦截器里编写日志输出,把消费的id号记录下来。
- 生产者,生产一条就记录一条
- 消费者,消费一条就记录一条
这样这样两边对照就可以把丢失的id号 定位出来。也可以通过分布式链路追踪系统 扯远了,以后再说吧
6.3 如何保证消息顺序消费?
一个topic下有多个队列,为了保证发送有序,RocketMQ提供了MessageQueueSelector队列选择机制,他有三种实现:
我们可使用Hash取模法,让同一个订单发送到同一个队列中,再使用同步发送,只有同个订单的创建消息发送成功,再发送支付消息。这样,我们保证了发送有序。
RocketMQ的topic内的队列机制,可以保证存储满足FIFO(First Input First Output 简单说就是指先进先出),剩下的只需要消费者顺序消费即可。
RocketMQ仅保证顺序发送,顺序消费由消费者业务保证!!!
这里很好理解,一个订单你发送的时候放到一个队列里面去,你同一个的订单号Hash一下是不是还是一样的结果,那肯定是一个消费者消费,那顺序是不是就保证了?
参考于 敖丙的CSDN
6.4 如何处理消息过程中的重复消费?
采用 幂等性
幂等性是一个数学上的概念,它是这样定义的:如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。
这里被扩展到计算机领域,被广泛的应用于多次执行产生的影响均与一次执行的影响相同
使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。
- 数据库的唯一约束实现幂等
- 为更新的数据设置前置条件
- 记录并检查操作
数据库的唯一约束实现幂等
我先举一个我自己系统的例子:用户在充值账号余额时,会产生一个账单ID。
我们在实现唯一约束的时候就可以重新创建一个表。伪代码如下
create table aaa( id bigint(15) not null comment '约束id', user_id bigint(15) not null comment '用户id', bill_id bigint(15) not null comment '账单id', money decimal(10,2) not null comment '充值金额', PRIMARY KEY (`id`) USING BTREE, KEY `adasdasdas` (`user_id`,`bill_id`), -- 唯一约束 用户di和账单id ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账单约束表';
这样,我们消费消息的逻辑可以变为:“在转账流水表中增加一条转账记录,然后再根据转账记录,异步操作更新用户余额即可。”在转账流水表增加一条转账记录这个操作中,由于我们在这个表中预先定义了“账户 ID 转账单 ID”的唯一约束,对于同一个转账单同一个账户只能插入一条记录,后续重复的插入操作都会失败,这样就实现了一个幂等的操作。我们只要写一个 SQL,正确地实现它就可以了。
基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,你可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。
参考李玥老师的 消息队列高手课 思想
为更新的数据设置前置条件
在更新数据时,我们可以设置一个更新前的值,如下图。
这里可以加一个充值前金额,这里因为我的体量,并发不大,暂时没加,后面我会根据老板的要求再加的。
如果有重复订单打过来,那我就可以计算充值前的金额,以及当前的付款金额。来付款来实现幂等性。
也可以通过版本号控制,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,一样可以实现幂等更新。
在修改数据记录并检查操作
可以采用Token,UUID的方式实现幂等性。这种方式是通用性比较强的。实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。
具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。
七、Redis
7.1 Redis项目中如何用的?用来做什么?
Redis在项目中主要用于缓存。分布式锁,限流,过滤器。也可以适当的充当小型的消息队列。
最核心的还是他的缓存。
众所周知,Redis是单线程+多路复制机制(伪多线程)。使Redis的性能非常的高。
主要用于存储热点数据,用户身份的有效性。
这里可以展开Redis单线程为什么那么快 展开介绍一下。可以参考下列文章的第二部分
根本不同的场景可以跟面试官都聊一下。比如用于缓存时,
- 如何解决数据过期时间问题
- Redis过期机制(Java中的GC)
- Redis过期策略的六大策略。
- 由六大策略引进LRU算法,LFU算法展开介绍一下
- 数据修改后,MySQL与Redis如何做好数据同步
- Redis底层的数据结构是由什么实现的。
- 体量增到一定量级时,布隆过滤器的诞生。作用,原理可以介绍一下。
比如用于消息队列时,
- 消息队列,发布,订阅模式的介绍
- Redis用于消息队列时的优缺点
比如用于身份校验时,
- 数据结构的选用。可以对Map,List,string 做一个数据结构的性能分析
上述的技术问题,在这些 【Redis技术文章】 中都可以找到答案
7.2 Redis挂了会发生什么?高可用,哨兵模式?
发生什么
Redis挂了,网站能不能访问主要取决于是否在Redis上存了校验Token以及是否在程序里做了try catch异常捕获。
如果存在Token。网站直接会崩溃
如果代码中没有处理好相应的Redis无响应代码 也会崩溃。
上述问题可以通过try-catch代码块来解决。但是这样的话就会造成代码臃肿,最好的解决办法就是采用Redis集群的方式,单台Redis挂了,可以通过另外几台继续提供支撑服务。
哨兵模式
其中一台挂了,或者主库挂了,肯定是要涉及到选主的或者去从操作的。由这个问题可以抛出来 哨兵模式的技术链, 只要扯到Redis挂了,必提哨兵。很多时候 不需要等面试官问,我们可以自由先发挥一些。这样面试官也会对你印象深刻一些。
哨兵主要负责的就是三个任务:监控、**选主(选择主库)**和 通知。
监控:哨兵会周期性的给所有的主从库发送ping命令,监测它们是否处于在线运行状态,
- 如果从库没有响应ping命令,哨兵就会把从库标记为下线状态。
- 如果主库没有响应ping命令,哨兵就会判断主库下线并且开始自动切换主库。
选主 :主库挂了之后,哨兵就需要在很多从库中,按照一定的规则选择出一个从库实例,把它作为新的主库。
通知 :主库重新诞生之后,哨兵就会通知从库,告诉它们新主库的信息。让他们执行replicaof
命令。重新建立之后开始数据同步流程。
一系列流程之后,主库出现了,从库也正常了。一切又回到了出故障之前的那个状态!
从库标记下线的话,比较简单一些,这里介绍一下选主的规则
哨兵选主库时,一般都称为 "筛选+打分"
筛选:(网络波动)除了检查从库当前的在线状态还要判断它网络的连接状态。如果从库和主库响应过慢,并且超过了一定的阈值,那么肯定是不能选择该从库充当我们的主库的。因为一旦该从库选择主库,一旦在后续的写入操作,数据同步操作中网络波动大,或者直接断开连接了,我们还需要重新做一下选择,通知,同步等。这样性能是非常低效的!
打分:(择偶标准)主要有三点如下
- 优先级最高的从库得分高:用户可以通过
slave-priority
配置项,给不同的从库设置不同的优先级,比如不同的从库中的内存配置,CPU配置等 - 同步进度:一般选择一个从库为主库,如果我们从库的数据同步进度更接近与前主库,那么从库切换成主库之后,数据同步的时间消耗更低。性能会更好一些。
- ID 号小的从库得分高。(Redis的默认规定没啥好说的)。在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
这里有一个面试高频题,就不展开了,有点多。直接上一个参考链接,有兴趣的可以了解一下。
7.3 Redis缓存击穿、缓存雪崩、缓存穿透讲讲?
雪崩
雪崩这个东西,见名思意。我们可以理解成一大堆雪冲了过来,一面墙无法抵挡猛烈的攻击。于是冲垮了墙,直接冲向了你们家的卧室!
一般造成缓存雪崩主要是有如下几个原因
- 发大洪水:系统设计问题,明明有100万流量,程序只设计了10万
- 洪水从天而降的意外:缓存key刚好大面积失效,过期时间设计的不合理
- 被瓦匠工糊弄了一下墙倒了:Redis实例宕机
我们在平时解决时,可以避免缓存写入的时间大面积相同,可以在后面加一个随机函数,让过期时间分布的频段多一些。还可以通过服务降级来解决缓存雪崩。
比如在去年新冠疫情的那会。严查所有过往的路人,一旦有咳嗽,发烧一律不予通行。如果没有咳嗽,发烧等情况还持有体检报告的可以回家自行隔离。
在Redis中也是同样道理,如果访问的是核心数据,我们可以放行,如果是访问附加属性我们可以直接返回初始数据,或者网络波动问题。这样就可以过滤一部分附加属性的请求了。
还有一种情况就是,洪水还没来,墙自己倒了。你看这不是赤裸裸的求干吗,你这不就是挑衅洪水的嘛。
一般为了系统业务能正常运行,我们会提前最好做好如下应对措施
- 实现服务熔断
- 实现请求限流机制。
- 高可用集群
服务熔断的话我们可以理解成,为了防止引发连锁反应(积分服务挂了,还能影响订单嘛)我们关掉了用户的积分服务。等修复成功之后再重新开启服务。这样就可以避免其他服务受此牵连。
在业务系统运行时,我们可以监测 Redis 缓存所在机器和数据库所在机器的负载指标,例如每秒请求数、CPU 利用率、内存利用率等。如果我们发现 Redis 缓存实例宕机了,而数据库所在机器的负载压力突然增加(例如每秒请求数激增),此时,就发生缓存雪崩了。大量请求被发送到数据库进行处理。我们可以启动服务熔断机制,暂停业务应用对缓存服务的访问,从而降低对数据库的访问压力
服务限流的话,我们可以理解成早晨上班的警察道路调配。一个路口是流入量是不变的,如果我们想正常运行,就必须把流入速率慢下来。
回到系统的话就是每秒1万个请求,限流之后,每秒1千个请求。再多的请求我们拒之门外排队等候。
高可用集群 的话也算是提前预防了。就好比在双十一或者流量超级春运的时候。流量超级大。我们提前在节日之前把对应的机器设施架设起来。一旦大屏面板监测到大批流量引入我们可以开启备选方案,通过增加机器来解决并发需求。这样也可以达到节省硬件成本的需求。
击穿
缓存击穿主要就是 热点数据失效 。双十一期间如果榜一的商品缓存失效了,恐怕就有悲剧了。一时间所有的请求都打到的数据库上。这是由于热点数据的过期时间设计不符导致的。
我们一般对这类数据会进行提前预热,比如热榜前100的数据,我们会预热30分钟。这样在秒杀的时候,就不会造成在短时间内大量请求打入数据库了。
穿透
缓存穿透顾名思义,就是在玩刺剑时,攒足力气,直冲一处。
回到系统中是这样的意思。黑客 在黑入我们系统时,往往会猜想一些缓存中没有的数据,使大量请求打到数据库,造成缓存穿透。当下次再次请求时,缓存中还是没有查询到,因为从数据库查询时,本来就没有所以也无法写入缓存。
我们的解决方案就是:不管数据库是否存在当前数据,我们都缓存的一个key,给这个key的value中 写入一个null。
八、项目方案
8.1 问秒杀项目:介绍一下你对项目高并发的理解;
高并发意味着大流量,需要运用技术手段抵抗流量的冲击,这些手段好比操作流量,能让流量更平稳地被系统所处理,带给用户更好的体验。
我们常见的高并发场景有:淘宝的双11、春运时的抢票、微博大V的热点新闻等。除了这些典型事情,每秒几十万请求的秒杀系统、每天千万级的订单系统、每天亿级日活的信息流系统等,都可以归为高并发。
很显然,上面谈到的高并发场景,并发量各不相同,那到底多大并发才算高并发呢?
- 不能只看数字,要看具体的业务场景。不能说10W QPS的秒杀是高并发,而1W QPS的信息流就不是高并发。信息流场景涉及复杂的推荐模型和各种人工策略,它的业务逻辑可能比秒杀场景复杂10倍不止。因此,不在同一个维度,没有任何比较意义。
- 业务都是从0到1做起来的,并发量和QPS只是参考指标,最重要的是:在业务量逐渐变成原来的10倍、100倍的过程中,你是否用到了高并发的处理方法去演进你的系统,从架构设计、编码实现、甚至产品方案等维度去预防和解决高并发引起的问题?而不是一味的升级硬件、加机器做水平扩展。
8.2 库存超卖如何解决的?(商城类项目)
库存超卖问题一直是电商系统中的一个热点话题。库存超卖无非就是对扣减库存的把控。下面我们从三个方面分析一下。
- 下单减库存
- 付款减库存
- 预扣减库存(失效)
下单减库存
下单减库存,这种方式很好理解。用户下单后就立马扣减库存,但是有一个问题就是,有些用户在 提交订单之后,未必就会付款 。
这样就会存在恶心下单,然后全都不付款。如果在秒杀活动的时候,遇到这种情况,这个活动就等于无效了。显然不是一个好的方案。
付款减库存
付款减付款,用户在下单后,不会扣减库存,只有在付款的时候才会减少库存。这样的问题就是,秒杀那一刻冲进来的流量。会 有部分人已下单但是付款失败 的情况。付款失败是因为在扣减库存的时候校验为0了
付款减库存,还会造成,下单量大于实际库存量。付款那里如果处理不好的话也会造成 库存超卖 的情况
预扣减库存(失效)
预扣减库存也是现在电商平台,外卖平台,打车平台常用的一种模式。下单后如果5-10分钟后仍不付款,系统就会自动取消。释放对应的库存。
问题并没有从根本上解决
虽然预扣减库存方式极大的解决了库存问题,但是仍然有漏洞。
比如10分钟自动取消,在双十一秒杀期间,如果有人恶意下单后在第9分钟的时候把订单取消了,取消后就立马重新下单。还会占用相应的库存。
解决方案
针对恶意用户下单的情况,我这里简单罗列了如下几种解决方案:
- 我们可以为经常提交订单之后不付款的用户添加对应的标签,当这些用户下单时,进行特殊处理,例如不扣减库存等(具体可以根据需求确定)。
- 在秒杀期间,为商品设置同一个人的最大购买件数,比如最多购买2件。
- 对不付款重复下单的操作进行限制,例如,对同一商品下单时,首先校验当前用户是否存在未付款的订单,并且订单中的商品与再次下单的商品是同一款商品,则提示先让用户付款后再提交订单等。
针对库存超卖的情况,我这里简单罗列了如下几种解决方案:
- 通过补货解决。
- 用户下单时提示库存不足。
秒杀系统如何扣减库存?
在真正的高并发、大流量场景下,大部分秒杀系统会采用 下单减库存 的方式。
在下单扣减库存的业务场景中,需要保证大流量、高并发下商品的库存不能为负。
这里,我们可以通过如下方案解决商品库存不能为负的问题、
- 在扣减库存后,通过在应用程序的事务中判断商品库存是否为负数,如果变成了负数,则回滚事务不再扣减库存。
- 在数据库中设置库存字段为无符号整数,从数据库层面保证无法出现负数的情况。
8.3 Redis缓存的库存怎么解决库存的超卖?
系统加载时,我们可以把整个商品的库存都写入Redis。为了Redis的内存考虑的话,我们可以采用Hash类型存储,只存储商品的ID 以及 库存。
众所周知,Redis是采用单线程执行命令的,所以不存在并发执行的,在每次扣减库存时,我们可以直接取 product_stock_key
的 164310204200001
商品判断是否为0。事务+判断
- 如果为0,直接return。返回秒杀失败,商品库存不足
- 如果大于0,就执行相应的数量扣减操作
8.4 项目过于依赖Redis,你如何解决Redis崩掉了?
项目过于依赖Redis的话,可以配套使用一下主从,哨兵模式。
可以从当前文章中,下列顺序进行查询。
- Redis板块
- Redis挂了会发送什么?高可用,哨兵模式?
- 哨兵模式
8.5 Redis缓存了什么内容?空间不够怎么办?
可以从当前文章中,下列顺序进行查询。
- Redis板块
- Redis项目中如何使用的?用来做什么?
空间不够用的话,主要从两方面入手 技术 和 设备
技术这块,可以聊聊Redis的淘汰策略,参考下列
8.6 如何实现缓存和数据库同步?如何保持数据不丢失?
缓存和数据同步问题,可以参考下列文章。
缓存和数据同步,读写缓存,只读缓存,同步直写策略,异步写回策略
Redis数据不丢失的话,主要用的是持久化机制。可以参考下列文章
8.7 目前的瓶颈?如何提高你的QPS? (秒杀项目)
暂时没遇到。
8.8 聊聊项目遇到的挑战,闪光点,亮点,解决方案吧
最大的挑战还是,SQL优化,系统优化,Redis优化吧。
因为认识我的都知道,我现在一个人负责跨境电商的设计,开发,维护工作。老板付了4000块钱的服务器费用。
因为我对技术比较有追求,所以我从当初的单体服务,重构成 微服务版本。大概10个服务,要跑在2+4,1+4服务器上。
这样的配置跑这样的服务。稍有不慎真的就是OOM了。
SQL优化
- 首先就是索引问题,该加的一个不漏,不该加的一个都不要动
- 大流量的主子表查询,采用在主表存储一个子表的json字段。减少多表关联的开销。
- 数据体量上来之后,可以引入历史表概念,把单表数据过大,冷门数据转移到备用服务器上
- 在第三条的基础上,如果实在庞大,可以考虑分库分表的思想。
- 这里可以再参考一下。MySQL => 调优 或者 索引失效 配合优化
系统优化
目前还没遇到系统优化问题,后期更新,后续会相应的更新。比如CPU,内存,网络性能,网络延迟
Redis优化
- 避免长耗时命令,O(n)的可以直接考虑步入优化队列了。比如List
- 绝对禁止使用keys命令
- 一定要限制hash,set,sorted set大小
- 排序、并集、交集等要不选放在客户端执行,要不就放在Redis的从库执行。放在主库等死吧。
- 可以根据业务的需求,重新配置Redis的持久化机制的级别。(always,every second,never)
- key过期时间优化,尽量采用随机时间,一定不要固定时间写死。
- 分片那里不太懂,后续更新
九、算法
打扰了,我暂时不会 我们下期再见!