likes
comments
collection
share

查漏补缺第十三期(滴滴实习一面)

作者站长头像
站长
· 阅读数 10

前言

目前正在出一个查漏补缺专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~

本专题主要以Java语言为主, 好了, 废话不多说直接开整吧~

项目里为什么要用消息队列

  1. 异步处理:消息队列允许将任务异步处理。当一个请求到达后端服务时,服务可以将相关任务放入消息队列中,然后立即返回响应给客户端,而不需要等待任务完成。这种异步处理方式可以提高系统的响应速度和吞吐量。通过将任务放入消息队列中,后端服务可以立即释放资源,并在适当的时候从队列中获取任务并处理。

  2. 解耦系统:消息队列可以用于解耦后端系统中的各个组件。组件之间可以通过消息队列进行通信,而不需要直接依赖彼此。这种解耦可以使系统更加灵活和可扩展。例如,当一个组件需要更新或者发生故障时,其他组件可以继续发送消息到队列中,而无需等待该组件的恢复或修复。

  3. 削峰填谷:消息队列可以用于平衡系统的负载。当请求量突然增加时,消息队列可以作为一个缓冲区,暂时存储请求,然后按照系统处理能力的速度逐个处理。这种方式可以防止系统超载,并且确保任务不会丢失。类似地,当请求量减少时,消息队列可以继续提供任务,以充分利用系统的资源,避免资源的闲置浪费。

  4. 可靠性和持久性:消息队列通常具有高度可靠性和持久性。消息队列会将消息持久化存储,以确保在系统故障或重启之后,消息不会丢失。此外,消息队列通常具有复杂的确认和重试机制,可以保证消息在处理过程中的可靠传递,即使在网络故障或处理错误的情况下也能恢复。

  5. 任务调度:消息队列可以用于任务调度和分发。后端系统可以将不同类型的任务放入不同的队列中,并且根据系统的负载、优先级和其他因素,按照预定义的逻辑来选择任务并进行分发。这种任务调度的方式可以提高系统的灵活性和性能。

消息队列在项目中具有很多优势,包括异步处理、解耦系统、削峰填谷、可靠性和持久性以及任务调度。这些优势可以改善系统的性能、可靠性和扩展性,使服务更加高效和稳定。

在请求很多的情况下,消息堆积处理不过来了,如何应对

当消息堆积处理不过来时,可以采取以下几种方式来应对:

  1. 增加消费者:增加消息队列消费者的数量,以提高消息的处理速度。通过增加消费者,可以并行地处理消息,从而加快整体处理速度。这需要确保后端系统有足够的资源来支持额外的消费者。

  2. 水平扩展:如果增加消费者的数量无法满足需求,可以考虑水平扩展后端系统。水平扩展是指将系统分为多个实例,每个实例独立处理消息。这样可以通过负载均衡将消息分发给不同的实例,从而提高整体处理能力。

  3. 优化处理逻辑:检查处理消息的逻辑是否存在性能瓶颈或低效操作。优化处理逻辑可以减少单个消息的处理时间,从而增加整体处理能力。可能的优化包括使用更高效的算法、减少数据库查询次数、缓存数据等。

  4. 增加消息队列容量:增加消息队列的容量可以暂时缓解消息堆积的问题。通过增加队列的容量,可以存储更多的消息,以便后续处理。然而,这只是一种临时的解决方案,需要确保系统能够在消息队列容量恢复正常之前及时处理积压的消息。

  5. 限流策略:在消息堆积处理不过来时,可以采取限流策略来控制请求的流量。限流可以通过拒绝新的请求、返回错误码或者延迟处理请求的方式来实现。通过限制请求的数量,可以保护后端系统免受过载的影响,确保系统的稳定性。

  6. 监控和报警:建立监控系统,实时监测消息队列的状态和处理速度。当消息堆积达到一定阈值或者处理速度下降时,及时发送报警通知,以便及早发现和解决问题。

如何解决超卖问题

商品超卖是指在高并发环境下,由于竞争条件和操作顺序不当,导致多个用户同时购买同一件商品,最终导致库存数量减少超过实际可售数量的情况。为了解决商品超卖问题,可以采取以下几种方法:

  1. 悲观锁:使用数据库的悲观锁机制,例如使用数据库的行级锁或表级锁。在商品购买过程中,使用锁来保护库存的更新操作,确保同一时间只有一个请求能够修改库存数量。悲观锁会对并发性能产生一定的影响,因此需要谨慎使用。

  2. 乐观锁:使用数据库的乐观锁机制,例如使用版本号或时间戳字段。每次更新库存时,先读取当前库存的版本号,然后在更新库存时比较版本号是否匹配。如果版本号匹配,则执行更新操作;否则,表示库存已被其他请求修改,需要进行回滚或重新尝试。

  3. 唯一索引和事务:在数据库中为商品的唯一标识(如商品ID)设置唯一索引,确保同一商品不能被重复购买。同时,使用数据库事务来保证购买操作的原子性和一致性。在事务中,先检查库存是否充足,然后进行扣减库存和生成订单等操作,最后提交事务。如果扣减库存失败或者生成订单失败,需要进行回滚操作。

  4. 队列和消息中间件:使用消息队列和消息中间件来进行异步处理。当用户下单时,先将购买请求发送到消息队列中,然后通过消费者进行处理。消费者从队列中取出消息,依次处理购买请求,并对库存进行扣减操作。这样可以保证购买请求的顺序性,避免并发冲突。

  5. 分布式锁:在分布式环境中,可以使用分布式锁来解决商品超卖问题。分布式锁可以确保在分布式系统中只有一个请求能够对库存进行修改。常见的分布式锁实现包括基于数据库的锁、基于Redis的锁和基于ZooKeeper的锁等。

需要根据具体的业务场景和系统架构选择合适的解决方案。在实施解决方案时,需要注意并发性能、数据一致性和系统可用性等因素,并进行充分的测试和评估。

在大促活动中,秒杀场景下扣减库存太慢了怎么办

在大促活动的秒杀场景下,由于高并发请求的压力,库存扣减操作可能成为性能瓶颈。为了解决库存扣减太慢的问题,可以采取以下几种方法:

  1. 异步扣减库存:将库存扣减操作设计为异步执行,即不需要等待扣减操作完成就立即返回响应给用户。可以通过消息队列或异步任务进行处理,将扣减库存的请求发送到消息队列中,由后台的消费者异步处理库存扣减操作。这样可以大大提高系统的并发处理能力。

  2. 分布式缓存:使用分布式缓存(如Redis)存储商品库存信息,并通过缓存来快速判断库存是否充足。在秒杀活动开始前,将商品的库存信息加载到缓存中,并在秒杀请求到达时,先从缓存中读取库存信息进行判断。这样可以减少对数据库的访问次数,提高库存检查的速度。

  3. 内存预减库存:在秒杀活动开始前,可以预先将商品的库存信息加载到内存中。当秒杀请求到达时,先在内存中进行库存检查和扣减操作。如果库存不足,可以快速返回秒杀失败的结果,避免对数据库的频繁访问。然后再将扣减后的库存更新到数据库中,保持数据的一致性。

  4. 分库分表:将商品库存信息进行分库分表存储,使库存的读写操作能够在多个数据库实例上并行处理。通过水平拆分数据库和数据表,可以提高并发处理能力,分摊扣减库存的压力。

  5. 限流和熔断机制:对秒杀请求进行限流,限制同时处理的请求数量,防止系统过载。可以使用令牌桶算法、漏桶算法等来控制请求的流量。同时,可以引入熔断机制,当系统负载过高时,暂时停止接受新的秒杀请求,直到系统恢复正常。

  6. 优化数据库操作:对库存扣减的数据库操作进行优化,例如优化SQL查询语句、建立合适的索引、调整事务隔离级别等。确保数据库操作的效率和性能,减少数据库锁竞争和查询延迟。

Redis大key如何解决

Redis 中出现大key(指存储的值过大)时,可能会对Redis的性能和内存占用造成负面影响。为了解决大key问题,可以考虑以下几种方法:

  1. 分割大key:将大key分割成多个较小的key-value对。例如,如果一个大key存储了一个较大的对象,可以将对象的各个属性拆分成多个key,将其存储为多个小的key-value对。这样可以避免单个key过大,降低内存占用和提高性能。

  2. 使用哈希结构:对于包含多个字段的大key,可以使用 Redis 的哈希结构(hash)进行存储。将大key拆分成多个字段,每个字段作为哈希结构的一个域(field),并将它们关联到同一个key上。这样可以将大key的数据分散存储,减少单个key的大小,提高读写性能。

  3. 压缩数据:如果大key存储的值是可压缩的(如文本、序列化的对象等),可以在存储到Redis之前对数据进行压缩。使用压缩算法(如Gzip、Snappy等)对数据进行压缩,减少存储空间的占用,同时在读取时进行解压缩操作。

  4. 数据分片:如果大key无法分割或压缩,可以考虑将数据分散到多个Redis实例上。通过使用分片技术(如一致性哈希)将大key的数据分散存储到多个 Redis 实例中,可以减轻单个实例的负载压力,并提高整体的性能和可扩展性。

  5. 使用专用存储引擎:如果大key的数据量非常大且不适合存储在Redis中,可以考虑使用专门的存储引擎,如分布式文件系统(如Hadoop、HDFS)、对象存储(如Amazon S3)或分布式数据库(如Cassandra、MongoDB)等。将大key的数据存储在适合的存储引擎中,通过在 Redis 中存储引擎的引用或标识符,进行数据的关联和访问。

Redis中什么是热key

Redis 中,热key指的是被频繁访问的键(key),即被大量读取或写入操作的键。这些键通常代表着应用程序中的热点数据,其访问频率远高于其他键。

当一个键被频繁访问时,可能会导致以下问题:

  1. 高并发竞争:多个客户端同时访问同一个热key时,可能会引发高并发竞争。竞争过程中可能会产生锁竞争、阻塞和等待,从而降低系统的响应速度和并发能力。

  2. 频繁持久化:当热key被频繁写入时,可能会导致 Redis 的持久化操作变得繁重。如果启用了持久化机制(如RDB快照或AOF日志),频繁写入热key可能会增加持久化操作的负担,导致性能下降和持久化过程的延迟。

  3. 内存消耗:热key通常会被频繁地存储在 Redis 的内存中,这可能导致内存资源的消耗。如果热key的数据量较大,可能会影响 Redis 的整体内存使用情况,并可能导致内存溢出或交换(swapping)的问题。

针对热key问题,可以采取以下一些解决方案:

  1. 缓存策略优化:根据实际需求,调整缓存的过期策略,避免热key长时间存在于缓存中。可以设置合理的过期时间,或者使用带有自动过期的数据结构(如Redis的Sorted Set)来处理热key。

  2. 数据分片:通过使用分片技术将热key的数据分散存储到多个 Redis 实例中,可以减轻单个实例的负载压力,并提高整体的性能和可扩展性。

  3. 缓存预热:在应用程序启动或重启之前,可以提前加载热key的数据到 Redis 缓存中,避免请求落到空缓存上,减少缓存穿透的风险。

  4. 限流和熔断机制:对于访问热key的请求进行限流,限制同时处理的请求数量,防止系统过载。同时,可以引入熔断机制,当系统负载过高时,暂时停止对热key的访问,直到系统恢复正常。

  5. 水平扩展和负载均衡:

通过水平扩展 Redis 集群,将热key的负载均衡在多个实例之间,以提高系统的性能和可靠性。

分配给进程的资源有哪些

进程在操作系统中被分配一定的资源,这些资源可以包括以下几个方面:

  1. 内存:操作系统为每个进程分配一定的内存空间,用于存储程序代码、数据和运行时的栈空间。内存资源包括代码段、数据段、堆和栈等。

  2. CPU 时间片:操作系统通过时间片轮转算法或其他调度算法,将CPU时间划分成小的时间片,并分配给各个进程。进程在获得CPU时间片后可以执行其指令。

  3. 文件描述符:文件描述符是操作系统用于标识打开文件或其他I/O设备的数字标识符。进程可以通过文件描述符进行文件读写、网络通信等操作。

  4. 网络端口:进程可以通过操作系统分配的网络端口与其他进程进行网络通信。操作系统分配的端口号用于标识进程的网络连接。

  5. 设备和外部资源:操作系统还可以为进程分配访问设备(如打印机、磁盘驱动器等)和其他外部资源的权限。进程可以通过操作系统提供的接口进行设备访问和资源操作。

  6. 进程控制块(PCB):PCB是操作系统为每个进程维护的数据结构,包含进程的状态、标识符、上下文信息、进程优先级等。PCB用于操作系统管理和调度进程。

  7. 用户和组权限:操作系统为进程分配一定的用户和组权限,用于限制进程对系统资源的访问和操作。这包括文件权限、进程间通信权限等。

这些资源的分配和管理是操作系统的职责,它负责协调进程之间的资源竞争和调度,以实现多任务的并发执行,不同的操作系统可能会有一些差异。

进程切换和线程切换的区别

进程切换和线程切换是操作系统中两个不同概念的切换过程。

进程切换是指从一个进程的上下文切换到另一个进程的上下文。在进程切换中,操作系统需要保存当前进程的执行状态(如程序计数器、寄存器内容、堆栈指针等),并恢复下一个进程的执行状态。进程切换通常需要较多的开销,因为需要切换整个进程的上下文,包括内存映射、文件描述符等,以及可能需要进行页表切换和缓存刷新等操作。

线程切换是指从一个线程的上下文切换到另一个线程的上下文。在线程切换中,操作系统只需保存和恢复线程的执行状态,如程序计数器、寄存器内容和堆栈指针等,而无需切换整个进程的上下文。线程切换的开销通常较小,因为线程共享进程的资源,如内存空间和文件描述符,不需要额外的内存映射和页表切换。

以下是进程切换和线程切换的一些主要区别:

  1. 资源消耗:进程切换需要切换整个进程的上下文,包括内存映射、文件描述符等,因此开销相对较大。而线程切换只需保存和恢复线程的执行状态,开销较小。

  2. 切换速度:由于进程切换涉及到更多的上下文切换和资源刷新操作,所以相对于线程切换来说速度较慢。

  3. 并发性:进程是独立的执行实体,具有独立的内存空间和资源,因此进程之间的并发性较高。而线程是进程内的执行单位,共享进程的资源,因此线程之间的并发性更高,线程切换的开销也较小。

  4. 数据共享和通信:进程之间的数据共享和通信通常需要借助于操作系统提供的机制,如管道、共享内存、消息队列等。而线程之间可以直接共享进程的内存空间,因此数据共享和通信更加方便和高效。

需要根据具体的应用场景和需求来选择使用进程还是线程,以及合适的线程模型和同步机制。进程和线程的选择会影响到系统的性能、资源利用和并发能力。

为什么并发执行线程要加锁

在并发执行的多个线程中,如果涉及到对共享数据的读写操作,就可能引发并发访问的竞态条件(Race Condition)。竞态条件会导致数据的不一致性、错误的结果或者程序的崩溃。

加锁是一种常用的解决并发访问问题的方法,通过加锁可以实现对共享资源的互斥访问,保证同一时间只有一个线程能够对资源进行操作,其他线程需要等待锁的释放。加锁可以提供以下几方面的好处:

  1. 保证数据一致性:通过加锁,可以确保在同一时间只有一个线程对共享资源进行修改,避免了多个线程同时读写导致的数据不一致问题。

  2. 防止竞态条件:通过锁机制,可以防止多个线程同时进行写操作,从而避免竞态条件的发生。

  3. 保护共享资源:通过加锁,可以保护共享资源不受并发访问的影响,确保数据的完整性和正确性。

  4. 实现互斥访问:加锁可以使得同一时间只有一个线程能够进入临界区(被保护的代码块),其他线程需要等待锁的释放。这样可以避免多个线程同时访问临界区,避免数据竞争和错误的结果。

需要注意的是,加锁虽然可以解决并发访问的问题,但过度使用锁也可能导致性能下降。锁的获取和释放会引入额外的开销,并且如果锁的粒度过大,可能会导致线程间的等待时间增加。因此,在设计并发程序时,需要根据实际情况合理使用锁,选择适当的锁粒度和锁的类型,以平衡数据一致性和性能的需求。

TCP和UDP的区别

TCP(Transmission Control Protocol)UDP(User Datagram Protocol)是两种常见的传输层协议,用于在计算机网络中传输数据。它们在以下几个方面有着明显的区别:

  1. 连接性:

    • TCP是面向连接的协议,使用三次握手建立连接,确保可靠的数据传输。数据在发送前需要建立连接,然后按照顺序传输,并提供重传和流量控制等机制。
    • UDP是无连接的协议,不需要建立连接,数据包以尽力交付的方式发送。每个数据包是独立的实体,互不依赖,不提供可靠性保证。
  2. 可靠性:

    • TCP提供可靠的数据传输,通过确认应答和重传机制来保证数据的完整性和可靠性。如果数据包丢失或损坏,TCP会重新传输丢失的数据。
    • UDP不提供可靠性保证,发送的数据包可能丢失、重复或者乱序。应用程序需要自行处理数据的完整性和顺序性。
  3. 传输效率:

    • 由于TCP提供的可靠性保证和流量控制机制,它的传输效率相对较低,需要额外的开销来维护连接状态和确认机制。
    • UDP没有拥塞控制和流量控制的开销,因此传输效率较高。它适用于实时性要求较高的应用,如音频和视频流媒体。
  4. 数据包大小:

    • TCP对数据包的大小没有严格限制,可以处理任意大小的数据。较大的数据会被分割为多个小的TCP段进行传输。
    • UDP对数据包的大小有限制,每个数据包的大小限制在64KB内。如果超过限制,需要分割为多个UDP数据包进行传输。
  5. 应用场景:

    • TCP适用于要求数据可靠传输、顺序性和拥有较小延迟的应用,如文件传输、Web浏览、电子邮件等。
    • UDP适用于实时性要求较高、延迟敏感的应用,如实时语音、视频通话、在线游戏等。

TCP的连接指的是什么

在TCP协议中,连接指的是通过建立TCP连接来实现通信的过程。TCP连接是一种虚拟的、逻辑上的连接,用于在网络中两个应用程序之间进行可靠的数据传输。

TCP连接的建立是通过三次握手(Three-Way Handshake)来完成的,具体过程如下:

  1. 第一步:客户端发送一个带有SYN(同步)标志的连接请求报文(SYN包)给服务器端。
  2. 第二步:服务器端接收到客户端的连接请求后,会发送一个带有SYN/ACK(同步/确认)标志的应答报文(SYN/ACK包)给客户端。
  3. 第三步:客户端收到服务器端的应答后,发送一个带有ACK(确认)标志的报文(ACK包)给服务器端。此时,TCP连接建立成功,可以开始进行数据的传输。

在连接建立之后,TCP连接双方可以进行数据的传输。TCP协议通过序列号和确认应答机制来保证数据的可靠性和顺序性。发送方将数据分割成小的数据段,并逐个发送给接收方,接收方通过确认应答机制告知发送方已接收到数据,并进行按序重组。

当通信结束时,双方可以通过发送一个特殊的TCP包来关闭连接,即四次挥手(Four-Way Handshake)的过程。

通过建立TCP连接,应用程序可以在网络中建立可靠的、面向连接的通信链路,确保数据的完整性和可靠性。TCP连接是一种全双工的通信方式,双方可以同时进行数据的发送和接收。

TCP为什么要三次握手

TCP使用三次握手的过程是为了建立一个可靠的、双向的通信连接。三次握手的目的在于确保双方都同意建立连接,并交换一些必要的信息以初始化连接的参数。

下面是三次握手的过程:

  1. 第一次握手(SYN): 客户端发送一个带有SYN(同步)标志的连接请求报文(SYN包)给服务器端。这个报文中会包含一个随机生成的初始序列号(ISN)作为起始值。

  2. 第二次握手(SYN + ACK): 服务器端接收到客户端的连接请求后,会发送一个带有SYN/ACK(同步/确认)标志的应答报文(SYN/ACK包)给客户端。这个报文中会包含确认号(ACK)字段,确认客户端的ISN,并同时也会包含服务器端的初始序列号。

  3. 第三次握手(ACK): 客户端收到服务器端的应答后,会发送一个带有ACK(确认)标志的报文(ACK包)给服务器端。这个报文中会将服务器端的初始序列号进行确认。

为什么要进行三次握手?

  1. 双方确认连接请求的意愿: 通过三次握手,客户端和服务器端都能确认对方有意愿建立连接。第一次握手客户端请求连接,第二次握手服务器端确认请求并同意建立连接,第三次握手客户端再次确认服务器端的意愿。这样可以避免无效的连接请求。

  2. 确定初始序列号(ISN): 在三次握手过程中,服务器端和客户端都会交换初始序列号,用于后续的数据传输和确认。初始序列号的选择很重要,它需要足够随机和唯一,以防止重复或恶意的连接建立。

  3. 防止已失效的连接请求产生的问题: 如果只有两次握手,假设客户端发送的连接请求在网络中延迟,而服务器端没有收到该请求,那么服务器端在一段时间后会重新发送连接请求应答。但客户端此时可能已经关闭了,服务器端的请求应答会导致新的连接被建立,从而产生问题。通过三次握手,可以确保服务器端在收到客户端的确认后,才会建立新的连接。

通过三次握手,TCP连接的双方能够确认彼此的意愿、初始化连接参数,并防止已失效的连接请求产生问题,从而建立一个可靠的双向通信连接。

三次握手会产生什么问题

三次握手会产生额外的资源消耗,主要包括以下几个方面:

  1. 建立连接的延迟: 由于需要进行三次握手的过程,建立TCP连接的时间会比较长。每次握手都需要经过网络传输和处理的时间,而且如果有丢包或延迟,可能会导致握手过程的重传。这会增加连接建立的延迟。

  2. 占用网络带宽: 三次握手过程中需要发送和接收多个数据包,这会占用一定的网络带宽。在高并发的情况下,大量的连接请求会导致网络拥塞,影响其他连接的建立和数据传输。

  3. 占用系统资源: 在服务器端,每个新的连接请求都需要创建一个新的TCP连接对象,并分配一些系统资源给该连接。这些资源包括端口号、内存空间、连接状态等。大量的连接请求会消耗服务器的系统资源。

字符串相加 (力扣415题)

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

输入:num1 = "11", num2 = "123"
输出:"134"
输入:num1 = "456", num2 = "77"
输出:"533"
输入:num1 = "0", num2 = "0"
输出:"0"
public class AddString {
    public static String addStrings(String num1, String num2) {
        int i = num1.length() - 1;
        int j = num2.length() - 1;
        int carry = 0;
        StringBuilder result = new StringBuilder();

        while (i >= 0 || j >= 0) {
            int digit1 = i >= 0 ? num1.charAt(i) - '0' : 0;
            int digit2 = j >= 0 ? num2.charAt(j) - '0' : 0;

            int sum = digit1 + digit2 + carry;
            result.insert(0, sum % 10);
            carry = sum / 10;

            i--;
            j--;
        }

        if (carry != 0) {
            result.insert(0, carry);
        }

        return result.toString();
    }

    public static void main(String[] args) {
        System.out.println(addStrings("11", "123")); // 134
        System.out.println(addStrings("456", "77")); // 533
        System.out.println(addStrings("0", "0")); // 0
    }

}

时间复杂度O(max(len1, len2)), len1num1.length, num2num2.length

空间复杂度: O(n)

结束语

大家可以针对自己薄弱的地方进行复习, 然后多总结,形成自己的理解,不要去背~

本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注鼓励一下呗~

相关文章

项目源码(源码已更新 欢迎star⭐️)

往期设计模式相关文章

设计模式项目源码(源码已更新 欢迎star⭐️)

Kafka 专题学习

项目源码(源码已更新 欢迎star⭐️)

ElasticSearch 专题学习

项目源码(源码已更新 欢迎star⭐️)

往期并发编程内容推荐

推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)

博客(阅读体验较佳)