系统设计类题目汇总三

20 秒杀系统的一些拓展和优化

20.1 你发送消息时,流程是将消息发送给MQ做异步处理,然后消费者去消费消息,之后调用运营商的发送消息接口,那如果调用运营商的接口后消息发送失败怎么办?

确实,对于这种核心业务流程,特别是涉及到运营商级的集成,我们还可以考虑以下更加精细化或者更加完善的方案:

  1. 熔断器模式

    • 当运营商接口连续失败达到一定阈值时,启用熔断器,暂时停止对运营商的调用。这有助于保护系统和运营商不被持续的失败请求所淹没。
    • 熔断器在一段时间后会进入“半开”状态,此时允许有限数量的请求尝试调用运营商接口。如果这些请求成功,熔断器关闭并恢复正常操作;如果仍然失败,则保持熔断状态。
  2. 回退策略

    • 当运营商接口不可用时,考虑使用另一个备份的发送方法。这可以是另一个运营商,或者是一个完全不同的消息传输方式(例如电子邮件、站内消息等)。
  3. 优先级队列

    • 在某些情况下,一些消息可能比其他消息更加紧急。使用优先级队列确保紧急消息优先发送。
  4. 反压力策略

    • 如果消息发送速率超过了系统或运营商的处理能力,应该减缓消息发送的速率,这称为“反压力”策略。
  5. 消息确认与检查

    • 如果可能的话,可以要求运营商返回消息发送的确认。这样,如果没有收到确认,可以安全地重试,因为你知道之前的尝试没有成功。
  6. 业务流控

    • 在某些情况下,可能需要暂时停止或限制生成新的消息,直到问题得到解决。
  7. 动态配置与切换

    • 对于多个运营商或发送方法,提供一个动态切换的能力。这样,当一个方法出现问题时,系统管理员可以快速切换到另一个方法,而无需重新部署或重启系统。

最后,不论选择哪种策略,都应该进行充分的测试,模拟各种可能的失败情况,确保系统在真实环境中能够稳健地运行。

20.2 购买商品时,如果有很高的并发,那这个时候按照你的逻辑,用户每买一个商品就插入订单数据,锁库存,有什么优化措施?

我回答的是:redis端先预减库存,然后再使用一个MQ,这个MQ专门用来存放用户的购买逻辑,每当MQ满了就一次性发送到 MySQL 中进行数据的落库

20.3 接上一个问题,如果很多用户都是购买同一个商品,这个时候你批量锁库存落库就会存在行锁的问题,导致用户这些数据只能一条一条落库,这个时候又要怎么优化呢?

答:在面试官的提示下,我答的是将商品库存数据拆分,比如对于同一个商品,每一个地区都插入一个库存数据,比如A商品,库存记录中有上海的库存记录,有北京的库存记录,这样南方的用户选择扣上海的库存,北方的用户扣北京的库存,这样虽然是同一件商品,但是由于是两条库存记录,因此能够分担并发的压力

您的答案是一个很好的策略,也即是说,通过对库存进行地理区域的分拆,有效地降低了单一记录上的锁竞争。这种方法的关键在于将热点数据进行细分,以达到负载均衡的效果。

20.3.1 这种方法具有以下优点:

  1. 降低锁的竞争:如您所说,不同的地理区域可以操作不同的库存记录,从而减少对同一个库存记录的并发访问。

  2. 提高响应速度:由于锁的竞争减少,用户可以更快地得到响应。

  3. 更好的扩展性:当某个地区的用户增多时,可以进一步地细分该地区的库存,例如,将北京细分为东城、西城等。

  4. 实时库存管理:对于电商平台来说,这种方法还可以实时地调整库存,例如,当上海的库存不足,而北京的库存充足时,可以考虑从北京调拨库存到上海。

20.3.2 然而,这种策略也有一些潜在的问题:

  1. 数据管理复杂性增加:需要为每个地区管理库存数据,这可能导致数据管理变得更加复杂。

  2. 库存同步问题:例如,如何确保当商品从一个地区调拨到另一个地区时,库存数据正确无误?

  3. 用户体验问题:如果用户想要购买的数量超过了他所在地区的库存,但在其他地区还有充足的库存,如何处理?

总之,通过对库存进行地理区域的分拆是一个很好的策略来降低锁的竞争,但同时也需要考虑到上述的潜在问题,并为其提供解决方案。

20.4 接上一个问题,如果这里分割库存不按照地区来分,而是直接划分成多个组,每一个组的库存数相同,然后请求来临时负载均衡,库存扣减完成后就关闭到这行数据的路由,这样的话就不存在调整库存到其他组了吧?

对,您提到的这种策略也是可行的,通过将库存分成多个组,并在每个组上设定相同的库存数,请求来临时可以进行负载均衡。这种方式的好处是减少了锁竞争,提高了系统的并发能力。不过在实施该策略时需要考虑以下几点:

  1. 库存划分与合并:将库存分成多个组后,需要在每个组上设定相同的库存数。同时,需要设计合适的机制以便在某个组的库存不足时可以及时合并其他组的库存,避免因某个组的库存不足而导致库存的浪费。

  2. 库存同步:在高并发场景下,每个组的库存会被多个线程同时访问和修改。需要确保各个线程对库存的访问和修改都能够正确同步。

  3. 负载均衡算法:需要选择适合的负载均衡算法来分配请求到各个库存组。这里可以选择简单的轮询、随机等算法,或者更复杂的基于权重的负载均衡算法。

  4. 数据一致性:如果库存数据存储在多个数据库中,需要考虑数据一致性的问题。这里可以使用两阶段提交、最终一致性等方法来保证数据一致性。

这种策略可以有效地降低锁竞争,提高系统的并发能力。但在实施该策略时需要充分考虑上述问题,并根据实际情况进行优化和调整。

21 rabbitmq怎么保证消息不丢失

在 RabbitMQ 中,有几种方式可以确保消息不会丢失:

  1. 持久化队列

    • 声明队列时,将其设置为持久化的。这样,即使 RabbitMQ 服务器重启,队列中的消息也不会丢失。但是,这并不能保证在服务器重启之前成功将消息写入磁盘。
    • 在 Java 客户端中,可以这样创建持久化的队列:channel.queueDeclare("queue-name", true, false, false, null);
  2. 消息持久化

    • 将消息设置为持久化的,这样即使 RabbitMQ 服务器重启,消息也不会丢失。但是,这并不能保证在服务器重启之前成功将消息写入磁盘。
    • 在 Java 客户端中,可以这样发布持久化的消息:channel.basicPublish("", "queue-name", MessageProperties.PERSISTENT_TEXT_PLAIN, "message".getBytes());
  3. 发送方确认机制

    • 发送方可以通过设置确认机制来确保消息已经成功发送到 RabbitMQ 服务器。在消息成功发送到服务器后,发送方会收到一个确认信息。
    • 在 Java 客户端中,可以这样设置发送方确认机制:channel.confirmSelect();
  4. 接收方确认机制

    • 接收方可以通过设置确认机制来确保消息已经成功处理。在消息成功处理后,接收方会发送一个确认信息给 RabbitMQ 服务器。
    • 在 Java 客户端中,可以这样设置接收方确认机制:channel.basicAck(deliveryTag, false);
  5. 集群部署与镜像队列

    • 通过将 RabbitMQ 部署成集群,并设置镜像队列,可以确保即使某个节点发生故障,消息也不会丢失。

需要注意的是,上述方法可能会影响 RabbitMQ 的性能。例如,开启消息持久化和确认机制会增加 RabbitMQ 的 IO 操作和网络传输。因此,在选择适当的方式来确保消息不丢失时,需要根据具体的应用场景和性能要求来权衡。

22 如何解决Redis中的Bitmap偏移量过大导致空间浪费的情况

大佬们请教个问题,现在有个场景需要统计每个设备的日活信息,设备的编号采用的是雪花ID的方式,目前想采用Redis的Bitmap的方式进行数据存储,但是有个问题是,假如1号那天有个设备上线了计算出偏移量为1将bitmap的指定的位设置了1,然后又有一个设备的id计算出的偏移量超过了Bitmap的最大限制,导致redis直接开辟了512M的内存,如何防止偏移量过大呢?还有假如一个设备计算出的偏移量是1,另外一个设备计算出的偏移量为100000,实际1号那天只有2个设备上线,那么2-99999就属于无效的空间导致内存的浪费,这种有什么方式可以解决?我知道的有谷歌的EWAHCompressedBitmap可以进行压缩,但是这个只能用在单实例上

22.1 思路一

给一下我的思路:假设用一个序号标识设备id在原来bitMap中的顺序,现在将这个id的前x位当做bitMAp的标识,后y位作为他在第i个bitmap中的位置,然后采用懒加载策略,只有当这个设备用到时才创建这个设备所在的bitMap,这样应对极端情况。
比如现在有100个设备id,编号是0-99,需要用大小为100bit的bitMap存储,假设现在将其分为20个bitMap,每一个bitMap只负责统计5个设备的活跃信息,假如某一天只有第1号和第99号设备上线,那么只需要创建第0号和第19号这两个bitMap,总共花费的内存是10bit;计算过程以id为99的设备为例:该设备在第99/5=19个bitMap上的索引为99%5=4的bit位上被标记

22.2 思路二:

一定要统计所有设备的日活吗,设备能不能分组,然后统计每一个设备组的活跃信息,这样也能压缩空间了

22.3 直接使用roaring bitmap

Roaring Bitmap是一种高效的Bitmap实现,比传统的Bitmap更加紧凑,适用于大量数据的情况。它在许多场景下都表现得相当出色,比如在大数据、数据仓库、搜索引擎等场景中。Roaring Bitmap的核心思想是将Bitmap分割成多个容器,根据容器中1的数量采用不同的数据结构进行存储,从而节省空间。

Roaring Bitmap的特点包括:

  1. 高效的存储:通过智能地选择数据结构,Roaring Bitmap可以高效地存储数据。比如,当一个容器中的1非常稠密时,Roaring Bitmap会选择一种紧凑的数据结构进行存储,从而节省空间。

  2. 快速的操作:Roaring Bitmap提供了一系列快速的操作,比如位的并集、交集、差集、翻转等。这些操作都是高度优化的,可以在很短的时间内完成。

  3. 广泛的应用:由于其高效和灵活性,Roaring Bitmap在许多场景下都非常适用。比如在大数据分析、数据仓库、实时数据流处理、搜索引擎等场景中都有广泛应用。

Roaring Bitmap的工作原理如下:

  1. 将Bitmap分割成多个容器:Roaring Bitmap将64位的Bitmap分割成多个16位的容器。每个容器都包含65536个位。

  2. 根据容器中1的数量采用不同的数据结构:Roaring Bitmap会根据容器中1的数量选择不同的数据结构进行存储。比如,当一个容器中的1非常稀疏时,Roaring Bitmap会选择一种称为"Array"的数据结构进行存储;当一个容器中的1非常稠密时,Roaring Bitmap会选择一种称为"Bitmap"的数据结构进行存储;当一个容器中的1非常集中时,Roaring Bitmap会选择一种称为"Run"的数据结构进行存储。

  3. 动态切换数据结构:随着数据的变化,Roaring Bitmap会动态地切换数据结构。比如,当一个容器中的1变得更加稠密时,Roaring Bitmap会从"Array"切换到"Bitmap"。

总之,Roaring Bitmap是一种非常高效和灵活的Bitmap实现,适用于大量数据的场景。它不仅可以高效地存储数据,而且还提供了一系列快速的操作,可以满足各种需求。

22.3.1 Roaring Bitmap是谁推出的

Roaring Bitmap是由Daniel Lemire、Owen Kaser、Nathan Kurz等人共同推出的。Daniel Lemire是该技术的主要发起人和贡献者之一,他是加拿大蒙特利尔大学(University of Montreal)的一位教授,同时也是一个活跃的开源软件开发者。

22.3.2 对于容器中1的分布情况(稀疏、稠密、集中)的检测:

  • 稀疏:如果一个容器中1的数量相对较少,那么这个容器就被认为是稀疏的。Roaring Bitmap会将这些1的位置直接存储在一个数组中,这个数据结构称为"Array"。
  • 稠密:如果一个容器中1的数量相对较多,接近或超过一定的阈值(例如容器大小的一半),那么这个容器就被认为是稠密的。Roaring Bitmap会将这个容器表示成一个Bitmap,这个数据结构称为"Bitmap"。
  • 集中:如果一个容器中1的数量是连续的或集中在某个范围内,那么这个容器就被认为是集中的。Roaring Bitmap会将这些连续的1表示成一个起始位置和长度,这个数据结构称为"Run"。

22.3.3 "Run"是一种特殊的数据结构,用于表示连续的1。一个Run包含两个字段:起始位置和长度。例如,如果一个容器中的第3到第10位都是1,那么可以使用一个Run表示这个范围,起始位置是3,长度是8。给我一个run结构的demo

"Run"数据结构并没有在Java的RoaringBitmap库中以单独的类的形式存在,而是作为RoaringBitmap的一部分实现的。这种数据结构最适合在连续的位上表示1。但是我可以用一个简单的Java代码来说明如何表示和操作"Run"结构。

public class Run {
    
    
    private int start; // 起始位置
    private int length; // 长度

    public Run(int start, int length) {
    
    
        this.start = start;
        this.length = length;
    }

    // 检查一个值是否在这个Run范围内
    public boolean contains(int value) {
    
    
        return value >= start && value < start + length;
    }

    // 获取这个Run的起始位置
    public int getStart() {
    
    
        return start;
    }

    // 获取这个Run的长度
    public int getLength() {
    
    
        return length;
    }

    // 获取这个Run的结束位置
    public int getEnd() {
    
    
        return start + length;
    }

    // 扩展这个Run的长度
    public void extend(int len) {
    
    
        this.length += len;
    }

    @Override
    public String toString() {
    
    
        return "Run [start=" + start + ", length=" + length + "]";
    }

    public static void main(String[] args) {
    
    
        Run run = new Run(3, 8);
        System.out.println("Run: " + run); // Run [start=3, length=8]

        System.out.println("Contains 2? " + run.contains(2)); // false
        System.out.println("Contains 5? " + run.contains(5)); // true

        System.out.println("Start: " + run.getStart()); // 3
        System.out.println("Length: " + run.getLength()); // 8
        System.out.println("End: " + run.getEnd()); // 11

        run.extend(3);
        System.out.println("Extended Run: " + run); // Run [start=3, length=11]
    }
}

在这个例子中,我们定义了一个Run类,它有两个字段:startlength。我们也定义了一些方法来操作这个Run数据结构,例如检查一个值是否在Run范围内、获取Run的起始位置和长度等。这个例子可以给您一个基本的理解,但在实际的Roaring Bitmap实现中,Run数据结构和其他数据结构(如Array和Bitmap)会更复杂并进行优化。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/132299837