• 系统设计类题目汇总四


    25 十个异步入库任务,如何保证他们原子入库?(字节一面)

    了解了你的问题背景,确保10个异步入库任务原子性执行(即要么全部成功,要么全部失败)有以下几种方法:

    1. 数据库事务
      如果所有的入库操作都是在同一个数据库中进行的,那么使用该数据库的事务机制是最直接的方法。你可以将这10个任务放在同一个事务中,任何一个失败都会导致整个事务回滚。

    2. 分布式事务
      如果这些任务跨越了多个数据库或服务,你可能需要使用分布式事务。2PC (Two-Phase Commit) 是一种常见的分布式事务协议,但请注意,它可能会导致系统的复杂性增加。

    3. Saga模式
      对于长时间运行的事务或跨多个服务/微服务的事务,Saga是一种替代的方法。Saga将一个长时间运行的事务拆分为多个较小的事务,它们各自完成一部分工作。如果在Saga的过程中出现错误,将执行一系列补偿事务以回滚先前的操作。

    4. 消息队列
      使用支持事务的消息队列,如Apache Kafka或RabbitMQ。你可以首先将任务发送到队列,等待确认所有任务都成功后再从队列中移除它们。如果任务失败,它们可以留在队列中进行重试或进一步处理。

    5. 幂等性
      保证每个任务都是幂等的,即它们可以被安全地重试而不会产生重复的效果。这样,如果任务失败,你可以简单地重新执行它。

    6. 外部协调服务
      使用像Apache ZooKeeper或etcd这样的分布式协调服务来跟踪每个任务的状态。如果任何任务失败,使用这些服务来确定哪些任务需要回滚。

    确保10个异步任务的原子性可能会增加性能和复杂性的开销,因此在选择解决方案时要权衡利弊。

    26 写一个接口应该考虑哪些点

    26.1 如果你接到一个需求,这个需求涉及到访问其他的api,你能从不同的角度,安全性,可用性、高性能角度,说明一下这个接口应该具备哪些能力嘛

    答:

    当然可以。如果你接到一个需求,需要设计一个接口来访问其他的API,从安全性、可用性和高性能的角度,该接口应具备以下能力:

    1. 安全性:

    1. 鉴权 (Authentication): 确保只有合法的用户或应用程序可以访问你的接口。常用的方法包括API密钥、OAuth、JWT等。

    2. 授权 (Authorization): 确保已经鉴权的用户或应用只能访问它们被允许的资源和操作。

    3. 数据加密: 使用HTTPS进行传输,确保数据在传输过程中不被截取或篡改。如果需要,还可以使用端到端加密。

    4. 错误处理: 避免显示敏感信息,例如数据库错误或调试信息。

    5. 防御性编程: 对输入进行验证和清理,以避免SQL注入、跨站脚本(XSS)等常见的安全威胁。

    6. 频率限制: 限制API的调用频率,防止恶意用户进行洪水攻击或过多请求。

    2. 可用性:

    1. 冗余: 如果可能,确保有备份或多个服务器实例来处理请求,以避免单点故障。

    2. 负载均衡: 使用负载均衡器,确保所有的服务器实例都有均匀的流量,并在某个实例故障时自动切换。

    3. 健康检查: 定期检查API的健康状况,确保其正常运行。

    4. 优雅的降级: 当外部服务或资源不可用时,提供有意义的错误信息或回退到备用的服务。

    5. 文档: 提供详细且易于理解的API文档,方便其他开发者使用。

    6. 使用限流算法:调用别人的时候,比如运营商接口,可以在运营商一方使用漏桶算法,如果想要应对突发流量,同时保护自己,使用令牌桶算法或者滑动窗口计数器

    7. **监控:**监控的时候发现负载过高,添加新节点,

    3. 高性能:

    1. 缓存: 使用缓存技术(如Redis、Memcached等)来缓存经常被查询的数据或结果,减少数据库或外部API的查询。

    2. 异步处理: 对于需要长时间处理的请求,考虑使用异步处理,然后再通知用户结果。

    3. 优化数据库查询: 确保数据库查询已经优化,避免N+1问题,使用索引等。

    4. 并发处理: 使用并发编程模型,确保API可以同时处理多个请求。

    5. 压缩: 使用gzip或其他技术压缩API响应,减少数据传输的大小和时间。

    6. 连接持久化: 使用持久化连接如HTTP/2,以减少建立和关闭连接的开销。

    7. 监控性能: 使用监控工具来跟踪API的性能,如响应时间、错误率等,以便进行及时的优化。

    这只是对这个话题的一个初步概述,每个点都可以深入探讨,取决于具体的应用场景和需求。

    26.2 还有没有其他需要考虑的点

    当然,除了上述提到的关于安全性、可用性和高性能的建议之外,还有其他一些重要的考虑因素。以下是对一个API应具备的其他能力的扩展:

    4. 扩展性:

    1. 模块化设计: 设计API时,应考虑到将来可能的变更和扩展,模块化的设计可以确保在不影响现有功能的情况下添加新功能。
    2. 版本管理: 随着业务需求的发展,API可能需要进行更改。通过合理的版本管理策略,可以确保向后兼容,同时支持新的功能。

    5. 透明度与监控:

    1. 日志记录: 记录API的使用情况,包括访问者、时间、响应时间、返回的数据和任何可能的错误。
    2. 告警系统: 在出现异常或错误时,系统应能自动发送警报,以便及时处理。
    3. 可视化监控: 使用工具如Grafana、Kibana等,以图形化的方式显示API的性能和使用情况。

    6. 用户友好性:

    1. 清晰的错误信息: 当用户发送的请求有问题时,返回的错误信息应该足够清晰,使用户能够理解问题并采取相应的措施。
    2. 支持多种格式: 考虑支持不同的数据格式,如JSON, XML等,以满足不同用户的需求。

    7. 兼容性:

    1. 跨平台: 保证API可以在不同的操作系统和设备上使用。
    2. 跨语言: API应设计成语言无关的,以便各种编程语言都可以方便地调用。

    8. 业务相关:

    1. 限流与计费: 如果API是一个付费服务,需要有一个机制来跟踪用户的使用情况,并根据使用量进行计费。
    2. 数据备份与恢复: 确保有备份策略,并且可以在数据丢失或损坏时恢复。

    9. 测试:

    1. 自动化测试: 建立自动化测试,确保API的每次更改都不会引入新的错误。
    2. 压力测试: 测试API在高流量下的表现,确保其在实际生产环境中的稳定性。

    以上只是对API设计和实施时应考虑的方面的一个总览。具体的需求和实践可能会有所不同,但这为你提供了一个良好的起点。

    27 一个接口A会调用接口B和C,B返回的时间是100ms,C返回的时间不固定,可能是200ms,也可能是800ms,现在要求A接口在500ms内收到B和C中任意一个接口返回结果,A就返回结果给用户。如何实现

    27.1 方法一:使用CompletableFuture发送两个异步调用请求,然后再使用CompletableFuture的静态方法编排两个异步请求的返回结果,其中可以加一个500ms的时间戳,如果500ms内有任意一个请求返回则返回,都没有返回就抛异常

    import java.util.concurrent.*;
    
    public class InterfaceCaller {
        private static final Executor executor = Executors.newFixedThreadPool(2);
    
        public static String callInterfaceB() {
            // Simulate calling Interface B
            // ... your implementation here ...
            return "Response from B";
        }
    
        public static String callInterfaceC() {
            // Simulate calling Interface C
            // ... your implementation here ...
            return "Response from C";
        }
    
        public static void main(String[] args) {
            CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> callInterfaceB(), executor);
            CompletableFuture<String> futureC = CompletableFuture.supplyAsync(() -> callInterfaceC(), executor);
    
            try {
                String result = CompletableFuture.anyOf(futureB, futureC).get(500, TimeUnit.MILLISECONDS);
                System.out.println(result);
            } catch (TimeoutException e) {
                System.out.println("Timeout exceeded!");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    
    • 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
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    27.2 方法二: 以Apache Kafka为例,详细描述下发消息、消费消息再到返回消息的完整过程:

    1. 消息下发:
    • 生产者创建:接口A作为消息的生产者,首先需要创建一个Kafka生产者实例。

    • 主题选择:接口A选择或创建一个主题(例如request-topic)来发布其请求消息。

    • 发布消息:接口A生成一个唯一的请求ID并发布消息到request-topic。这个消息除了包含业务数据外,还应该有其他元数据,如请求ID、时间戳等。

    2. 消息消费:
    • 消费者创建:接口B和C作为消息的消费者,它们分别创建Kafka消费者实例并订阅request-topic

    • 消息读取:接口B和C监听request-topic,当接口A的请求消息到达时,它们会从主题中拉取消息。

    • 业务逻辑处理:接口B或C读取消息内容,并开始进行必要的业务逻辑处理。

    3. 消息返回:
    • 选择返回的主题:完成处理后,接口B或C选择一个返回主题(例如response-topic-interfaceBresponse-topic-interfaceC)来发布其响应消息。

    • 发布响应消息:接口B或C生成响应消息,确保在其中包含原始请求的ID和处理结果,然后发布到相应的返回主题。

    • 接口A消费返回消息:接口A此时转变为消费者的角色,它订阅并监听response-topic-interfaceBresponse-topic-interfaceC等待响应。为了确保在500ms内获得响应,接口A设置一个定时器。

    • 超时与响应处理:如果接口A在500ms内收到了来自接口B或C的响应,它会中止其定时器并处理返回的消息。如果超时,接口A可以返回一个错误消息或默认响应。

    整个过程涉及了消息的生产、消费、业务处理和响应,是一个典型的发布-订阅模型。使用Apache Kafka,你可以利用其高吞吐量、持久性和分布式特性来实现这一过程。但同样要注意,引入消息队列会带来一定的延迟,尤其是在高并发的场景中,因此在设计时应权衡实时性和可靠性。

    27.3 使用Kafka和使用CompletableFuture实现异步操作各有优劣。下面是两者的比较:

    使用Kafka

    优点

    1. 解耦:生产者和消费者之间解耦,它们可以独立地进行扩展、部署和维护。
    2. 持久性:Kafka提供了消息持久性,即使消费者或生产者崩溃,消息仍然存在并可以被重新消费。
    3. 可靠性:Kafka设计用于处理大量消息流,具有高可靠性和容错能力。
    4. 可扩展性:Kafka集群可以水平扩展,以满足更高的吞吐量需求。
    5. 后处理能力:由于消息被存储,你可以稍后进行消费,这对于日志处理、分析等后处理任务很有用。

    缺点

    1. 延迟:Kafka的消息传递可能不如直接调用快,尤其是在高并发的情况下。
    2. 复杂性:引入Kafka会增加系统的复杂性,需要管理和维护Kafka集群。
    3. 资源开销:Kafka需要额外的资源如存储、CPU和内存。

    使用CompletableFuture

    优点

    1. 简单性:使用CompletableFuture实现并发操作通常比设置和维护Kafka更简单。
    2. 速度CompletableFuture为内存中的操作提供了近乎实时的响应速度。
    3. 直接性:直接使用Java API,无需额外的中间件或依赖。
    4. 灵活性CompletableFuture提供了丰富的API,如组合、链式操作等,使得编写复杂的异步逻辑更为方便。

    缺点

    1. 局限性CompletableFuture主要用于单个JVM中的并发操作,不适用于分布式系统或跨服务的通信。
    2. 非持久性:如果应用崩溃,CompletableFuture中的操作可能会丢失。
    3. 资源限制:所有并发操作共享同一个JVM的资源,如果任务太多,可能会导致资源耗尽。

    总结:

    • 对于在单个应用或服务内需要快速并发处理的场景,CompletableFuture是一个很好的选择。
    • 对于需要跨应用、跨服务或分布式环境中的消息传递、解耦和持久性的场景,Kafka或其他消息队列是更合适的选择。

    选择哪种方法取决于你的具体需求、资源和项目的复杂性。

    28 YGC的原因和解决方案

    28.1 如果你负责的项目,经常发生ygc,是什么原因

    答:

    我的答案:

    (1)内存泄露,资源没有回收

    (2)应用不断new出新的对象

    (3)minorGC时的并发线程数设置的过低,回收速度赶不上

    gpt4答案:

    经常发生Young GC(YGC)可能是由多种原因导致的。以下是一些常见的原因及其描述:

    1. 短生命周期的对象产生过多

      • 如果应用创建了大量的短生命周期的对象(即对象很快被创建然后变得不可达),这些对象会迅速填满年轻代(Young Generation)并触发YGC。
    2. 年轻代空间设置不当

      • 如果年轻代的大小设置得太小,YGC可能会频繁发生。你可以考虑调整年轻代的大小来适应应用的需求。
    3. 代码存在不良的数据结构或算法

      • 例如,使用了产生大量临时对象的循环或算法。
    4. 外部因素

      • 如频繁的文件I/O操作、数据库查询等,可能间接导致大量的临时对象创建。
    5. 使用了不适合的框架或库

      • 某些框架或库可能在内部创建了大量的临时对象。
    6. 频繁的字符串操作

      • 如大量的字符串拼接操作可能会产生大量的临时字符串对象。
    7. 频繁使用了缓存

      • 如果缓存策略或大小设置不当,可能会导致大量的对象进入年轻代。
    8. 应用负载突然增大

      • 如应用突然接收到大量请求,可能会短时间内产生大量的临时对象。

    要诊断和解决频繁的YGC,你可以采取以下措施:

    • 使用JVM监控工具,如jstat、VisualVM或Java Mission Control,来查看GC的频率、持续时间和内存使用情况。

    • 使用Java的堆分析工具,如HeapDump或jmap,来查看哪些对象占用了最多的年轻代空间。

    • 根据上述工具的输出,调整年轻代大小或优化代码来减少短生命周期的对象的产生。

    • 在优化后,持续监控以确保问题得到了解决。

    28.2 YGC的根引在哪儿

    答:方法区,栈帧中的局部变量表中,也存在引用,同时还有常量池

    28.3 针对不断new出的对象,如何解决

    针对不断被new出来的对象,以下是一些解决方案和建议:

    1. 对象池化:

      • 对于经常创建和短时间内就被销毁的对象,你可以使用对象池。例如,Apache Commons Pool 提供了一种通用的对象池化解决方案。通过重用对象,你可以减少需要频繁创建和销毁的对象的数量,从而减少GC的压力。
    2. 减少临时对象的创建:

      • 优化代码,避免创建不必要的临时对象。例如,对于字符串操作,可以考虑使用StringBuilderStringBuffer来减少临时字符串的创建。
    3. 优化数据结构和算法:

      • 审查代码中使用的数据结构和算法,确保它们不会产生大量的临时对象。
    4. 缓存:

      • 对于经常被创建但状态不经常改变的对象,可以考虑使用缓存来存储这些对象,而不是每次都重新创建。
    5. 延迟对象的创建:

      • 只有在真正需要对象时才创建它,而不是预先创建。
    6. 调整JVM参数:

      • 调整Young Generation的大小,这可能会影响YGC的频率和持续时间。
    7. 使用单例:单例能够节约空间,避免对象的重复创建

    29 假如说现在一个系统的平均响应时间由250ms到达了2.5s,持续了很长时间,造成这个问题的因素有哪些,没有标准答案

    答:

    (1)DDOS攻击,SYN攻击

    (2)jvm的垃圾收集时间过长

    (3)正常的机器负载太高,而且都是正常流量

    (4)可能后端系统的某一个服务器集群的一个leader节点挂了,比如nginx,api-gateway,或者业务集群

    (5)调用失败,引发了服务的雪崩

    (6)同步读取或者删除了redis系统中大key

    (7)redis的大hash表在扩容

    (8)正常的流量突然

    30 防爆破(希音校招一面)

    30.1 防爆破有了解吗

    答:

    当然了解。爆破攻击,通常称为暴力破解攻击(Brute Force Attack),是一种试图猜测用户名、密码或其他认证凭据的攻击方法,通常通过尝试所有可能的组合来实现。以下是一些常见的防爆破策略和建议:

    1. 账户锁定:在短时间内连续多次登录失败后,暂时锁定账户。这可以阻止攻击者连续尝试。

    2. 延迟响应:每次登录失败后,增加下一次尝试的响应时间。这会使暴力破解变得非常缓慢。

    3. 验证码:使用图形验证码、短信验证码或其他形式的二次验证,以确保真实用户的身份。

    4. 多因素认证:使用如短信、电子邮件、硬件令牌或生物特征等多种认证方式,增加破解的难度。

    5. 登录尝试次数限制:限制用户在一定时间内的登录尝试次数。

    6. IP 黑名单:检测并记录多次登录失败的IP地址,并将其加入黑名单,阻止其进一步的登录尝试。

    7. 密码复杂性:要求用户设置复杂的密码,包括大写字母、小写字母、数字和特殊字符,并定期更改密码。

    8. 账户名策略:避免使用容易猜测的账户名,如“admin”或“root”。

    9. 日志和监控:记录所有的登录尝试,并使用监控工具检测异常行为。

    10. 使用Web应用防火墙 (WAF):WAF可以帮助检测和阻止暴力破解攻击。

    11. 教育用户:教育用户不要使用容易猜测的密码,并定期更改密码。

    12. 密码哈希:在存储密码时,使用强哈希函数(如bcrypt或Argon2)并加盐,以增加破解的难度。

    这些策略可以单独或组合使用,以提高系统的安全性。最重要的是,定期审查和更新安全策略,以应对新的威胁和攻击手段。

    30.2 密码哈希:在存储密码时,使用强哈希函数(如bcrypt或Argon2)并加盐,以增加破解的难度。为什么这种可以增加破解的难度呢

    密码哈希和加盐是密码存储的两个关键技术,它们可以显著增加破解密码的难度。以下是为什么这两种技术可以增加破解难度的原因:

    1. 密码哈希

      • 不可逆性:哈希函数是一种单向函数,这意味着从哈希值很难(或说几乎不可能)反推出原始输入。因此,即使攻击者获得了哈希值,他们也无法直接知道原始密码。
      • 确定性:对于同一个输入,哈希函数总是产生相同的输出。但是,即使是微小的输入变化,也会产生截然不同的哈希值。这使得攻击者无法通过分析哈希值的模式来猜测密码。
      • 强哈希函数:如 bcrypt 和 Argon2 设计上是计算密集型的,这意味着生成哈希值需要相对较长的时间。这种特性使得暴力破解或字典攻击变得非常缓慢和不切实际。
    2. 加盐

      • 独特性:盐是一个随机生成的值,与每个密码结合在一起进行哈希。这意味着即使两个用户使用相同的密码,由于盐的不同,它们的哈希值也会不同。
      • 抵御彩虹表攻击:彩虹表是预先计算好的哈希值列表,用于快速查找和破解哈希值。通过为每个密码添加独特的盐,可以使彩虹表攻击变得不切实际,因为攻击者需要为每个可能的盐值生成一个新的彩虹表。
      • 增加破解成本:即使攻击者想要针对单一哈希值进行暴力破解或字典攻击,由于盐的存在,他们必须为每个哈希值单独进行破解,而不能一次性破解所有哈希值。

    综上所述,密码哈希和加盐可以显著提高密码的安全性,使得攻击者即使获得了存储的哈希值,也很难破解出原始密码。

    30.3 有没有采用加token的方式,来防爆破呢

    是的,使用令牌(token)是一种有效的方法来防止暴力破解攻击。这种方法通常与其他安全措施结合使用,以增强系统的安全性。以下是如何使用令牌来防止暴力破解的几种策略:

    1. 限制令牌的有效期:为每个登录尝试生成一个短暂的、唯一的令牌,并在一定时间后使其失效。这意味着攻击者不能使用旧的或无效的令牌进行暴力破解。

    2. 令牌计数器:为每个用户或IP地址设置一个令牌计数器。每次登录尝试都会消耗一个令牌。当令牌用完时,用户或IP地址将被暂时锁定,直到令牌重新生成。

    3. 令牌桶:使用令牌桶算法限制登录尝试的速率。每个用户或IP地址都有一个令牌桶,桶中的令牌数量代表允许的登录尝试次数。每次登录尝试都会消耗一个令牌,而令牌会以固定的速率重新生成。

    4. 双因素认证令牌:除了用户名和密码外,还需要一个一次性的令牌来登录。这个令牌可以通过硬件设备、短信或手机应用生成。即使攻击者知道用户名和密码,没有令牌也无法登录。

    5. 验证码:在登录表单中添加一个验证码,要求用户输入显示的字符或数字。这可以确保真实的用户正在尝试登录,并阻止自动化的暴力破解工具。

    6. JWT (JSON Web Tokens):在某些应用中,可以使用JWT作为令牌,其中包含用户的身份信息和其他元数据。JWT可以被服务器验证,确保其内容没有被篡改。

    使用令牌的方法不仅可以防止暴力破解,还可以增强系统的整体安全性。然而,令牌系统也需要适当的管理和维护,以确保其有效性和安全性。

    31 如何设计缓存(字节一面)

    当设计缓存时,通常首先考虑以下因素:

    • 容量:缓存应具有大小限制,超出容量时需要进行清理。
    • 淘汰策略:如LRU(最近最少使用)、FIFO(先进先出)或LFU(最少使用)等。
    • 并发:为了支持高并发访问,缓存应该是线程安全的。
    • 持久化:考虑是否需要将缓存数据持久化到磁盘。

    基于以上考虑,一个简单的缓存设计如下:

    public class Cache<K, V> {
    
        private final int capacity;
        private final Map<K, V> map;
        private final Queue<K> keys;
        
        public Cache(int capacity) {
            this.capacity = capacity;
            this.map = new ConcurrentHashMap<>();
            this.keys = new ConcurrentLinkedQueue<>();
        }
    
        public V get(K key) {
            return map.get(key);
        }
    
        public void put(K key, V value) {
            if (map.size() >= capacity) {
                K oldestKey = keys.poll();
                if (oldestKey != null) {
                    map.remove(oldestKey);
                }
            }
            map.put(key, value);
            keys.add(key);
        }
    }
    
    • 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
    • 26
    • 27

    这是一个简单的FIFO缓存。

    32 文件下载的实现与安全性考量(字节二面)

    实现文件下载,通常使用HTTP的Content-Disposition header来提示浏览器进行下载。

    安全性考虑:

    • 权限检查:确保用户有权下载请求的文件。
    • 路径遍历攻击:不要直接使用用户提供的路径参数,否则攻击者可能会尝试访问不应被公开的文件。
    • 文件类型检查:确保文件类型是安全的,并且与声称的类型匹配。
    • 速率限制:对同一用户或IP地址的下载速率进行限制,以防止资源耗尽或拒绝服务攻击。
    • 使用HTTPS:确保文件内容在传输过程中不被窃取或篡改。

    综上所述,设计缓存和处理文件下载都涉及多方面的考虑,特别是在安全性和性能方面。这只是一个简单的概述,实际应用中可能需要根据具体需求进行更深入的设计和优化。

    33 如何实现协程(字节国际化电商提前批一面)

    1. 什么是协程?

    协程(Coroutine)是一种用户态的轻量级线程,它不依赖于传统的操作系统级线程的上下文切换来实现。协程的调度完全由用户空间程序控制。因为它不涉及内核态与用户态之间的上下文切换,所以效率更高。

    2. 操作系统最小是线程,那协程怎么实现?

    协程并不是由操作系统内核直接支持的,而是建立在应用程序或库之上的一种编程模型。线程的上下文切换涉及到用户态和内核态的切换,而协程的切换只在用户态完成,因此协程的切换更快。

    协程的实现依赖于以下几个关键技术:

    • 保存和恢复上下文:协程在切换时需要保存当前的执行上下文(如CPU寄存器值、栈指针等)并恢复其他协程的执行上下文。

    • 非阻塞I/O或异步I/O:这使得在等待I/O操作时,协程可以让出控制权给其他协程执行,从而提高总体效率。

    • 协作式调度:与抢占式多线程调度不同,协程通常使用协作式调度,这意味着它们需要显式地交出控制权。

    3. 多个线程对多个协程原理设计

    多个线程可以与多个协程结合使用,例如为了利用多核CPU的并行性。这种模型称为M:N模型,其中M个线程运行N个协程。

    原理和设计:

    • 协程调度器:每个线程都有一个协程调度器,它维护一个协程的运行队列。

    • 负载均衡:当某个线程上的协程数量太多或太少时,可以将协程迁移到其他线程上,进行负载均衡。

    • 同步和通信:由于存在多线程,协程之间的通信可能涉及跨线程通信。这需要额外的同步机制,例如使用锁或原子操作。

    • 事件通知:结合事件驱动的模型,如epoll、kqueue等,使得在等待I/O或外部事件时,相关的协程可以被挂起,其他协程可以继续在该线程上运行。

    总之,协程提供了一种在用户态进行高效任务调度的机制。当与多线程结合时,它可以在并行和并发之间提供更好的平衡,使得程序更加高效、简单和可扩展。

    4. 协作式调度:与抢占式多线程调度不同,协程通常使用协作式调度,这意味着它们需要显式地交出控制权。什么是协作式调度,和抢占式调度有什么不同?

    协作式调度和抢占式调度是两种不同的任务或线程调度方法。它们主要在调度时机和机制上有所区别。

    1. 协作式调度 (Cooperative Scheduling):
    • 定义:在协作式调度中,当前运行的任务或线程将一直运行,直到它自愿放弃CPU的控制权,或者它明确地让出CPU给其他任务或线程。

    • 特点

      • 任务需要显式地让出控制权(例如,通过调用一个yield函数)。
      • 任务一旦开始运行,除非它自己放弃控制权,否则不会被其他任务中断或抢占。
      • 调度开销相对较低,因为只有在任务自愿让出控制权时才进行。
    • 优点:更为简单,预测性好。

    • 缺点:如果某个任务不让出控制权,它可能会阻塞整个系统或导致其他任务得不到执行机会。

    2. 抢占式调度 (Preemptive Scheduling):
    • 定义:在抢占式调度中,任务或线程不必显式地放弃CPU的控制权。而是操作系统或调度器在合适的时机(例如,定时器中断或优先级更高的任务出现时)会强制地从当前运行的任务中取回控制权,并分配给另一个任务。

    • 特点

      • 任务可以被外部因素(如中断或调度器)抢占。
      • 调度器会根据一些策略(如优先级、轮询或最短任务优先)来选择下一个要运行的任务。
    • 优点:可以保证系统的响应性,即使某个任务在长时间内持续运行也不会阻塞系统。

    • 缺点:调度开销可能相对较高,因为任务可能会经常被中断和恢复。

    总结:协作式调度依赖于任务自愿让出控制权,而抢占式调度允许系统在任何时间点强行中断任务。协程通常采用协作式调度,这是因为协程切换是在用户态完成的,而无需涉及操作系统的内核调度器。

    34 让你设计一个应用层协议怎么设计

    设计一个应用层协议通常要考虑以下几点:

    • 目标和需求:确定该协议的目标。例如,是否是为了传输文件、实时数据还是远程命令执行。
    • 消息结构:定义消息的格式,包括头部、正文和尾部。例如:[header][body][footer]
    • 命令(请求)和响应:确定支持哪些命令和相应的响应格式。
    • 状态管理:确定是否需要管理连接状态。例如,是否需要登录、握手等。
    • 错误处理:定义错误码和错误消息,确定在出现错误时如何通知对方。
    • 版本管理:协议可能需要迭代,所以定义版本号是很重要的,以便向后兼容。
    • 安全性:确定是否需要加密、验证或其他安全措施。

    35 MQ的应用场景

    消息队列(MQ)有以下几个典型的应用场景:

    • 解耦:在分布式系统中,服务之间可以通过MQ进行通信,使系统各部分能够独立开发、部署和扩展。
    • 异步处理:当一项操作需要较长时间完成时,可以将操作放入MQ,由后台进程进行处理,从而不阻塞前台进程。
    • 流量削峰:在高流量时期,可以先将请求放入MQ,然后按照系统的处理能力逐个处理。
    • 数据同步:在分布式系统中,可以使用MQ来同步各个节点的数据。

    36 TCP和UDP的区别及应用场景

    • 区别

      • 可靠性:TCP是可靠的,它保证了数据的有序性和完整性;UDP则是不可靠的。
      • 连接:TCP是面向连接的;UDP是无连接的。
      • 速度:由于TCP有更多的错误检查和确认机制,UDP通常比TCP快。
      • 用途:TCP用于需要可靠传输的应用;UDP用于需要快速传输的应用。
    • 应用场景

      • TCP:Web浏览、文件传输、电子邮件等。
      • UDP:视频流、实时游戏、VoIP等。

    36.1 LOL和CF等游戏是基于UDP的吗?

    确实,很多实时多人在线游戏(如《英雄联盟》LoL、《穿越火线》CF等)基于UDP协议来传输数据。原因有以下几点:

    1. 低延迟:UDP不需要建立连接,也没有重传机制,因此它可以提供更低的延迟。
    2. 实时性:在实时游戏中,玩家的位置、状态和动作需要实时更新。如果使用TCP,因为其保证数据的完整性和顺序,可能会导致因为等待某个数据包而造成延迟。而在实时游戏中,玩家更关心当前的状态,而不是过去的状态。例如,如果玩家的一个动作没有成功传输,那么可能只需要再次发送当前状态,而不需要重新发送丢失的动作。
    3. 可控的错误恢复:游戏开发者可以根据游戏的特点自定义错误恢复策略。例如,如果某个数据包没有到达,可能选择重新发送,或者直接跳过。

    然而,这并不意味着游戏只使用UDP。很多游戏会同时使用TCP和UDP。例如,玩家的登录、账号管理、排行榜等非实时功能可能会使用TCP,因为这些操作需要可靠的数据传输;而实时的游戏状态更新则使用UDP。

    总之,选择使用UDP或TCP取决于应用的需求。在需要实时性和低延迟的场景中,UDP往往是更好的选择。

    37 如何保证TCP传输有序,我说seq和ack保证一般是序列号12345这种,然后人家问那13579这种行不行

    TCP使用序列号(SEQ)和确认号(ACK)来确保数据的有序性。每个发送的数据包都有一个序列号,接收方会发送一个ACK确认已接收到该序列号的数据包。

    关于使用13579这样的序列号,实际上TCP的序列号只是一个数字,它不一定是连续的。关键在于,无论使用哪种序列号,接收方都能够根据这些序列号将数据重新组合成正确的顺序。所以,从理论上讲,使用13579这样的序列号也是可以的,但在实际实现中,通常使用连续的序列号,因为这样更简单、直观且容易实现。

    38 读多写少的情况下,怎么设计hashmap。我说了个加读写锁。问无锁的方案?(滴滴2024秋招一面)

    38.1 读写锁方案

    38.2 COW技术

    在读多写少的场景下,可以使用Copy-On-Write的策略,即每次写操作时,复制一份当前的数据结构进行修改,然后再将引用指向新的数据结构。
    Java中的CopyOnWriteArrayList和CopyOnWriteArraySet就是这种策略的实现。
    这种策略适用于读操作远远多于写操作的场景,因为写操作的代价较高。

    38.3 仿redis的主写从读方案,恒久放置两个map,主节点负责写,从map负责读,当主map写入后会定期更新道从map

    这三种方案各有其优缺点,适用于不同的场景。下面对这三种方案进行评估:

    38.1 方案一:读写锁方案

    • 优点
      • 并发控制:读写锁可以允许多个读操作同时进行,而写操作会独占锁。这在读多写少的场景下能够提供良好的性能。
      • 实时性:读操作可以立即看到写操作的结果,保证了数据的实时性。
    • 缺点
      • 写操作阻塞:当有写操作时,所有的读操作都需要等待,这可能会导致性能瓶颈。
      • 复杂性:需要仔细管理锁的获取和释放,否则可能会导致死锁。

    38.2 方案二:COW技术

    在读多写少的场景下,可以使用Copy-On-Write的策略,即每次写操作时,复制一份当前的数据结构进行修改,然后再将引用指向新的数据结构。
    Java中的CopyOnWriteArrayList和CopyOnWriteArraySet就是这种策略的实现。
    这种策略适用于读操作远远多于写操作的场景,因为写操作的代价较高。

    • 优点
      • 高并发读:由于读操作不需要加锁,所以可以实现高并发的读操作。
      • 简单:实现相对简单,不需要复杂的锁管理。
    • 缺点
      • 写操作代价高:每次写操作都需要复制整个数据结构,代价较高,不适合写操作频繁的场景。
      • 内存占用:由于需要复制整个数据结构,所以可能会占用较多的内存。
      • 数据一致性:读操作可能无法立即看到写操作的结果。

    38.3 方案三:仿redis的主写从读方案,恒久放置两个map,主节点负责写,从map负责读,当主map写入后会定期更新道从map

    • 优点
      • 高并发读:读操作只需要从从节点读取数据,可以实现高并发。
      • 写操作不阻塞读操作:主节点的写操作不会阻塞从节点的读操作。
    • 缺点
      • 数据一致性:从节点的数据可能会滞后于主节点,读操作可能无法立即看到最新的数据。
      • 复杂性:需要维护主从节点的数据同步,增加了系统的复杂性。
      • 资源占用:需要额外的资源来维护从节点。

    总的来说,选择哪种方案取决于具体的应用场景和需求。如果对数据实时性要求较高,并且可以接受写操作的阻塞,那么读写锁方案可能更合适。如果读操作远远多于写操作,并且可以接受写操作的高代价和数据的短暂不一致,那么COW技术可能更合适。如果需要高并发读,并且可以接受数据的短暂不一致和系统的额外复杂性,那么主写从读方案可能更合适。

    39 N个有序数组求交集并集,数组长度很长,不能全部加载到内存里(滴滴2024秋招一面)

    39.1 N个有序数组求交集并集,数组长度很长,不能全部加载到内存里。取并集策略:我的想法是,在内存中使用bitMap作为集合,然后每次从几个数组中截取一部分前缀到内存中,进行数据的合并,最终即使是1亿条数据,也不会占用很大的空间。

    你提到使用Bitmap来存储并集,这是一个内存效率很高的方法,特别是当数据范围较大,但实际存储的数据稀疏时。但是,Bitmap的一个缺点是它不能很好地处理非整数的数据,或者数据范围非常大的情况。

    对于你提到的“每次从几个数组中截取一部分前缀到内存中,进行数据的合并”这个策略,实际上是一种外排序的变种。你可以使用归并排序的思路,每次从所有数组中各取出一部分数据加载到内存中,然后进行归并操作,将结果存储到结果集中。这样即使单个数组无法全部加载到内存中,也能够得到最终的并集。

    39.2 取交集策略:先使用一个bitMap求取并集,然后使用N个bitMap分别存储这N个有序数组,再遍历并集bitMap,如都在这N个bitMap中则加入交集中

    你提到的使用Bitmap来分别存储每个数组,然后遍历并集Bitmap来求交集的方法是可行的。但是这种方法的空间复杂度较高,特别是当N很大时,你需要存储N个Bitmap,这可能会占用大量的内存。

    一个更加节省内存的方法是,直接在归并求并集的过程中,就同时计算交集。具体来说,你可以维护一个计数器,记录当前元素在多少个数组中出现过。每次从所有数组中取出的数据进行归并时,如果某个元素在所有数组中都出现过,那么就将其加入到交集中。这样就不需要额外的空间来存储Bitmap,也能够得到最终的交集。

    总的来说,你的策略是一个很好的起点,但是在实际操作中可能需要根据数据的特点和内存的限制进行一些调整和优化。

  • 相关阅读:
    2022安全与软工顶会中区块链智能合约相关论文
    Git 常用指令
    Day06--上拉触底案例-初步实现上拉触底效果
    盲盒一番赏小程序:打开未知的惊喜之旅
    芯片设计后端遇到的各种文件类型和文件后缀
    【Pandas数据分析5】数据清洗
    美团动态线程池实践思路,开源了
    Spring中Bean的作用域和生命周期
    基于SSM的个人健康饮食管理小程序系统源码【包调试】
    线程池实现
  • 原文地址:https://blog.csdn.net/yxg520s/article/details/132673963