美团后端一二面
一面
1、redis基本数据类型,应用场景?
- String(字符串):用于存储文本、数值等数据。常用于缓存、计数器、会话管理等。
- Hash(哈希表):适合存储对象(如用户数据、商品信息)的多个字段和值。可以快速获取或更新字段的值。
- List(列表):用于存储有序的元素列表。适合实现消息队列、栈、发布订阅等功能。
- Set(集合):用于存储不重复的元素。适合存储标签、好友列表等,也可用于计算交集、并集等操作。
- Sorted Set(有序集合):类似Set,但每个元素都有一个分数,可以用分数来排序元素。适合实现排行榜、优先级队列等。
- Bitmap(位图):适合存储布尔值,可用于跟踪用户在线状态、用户活跃度等。可以执行位运算来统计和查询状态。
- HyperLogLog:用于估算集合中不重复元素的数量,适合用于统计UV(独立访客数)等场景。
- Geospatial(地理空间):用于存储地理位置信息,支持距离计算和附近位置的查询。适合实现地图应用、位置服务等。
- Pub/Sub(发布订阅):用于实现消息发布和订阅机制,适合构建实时通知、事件驱动系统等。
2、redis实现共同关注?
要实现共同关注功能(类似社交网络中的好友关系),可以使用Redis的数据结构来存储关注关系,也就是题目问的。
给个例子:
假设有两个用户,用户A和用户B,他们都可以关注其他用户,我们想找出他们共同关注的用户。
使用集合(Set)来存储用户的关注列表:
- 用户A的关注列表:
sadd userA_following userC userD userE - 用户B的关注列表:
sadd userB_following userD userE userF
- 用户A的关注列表:
找出用户A和用户B的共同关注:
使用集合的交集操作(
sinter)来找出两个用户的共同关注:- C++
sinter userA_following userB_following
将返回一个集合,包含用户A和用户B共同关注的用户(在此示例中是userD和userE)。
- 可以使用类似的方法来查找其他用户的共同关注,或者执行其他操作,如取消关注、添加新的关注等。
3、redis持久化,AOF,RDB,会丢数据吗?
- AOF(Append-Only File)持久化:
- AOF持久化以追加的方式记录每个写操作(包括SET、INCR等)到一个日志文件中,该文件包含了恢复数据所需的所有写操作。
- AOF持久化可以配置为每秒同步一次(默认配置),或者根据需要更频繁地同步。
- 由于AOF记录了每个写操作,通常情况下不会丢失数据,即使Redis宕机,也可以通过AOF文件来完全恢复数据。
- RDB(Redis DataBase)持久化:
- RDB持久化是通过周期性快照(快照)整个数据集到磁盘的方式。
- RDB文件包含了在某个时间点上数据集的快照,因此在每次快照之间的数据更改可能会丢失。
- 默认情况下,Redis会每隔一段时间(可以配置)执行一个RDB快照,如果Redis在两次快照之间崩溃,那么这两次快照之间的数据将丢失。
总的来说:
- AOF持久化通常不会丢失数据,因为它记录了每个写操作,但可能会有一定的数据恢复成本。
- RDB持久化在快照间可能会丢失数据,但因为RDB文件只包含快照时的数据,所以通常比AOF更快速。
4、redis穿透击穿雪崩,解决?
- 缓存穿透:
- Bloom Filter:使用布隆过滤器来过滤掉不存在于缓存中的请求,减轻对数据库的请求压力。
- 空值缓存:对于数据库中不存在的数据,仍将其缓存,但设置一个较短的过期时间,防止频繁查询。
- 缓存空对象:当查询数据库返回空结果时,将这个结果也缓存,但设置较短的过期时间,防止多次重复查询。
- 缓存击穿:
- 互斥锁:使用互斥锁来保护缓存,当缓存失效时,只允许一个请求查询数据库,其他请求等待结果,避免多个请求同时击穿缓存。
- 热点数据预热:定期或在启动时预加载热门数据到缓存中,以避免因突然大量请求而导致缓存击穿。
- 缓存雪崩:
- 缓存失效时间随机性:为缓存的失效时间增加一定的随机性,避免所有缓存同时失效,减轻对数据库的并发请求。
- 持久化缓存:使用AOF和RDB的持久化机制来保障缓存数据的可靠性,即使缓存雪崩,也可以从持久化数据中恢复。
- 多级缓存:使用多级缓存(例如,本地内存缓存、分布式缓存、CDN等)来分担缓存压力,即使一个缓存层发生雪崩,其他层也能提供服务。
- 缓存更新策略:
- 异步刷新:在缓存失效后,后台异步更新缓存,避免请求等待缓存更新。
- 加锁更新:在缓存失效时,只允许一个请求查询数据库并更新缓存,其他请求等待结果。
- 合理的缓存策略:
- 根据数据的访问模式和业务需求,选择合适的缓存策略,如LRU(最近最少使用)、LFU(最不常使用)、TTL(Time To Live)等。
5、redis大Key,怎么处理?
Redis中的大Key通常指的是一个缓存键对应的值非常大,可能是大型数据结构、大文本、或二进制数据等。存储大Key会导致内存占用过高,影响性能。给大家一些处理Redis大Key的参考方法:
拆分数据:
- 如果可能,将大型数据拆分成多个小型数据,每个小数据存储在独立的键中。这样可以降低单个键的大小,减小内存占用。
- 例如,如果一个键存储了大型JSON对象,可以拆分成多个子键,每个子键存储JSON对象的一部分数据。
使用数据压缩:
对于文本或二进制数据,可以在存储之前进行压缩,然后在读取时解压缩。Redis并不直接支持数据压缩,但你可以在应用层进行压缩和解压缩操作。
持久化到磁盘:
对于大型数据,可以选择将部分数据持久化到磁盘而不是全部保存在内存中。Redis支持RDB快照和AOF持久化,可以配置合适的策略。
使用数据分片:
如果大Key是由多个小Key组成的集合,可以使用Redis的数据分片或分区技术,将数据分布在多个Redis实例中,每个实例负责一部分数据。这可以减轻单个实例的内存压力。
定期清理:
- 如果大Key的生命周期有限,可以定期清理不再需要的数据,以释放内存。
- 使用
DEL命令删除不再需要的大Key。
使用内存优化的数据结构:
对于大型集合或列表,可以考虑使用Redis的内存优化数据结构,如HyperLogLog、Redis Streams等,以减小内存占用。
数据预热和缓存策略:
如果大Key是在系统启动时加载的,可以考虑实现数据预热策略,将热门数据提前加载到缓存中,以减轻启动时的内存压力。
6、redis分布式锁,Zookeeper分布式锁,怎么选择?
给大家一些选择是需要考虑的因素,在什么情况用哪种分布式锁是更优选择。
性能:
- Redis通常比ZooKeeper更快,因为它是基于内存的存储系统,而ZooKeeper是基于磁盘的。
- 如果你需要高性能的分布式锁,Redis通常是更好的选择。
一致性:
- ZooKeeper提供了强一致性,这意味着它可以用于更严格的分布式应用,如协调、选举等。
- Redis提供的分布式锁可能在某些情况下出现失效或死锁,因为它是基于主从复制的。
部署复杂性:
- ZooKeeper需要独立的ZooKeeper集群,需要维护和管理。
- Redis通常更容易部署,特别是如果你已经在应用中使用Redis。
可用性:
- Redis通常更容易扩展和部署,因此可以更容易实现高可用性。
- ZooKeeper需要更复杂的部署和维护,特别是在多数据中心环境中。
应用已有基础:
- 如果你已经在应用中使用了Redis,添加Redis分布式锁可能更容易集成。
- 如果你的应用已经使用ZooKeeper,那么选择ZooKeeper分布式锁可能更合适。
数据持久性需求:
- 如果你需要更强的数据持久性和一致性,可以考虑使用ZooKeeper。
- Redis可以提供持久性,但它通常配置为在性能和可用性之间做出权衡。
社区支持:
Redis和ZooKeeper都有活跃的社区支持,但你可能需要根据你的具体需求来评估社区支持和文档资源。
综合考虑以上因素,如果你的应用需要高性能的分布式锁并且可以容忍某些一致性上的弱点,那么Redis分布式锁可能是一个不错的选择。但如果你需要强一致性和更复杂的分布式协同操作,ZooKeeper分布式锁可能更合适。
7、熔断与限流?
熔断和限流是在分布式系统中用于提高可用性和稳定性的两种重要技术。
- 熔断:
熔断是一种用于避免系统雪崩效应的机制。当一个服务或组件的错误率超过一定阈值时,熔断器打开,阻止请求进一步访问该服务,而不是继续发起请求并导致系统的更多部分崩溃。一旦熔断器打开,它会定期检查服务的可用性,如果服务恢复正常,则关闭熔断器,允许请求再次访问。
熔断的好处包括:
- 防止错误的服务影响整个系统。
- 减少不必要的请求到故障服务,减轻了服务的负载。
- 帮助系统恢复正常运行,而不是持续失败。
- 限流:
限流是一种控制服务请求速率的机制,确保不会过多的请求同时涌入系统。限流可以根据应用需求以不同的方式实现,例如:
- 固定速率限流:每秒最多允许N个请求。
- 漏桶算法:请求以固定速率进入“桶”,当“桶”满了时,多余的请求将被丢弃。
- 令牌桶算法:每个请求需要获取一个令牌,令牌以固定速率生成,如果没有令牌则请求被拒绝。
限流的好处包括:
- 防止突发的大量请求导致系统过载。
- 控制系统的负载,保护后端服务免受请求的过多压力。
- 提供一种方式来保护资源,如API,免受滥用和DDoS攻击。
8、为什么kafka多用于消息中转,很少用于实时计算?
- 持久性存储:Kafka是一种持久性消息队列,它将消息持久存储在磁盘上,确保数据不会因消费者无法及时处理而丢失。这使得Kafka非常适合用作消息中转,允许消费者在任何时间点访问消息,而不仅仅是在实时计算中。
- 消息存储和检索:Kafka的主要目的是存储和检索消息,而不是进行复杂的实时计算。它提供了高吞吐量和低延迟的消息存储和检索能力,但并不提供计算框架,因此在实时计算方面相对较弱。
- 数据保留:Kafka支持根据不同的策略保留消息,例如根据时间或存储大小。这使得Kafka适用于长期数据存储和数据历史查询,而不仅仅是实时计算。
- 消息分发和复制:Kafka具有强大的消息分发和复制机制,可以确保消息可靠地传递到多个消费者或订阅者。这使得Kafka成为消息分发的理想选择,但它并不提供实时计算所需的状态管理和处理。
9、kafka高可用,高吞吐?
高可用:
- 分布式架构:Kafka 是一个分布式系统,它可以部署在多台服务器上,这使得它具备冗余性。如果一台服务器故障,其他服务器可以继续工作,确保消息流的可用性。
- 复制和副本:Kafka 使用分区的方式来组织消息,每个分区都有多个副本。这意味着消息会被复制到多个服务器上,以防止数据丢失。如果一个副本不可用,其他副本仍然可以提供数据。
- ZooKeeper:Kafka 使用 Apache ZooKeeper 来管理和维护集群的元数据和协调任务。ZooKeeper 提供了分布式锁和选举机制,确保 Kafka 集群中的各个组件协同工作。
- 消费者位移:Kafka 记录了消费者在每个分区中的位移,这意味着即使消费者失败,它可以重新从离开的地方开始消费消息,而不会丢失数据。
高吞吐:
- 分区:Kafka 使用分区来水平扩展。每个分区可以由不同的服务器来服务,这样可以平均分配负载,提高吞吐量。
- 批处理和零拷贝:Kafka 使用批处理机制来减少磁盘和网络开销。它还利用零拷贝技术,将数据从生产者发送到消费者,减少了 CPU 和内存开销。
- 压缩:Kafka 支持消息压缩,可以减小数据传输的大小,提高吞吐量。
- 持久性:Kafka 的消息被持久化到磁盘上,这允许数据在多个消费者之间共享,并确保即使消费者不在线,数据也不会丢失。
- 分布式部署:Kafka 可以在多台服务器上部署,充分利用硬件资源,提供高吞吐量。
10、kafka不重不丢?
Kafka 在设计上追求不重不丢,这意味着它努力确保消息不会被重复传递,也不会在传递过程中丢失。为了实现不重不丢的特性,Kafka 使用了以下关键机制:
- 消息复制和副本:Kafka 使用多个副本来保存消息,每个分区的消息都有多个副本分布在不同的服务器上。这样,即使某个服务器故障,仍然可以从其他副本中获取消息。
- 消费者位移:Kafka 记录了每个消费者在每个分区中的位移(offset),表示消费者已经处理到哪个位置的消息。消费者可以定期提交位移,以确保它们不会重复消费消息。
- 生产者确认机制:Kafka 生产者在将消息发送到服务器后,会等待服务器的确认(acknowledgment)。只有当服务器确认接收到消息后,生产者才会认为消息已经成功发送。
- 事务支持:Kafka 提供了事务支持,允许生产者在发送消息时执行事务性操作。这意味着消息要么全部成功发送,要么一个都不发送,以确保不重不丢。
- 幂等性生产者:Kafka 生产者支持幂等性,即使生产者发送相同的消息多次,只有一次会生效,这有助于避免重复消息。
11、kafka消费端幂等性?
Kafka 消费端的幂等性是指消费者能够处理来自 Kafka 主题的消息,而不会导致重复数据或意外副作用。保持幂等性对于确保数据处理的正确性和可靠性至关重要。
给大家提供一些Kafka 消费端幂等性的实现方法和建议:
- 消费者位移管理:Kafka 消费者应正确管理位移,以避免消息的重复处理。消费者应定期提交已经成功处理的消息的位移,以确保它们不会再次消费相同的消息。
- 消息处理的幂等性:消费者的消息处理逻辑应该是幂等的,即无论处理相同的消息一次还是多次,结果应该相同。这可以通过设计消息处理逻辑来确保,例如,通过检查消息的唯一标识符,避免重复插入相同的数据。
- 幂等性标识符:对于某些处理场景,可以在消息中包含幂等性标识符。消费者在处理消息之前检查这个标识符,以确保不会重复处理相同的消息。
- 事务性处理:Kafka 支持事务,消费者可以使用事务性处理来确保消息的幂等性。在处理消息前,消费者可以启动事务,并在成功处理后提交事务。这样可以确保消息只会被处理一次。
- 异常处理:消费者需要正确处理异常情况。如果消息处理失败,消费者应该能够重新处理消息而不引入额外的副作用。这可能涉及到将消息从未处理状态切换到已处理状态的机制。
- 幂等性测试:对于关键的消息处理逻辑,建议编写单元测试来验证其幂等性。这些测试可以模拟重复消息处理,以确保不会引入重复数据。
12、kafka消费者群组?
Kafka 消费者群组是 Kafka 中用于协同消费主题中消息的机制。它允许多个消费者协同工作以从一个或多个主题中消费消息。
- 多个消费者:一个消费者群组可以包括多个消费者。这些消费者可以在不同的应用程序或服务器上运行。
- 主题分区:Kafka 主题通常被划分为多个分区,每个分区包含一部分消息。消费者群组可以同时消费多个分区中的消息。
- 负载均衡:Kafka 自动分配分区给消费者,以实现负载均衡。每个分区通常只分配给一个消费者来避免重复消费。
- 水平扩展:通过增加消费者,可以水平扩展消费者群组以处理更多的消息。
- 消费者位移管理:Kafka 为每个消费者群组中的消费者维护位移(offset),表示它们在每个分区中的消费位置。这确保了消费者能够从上次停止的位置继续消费。
- 消费者协作:在同一个消费者群组中,每个分区只能由一个消费者消费。这有助于避免重复消费。
- 消息处理并行性:每个消费者可以在独立的线程中处理消息,从而提高消息的处理并行性。
- 自动重平衡:如果有新的消费者加入或旧的消费者退出,Kafka 自动触发群组的重平衡,以重新分配分区,确保负载均衡。
- 消费者状态监控:Kafka 提供监控工具来跟踪消费者群组的状态,包括消费速度和位移。
13、kafka生产端分配,路由?
Kafka 生产端负责将消息发送到 Kafka 集群,消息将被路由到适当的主题和分区。Kafka 使用分区策略来确定消息的分区,以便将消息均匀地分布到不同的分区中。
一些相关概念:
- 主题:Kafka 主题是消息的逻辑容器,生产者将消息发送到一个或多个主题。主题通常代表了一类消息,例如,日志、事件、或者其他数据类型。
- 分区:Kafka 主题可以被划分为多个分区,每个分区是消息的物理存储单元。分区可以并行地处理消息,并且每个分区都有自己的位移(offset)来跟踪已经被消费的消息。
- 分区策略:分区策略是确定消息将被发送到哪个分区的规则。Kafka 提供了多种分区策略,包括轮询(Round-robin)、哈希(Hashing)、和自定义分区策略。生产者可以根据需求选择适当的分区策略。
- 轮询分区策略:轮询分区策略是最简单的策略,它按照顺序将消息发送到不同的分区。这确保了消息在不同分区之间均匀分布,适用于负载均衡的场景。
- 哈希分区策略:哈希分区策略基于消息的键(Key)进行哈希计算,将相同键的消息路由到相同的分区。这确保了具有相同键的消息总是进入同一分区。
- 自定义分区策略:开发人员可以编写自定义分区策略来根据特定的业务逻辑将消息路由到分区。这允许更灵活的消息路由。
- Producer API:Kafka 提供了各种编程语言的生产者 API,使开发人员能够轻松地将消息发送到 Kafka 集群,并根据需求配置分区策略。
- 生产者确认:Kafka 生产者可以配置确认机制,以确保消息被成功写入分区。这包括确认(acknowledgment)机制,生产者将等待分区的确认后才会认为消息发送成功。
- 消息分布:消息将按照分区策略分布到不同的分区中,生产者可以向不同的分区发送消息,以确保消息的分布和处理。
14、mysql索引,为什么B+树?
- 平衡性:B+树是一种自平衡的数据结构,它保持了树的高度相对较低,这意味着在最坏情况下,查找一个条目的时间复杂度是O(log n),其中n是索引中的条目数量。这对于支持高效的检索操作非常重要。
- 顺序访问性能:B+树的内部节点包含了指向子节点的指针,这使得B+树在范围查询中非常高效。例如,如果你需要检索某一范围内的数据,B+树可以沿着树的叶子节点顺序遍历,从而提高顺序访问的性能。
- 磁盘读写性能:B+树的节点大小通常是数据库页的大小,这有助于减少磁盘读写操作。较大的节点可以容纳更多的键值对,减少了磁盘I/O的频率。
- 有序性:B+树的叶子节点形成了一个有序链表,这使得范围查询和排序非常高效。数据库可以快速地遍历有序数据。
- 范围查询优势:由于B+树的有序性,它在范围查询中非常强大。你可以快速找到范围内的数据,而不必扫描整个表。
- 支持多列索引:B+树索引可以支持多列组合索引,这对于复杂查询条件非常有用。
- 高扇出性能:B+树具有高扇出性能,即每个内部节点有很多子节点。这降低了树的高度,减少了磁盘读取的次数。
- 支持并发操作:B+树索引支持并发插入和删除操作,这对于多用户、多线程的数据库非常重要。
15、主键索引,二级索引?
- 主键索引:
- 主键索引是用于唯一标识每一行记录的索引。每个表只能有一个主键索引。
- 主键索引通常用于加速检索特定记录或进行数据修改操作。
- 主键索引要求索引列的值唯一且非空。
- 主键索引是表的聚集索引,这意味着表中的数据按照主键索引的顺序物理存储。
- 主键索引通常具有较高的查找性能,因为它能够快速定位到特定的行。
- 二级索引:
- 二级索引是除主键索引以外的其他索引,它们用于加速特定查询条件的检索。
- 表可以有多个二级索引,用于加速不同类型的查询,如根据非主键列的条件检索数据。
- 二级索引的值可以重复,不要求唯一性,因为它们只是辅助索引,用于更快速地定位到主键值。
- 二级索引通常包含了索引列的值和对应的主键值,以便在查找时能够直接找到对应的行。
- 二级索引可以提高查询性能,但也会占用额外的存储空间和增加更新操作的开销。
16、mysql ACID?
- 原子性(Atomicity):
- 原子性确保事务是一个不可分割的操作单元,要么完全执行,要么完全不执行。如果事务的任何部分失败,整个事务将被回滚,数据库状态将恢复到之前的状态。
- 原子性防止了不完整或部分执行的事务,确保了数据库的一致性。
- 一致性(Consistency):
- 一致性确保事务在执行前后维持数据库的一致性状态。这意味着事务在执行后,数据库应该从一种一致的状态转移到另一种一致的状态。
- 一致性要求事务的操作遵循数据库的完整性约束和业务规则,以保持数据的合法性。
- 隔离性(Isolation):
- 隔离性确保并发执行的多个事务不会相互干扰,每个事务都应该被视为在没有其他事务干扰的情况下执行。
- 隔离级别(如读未提交、读已提交、可重复读和串行化)定义了不同程度的隔离,以控制并发事务之间的相互影响。
- 持久性(Durability):
- 持久性确保一旦事务成功提交,其结果将永久存储在数据库中,即使系统崩溃或断电也不会丢失。
- 数据库系统通常使用日志文件来实现持久性,确保在系统崩溃后可以恢复事务。
17、mysql事务隔离级别,RR解决幻读吗?什么场景下会幻读?
- 什么是幻读?
幻读是一种并发事务的问题,它发生在多个事务之间,其中一个事务在某个范围内插入新行,而另一个事务尝试查询相同范围内的数据。这可能导致查询事务看到新插入的行,即使在它开始查询之前这些行并不存在。幻读是一个类似于脏读的问题,但是它关注的是插入操作而不是修改操作。
- RR隔离级别如何解决幻读?
RR隔离级别通过使用锁或多版本并发控制(MVCC)来解决幻读问题。在RR隔离级别下,事务会获取一个范围锁,以确保在事务中查询的范围内的数据在事务结束之前不会被其他事务修改或插入。
举例来说,如果一个事务A在RR隔离级别下查询某个范围的数据,另一个事务B想要在相同范围内插入新的行,事务B将被阻塞,直到事务A完成。这可以防止事务A看到事务B插入的新行,从而解决了幻读问题。
可能出现幻读问题:
假设一个在线购物系统,多个用户同时浏览某个商品的库存情况。如果一个用户正在查询库存时,另一个用户刚好购买了最后一件商品,事务A可能在查询时看到商品的数量是1,但在实际购买时,库存已经为0了,这就是幻读问题。
18、MVCC,事务版本号怎么生成,存在哪里?
MVCC是一种并发控制机制,允许数据库系统在同一时间点存在多个版本的数据,以支持事务隔离和并发查询。在MVCC中,每个数据行都有一个或多个版本号,用于标识数据的不同版本。这些版本号是如何生成和存储的取决于数据库管理系统的具体实现。
通常,MVCC系统中的版本号是在数据行上生成的,并且通常包括以下信息:
- 事务ID(Transaction ID):版本号通常包含了生成该版本的事务的唯一标识符或ID。这使得数据库能够跟踪哪个事务生成了哪个版本的数据。
- 时间戳(Timestamp):版本号通常包括生成该版本的时间戳。时间戳可以是事务开始或提交的时间,以及其它时间单位,用于确定版本的时间顺序。
版本号的生成和存储方式因DBMS而异,但通常存储在数据行的元数据中,以便系统能够在查询时识别和访问不同版本的数据。数据库系统还维护一个版本控制的数据结构,通常称为版本链或版本表,以跟踪每个数据行的不同版本及其关系。
MVCC的主要优点是它允许高度并发的读取操作,因为每个事务都可以看到一致性的数据快照,而不会阻塞其他事务的写入操作。不同数据库管理系统的MVCC实现方式可能有所不同,但它们都旨在提供高并发性和事务隔离。在查询时,数据库系统会根据当前事务的ID或时间戳来选择适当版本的数据,以确保事务之间的隔离。
19、binlog,redolog,undolog?
- Binlog(二进制日志):
binlog是 MySQL 数据库中的二进制日志,用于记录数据库的修改操作,如插入、更新和删除。它记录了 SQL 语句或数据更改事件的二进制表示形式,而不是实际的数据值。- 主要用途是用于数据库的备份、主从复制和故障恢复。通过分析
binlog,可以还原数据库的历史状态。
- Redo Log(重做日志):
redo log是数据库管理系统中的一种日志,通常用于记录数据修改操作。它以物理方式记录了对数据库页的更改,而不是 SQL 语句。- 主要用途是确保事务的持久性(Durability),在数据库系统发生崩溃或故障时,可以使用
redo log来重放事务,以确保数据的一致性。
- Undo Log(撤销日志):
undo log也是数据库管理系统中的一种日志,用于记录事务的撤销操作。它包含了事务执行前的数据状态,以便在需要时回滚事务。- 主要用途是支持事务的回滚操作和多版本并发控制(MVCC)。在事务发生回滚时,可以使用
undo log将数据还原到之前的状态。
简单记忆:
binlog用于记录数据更改的逻辑日志,通常用于备份和复制。redo log用于记录物理数据页的更改,以确保持久性。undo log用于支持事务的回滚和多版本并发控制。
20、挂了用什么log,主从同步用什么log?
当数据库系统发生崩溃或非正常关闭时,崩溃恢复日志(通常是 redo log)用于重放未完成的事务,以确保数据的持久性。主从同步通常依赖于二进制日志(Binlog)来保持主数据库和从数据库之间的数据一致性,以支持数据库复制和高可用性方案。
在数据库管理中,有下面几种不同的日志类型用于不同的目的,主要包括:
Crash Recovery Log(崩溃恢复日志):
用于在数据库系统发生崩溃或非正常关闭时,恢复数据到一致状态。通常,这包括数据库的
redo log,用于重放未完成的事务以确保数据的持久性。Binary Log(二进制日志或Binlog):
用于记录数据库的修改操作,如插入、更新和删除。它通常用于数据库备份、主从复制和数据库同步。
Error Log(错误日志):
用于记录数据库系统的错误消息、警告和异常情况。这对于诊断和解决问题非常有帮助。
Transaction Log(事务日志):
用于记录事务的操作,以支持数据库的事务性和回滚操作。这包括数据库的
undo log,它用于支持事务的回滚操作。
21、binlog的两阶段提交?
Binlog 两阶段提交的工作原理:
- 阶段一 - 预提交(Prepare Phase):
- 当主数据库上执行一个事务时,Binlog 记录了该事务的所有修改操作。
- 在事务提交之前,主数据库向所有从数据库发送预提交信息,告诉它们该事务即将提交。
- 从数据库接收到预提交信息后,将 Binlog 中的事务操作记录到本地 Relay Log 中,但不应用这些修改。
- 阶段二 - 最终提交(Commit Phase):
- 一旦所有从数据库确认已准备好接受事务并已将预提交的 Binlog 记录到 Relay Log 中,主数据库执行最终提交。
- 主数据库通知所有从数据库,可以应用该事务的 Binlog 记录到它们的数据中。
- 从数据库收到最终提交通知后,将 Relay Log 中的 Binlog 记录应用到它们的数据中,确保数据的一致性。
通过这两个阶段的协调,MySQL 确保在主从复制中的数据更新是原子的,以避免数据不一致的情况。如果有从数据库无法确认准备好或发生错误,主数据库会等待,以确保所有从数据库都能达到一致的状态,然后才会执行最终提交。
22、算法题:买卖股票
描述:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
思路:
- 初始化
min_price为第一天的股票价格prices[0],并将max_profit设为0。 - 从第二天的价格
prices[1]开始遍历数组,依次执行以下操作:- 如果当前价格
prices[i]小于min_price,则将min_price更新为prices[i]。 - 否则,计算当前价格
prices[i]减去min_price的差值,如果这个差值大于max_profit,则将max_profit更新为这个差值。
- 如果当前价格
- 遍历完成后,
max_profit就是所能获取的最大利润。
示例代码:
#include <vector>
int maxProfit(std::vector<int>& prices) {
if (prices.empty()) {
return 0;
}
int min_price = prices[0];
int max_profit = 0;
for (int i = 1; i < prices.size(); i++) {
if (prices[i] < min_price) {
min_price = prices[i];
} else {
int current_profit = prices[i] - min_price;
if (current_profit > max_profit) {
max_profit = current_profit;
}
}
}
return max_profit;
}
int main() {
std::vector<int> prices = {7, 1, 5, 3, 6, 4};
int result = maxProfit(prices);
std::cout << "Max Profit: " << result << std::endl;
return 0;
}二面
深挖项目
1、TCP和UDP?
TCP:
- 面向连接: TCP 是面向连接的协议,它在通信开始之前需要建立连接,然后在通信结束后释放连接。这确保了可靠的数据传输,但会引入一些额外的开销。
- 可靠性: TCP 提供可靠的数据传输,确保数据包按顺序到达,并能够重新发送丢失或损坏的数据包,以保持数据的完整性。
- 流控制: TCP 支持流量控制机制,以防止发送方发送过多的数据,从而防止网络拥塞。这通过滑动窗口机制实现。
- 拥塞控制: TCP 有内置的拥塞控制机制,可监视网络拥塞情况,并动态调整发送速率以避免拥塞。
- 有序性: TCP 保证数据包按顺序到达,这对于那些要求数据有序性的应用非常重要。
- 连接开销: 由于建立和维护连接的开销,TCP 的开销相对较高。这使得它适用于需要可靠性的应用,如网页浏览、文件传输和电子邮件。
UDP:
- 无连接: UDP 是一种无连接的协议,它不需要建立连接,数据包可以直接发送到目的地。这降低了通信的开销,但也意味着它不提供连接的可靠性。
- 不可靠性: UDP 不提供数据包的可靠性,数据包可能会丢失、重复或乱序。它适用于那些对数据可靠性要求不高的应用,如音视频流媒体和在线游戏。
- 无流控制: UDP 不提供内置的流量控制机制,发送方可以以任何速率发送数据,不考虑接收方的处理能力。
- 无拥塞控制: UDP 也没有内置的拥塞控制机制,它不会主动监视网络拥塞情况,因此在拥塞网络中可能导致数据包丢失。
- 低开销: 由于没有连接建立和维护的开销,UDP 具有较低的开销,适用于需要低延迟的应用,如实时音视频传输。
2、TCP可靠机制?
- 序列号和确认号: TCP 数据包中包含序列号和确认号。发送方使用序列号对数据包进行编号,接收方使用确认号来确认已经成功接收到的数据。这确保了数据包的有序传输。
- 确认机制: 接收方会定期发送确认(ACK)数据包,通知发送方已经成功接收到的数据包。如果发送方在一定时间内没有收到确认,它会重新发送相应的数据包。
- 超时重传: 如果发送方在一定时间内没有收到确认,它会假定数据包丢失,并进行超时重传。这确保了即使数据包在传输过程中丢失,它们最终仍能被成功传输。
- 流控制: TCP 使用滑动窗口机制来进行流控制。接收方可以通过通告窗口大小来告知发送方它还能接收多少数据。这有助于防止发送方发送过多的数据,从而避免数据丢失和网络拥塞。
- 拥塞控制: TCP 内置了拥塞控制机制,可以监视网络拥塞情况。如果发现网络拥塞,发送方会降低发送速率,以避免进一步加重拥塞。
- 有序性: TCP 保证数据包按顺序到达接收方,即使在网络中可能出现乱序传输,TCP 会将数据包按正确的顺序重新排列。
- 可靠的连接建立和终止: TCP 在连接建立和终止过程中使用多个握手和挥手步骤,以确保连接的可靠性。这包括三次握手和四次挥手过程。
3、Mysql引擎?
两个常见的:
- InnoDB: InnoDB 是 MySQL 默认的存储引擎,它支持事务处理和具备高度的数据完整性。InnoDB 提供了行级锁定和外键约束,适用于需要强调数据一致性和事务处理的应用。
- MyISAM: MyISAM 是 MySQL 中的另一种存储引擎,它支持全文本搜索和表级锁定。MyISAM 不支持事务处理,但适用于需要快速读取和写入的应用,如数据仓库和日志记录。
一些作为了解的:
- MEMORY: MEMORY 存储引擎将数据存储在内存中,因此读取速度非常快,但数据不会持久保存。它适用于缓存和临时数据存储。
- NDB Cluster: NDB Cluster 存储引擎是 MySQL 的集群存储引擎,支持分布式数据库。它适用于需要高可用性和负载均衡的应用。
- Archive: Archive 存储引擎用于高压缩率的数据存储,适用于归档数据和历史数据存储。
- TokuDB: TokuDB 存储引擎专注于大数据处理,支持高性能的数据插入和查询。它适用于需要处理大量数据的应用,如日志和分析应用。
- Federated: Federated 存储引擎允许在一个 MySQL 数据库中访问另一个远程 MySQL 数据库的表。它适用于分布式数据访问。
- Blackhole: Blackhole 存储引擎实际上不存储数据,而只是将数据传递到其他存储引擎。它可用于数据复制和数据分发。
- CSV: CSV 存储引擎用于读取和写入 CSV 文件格式的数据。它适用于与其他应用程序交换数据。
- JSON: JSON 存储引擎支持 JSON 数据类型,用于存储和查询 JSON 数据。
4、Mysql索引,为什么是B+树?
一面已答。
5、索引失效?
索引失效指的是在数据库查询中,数据库管理系统无法有效地使用现有的索引来加速查询,而需要进行全表扫描或全索引扫描的情况。索引失效可能会导致查询性能下降。
索引失效的一些原因:
- 未使用索引列进行查询: 如果查询条件不包含在索引列中,索引就无法加速查询。例如,如果有一个名为 "name" 的索引,但查询是基于 "age" 列的,那么索引就无法用于加速查询。
- 使用函数或操作符处理索引列: 当在查询中对索引列使用函数或操作符时,索引可能会失效。例如,如果对 "name" 列进行了 UPPER() 函数操作,索引就无法使用。
- 使用不等号条件: 对于某些不等号条件(如不等于、大于、小于等),索引的使用可能会受到限制。通常情况下,等于条件(=)更容易使用索引。
- 数据分布不均匀: 如果索引列的数据分布不均匀,即某些值出现次数很多,而其他值出现次数很少,索引的选择性较低,可能不会被选择来加速查询。
- 索引列类型不匹配: 如果查询条件的数据类型与索引列的数据类型不匹配,索引可能会失效。例如,如果索引列是字符串类型,但查询条件使用了数字,索引可能无法使用。
- 索引顺序不匹配: 对于复合索引,如果查询条件的列顺序与索引的列顺序不匹配,索引可能无法用于加速查询。
- 表数据量太小: 对于非常小的表,全表扫描通常比使用索引更高效,因此数据库管理系统可能会选择不使用索引。
6、脏读,不可重复读,幻读?
- 脏读(Dirty Read): 脏读发生在一个事务读取了另一个事务未提交的数据。当一个事务读取另一个事务的数据,而后者后来回滚,导致读取的数据事实上是无效的。这会引起严重的数据不一致问题。
- 不可重复读(Non-Repeatable Read): 不可重复读发生在一个事务内的两次查询之间,另一个事务修改了数据,导致第一次查询的结果与第二次查询的结果不一致。这是因为第一次查询返回的数据在第二次查询之前已经被修改。
- 幻读(Phantom Read): 幻读是在一个事务内的两次查询之间,另一个事务插入了新的数据行,导致第一次查询的结果与第二次查询的结果不一致。尽管数据未被修改,但由于新数据的插入,第二次查询可能返回不同的结果。
这些问题与数据库的隔离级别有关,SQL 标准定义了四种隔离级别:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)、和串行化(Serializable)。不同的隔离级别会导致不同的数据一致性问题:
- 在读未提交隔离级别下,允许脏读、不可重复读和幻读。
- 在读提交隔离级别下,防止脏读,但允许不可重复读和幻读。
- 在可重复读隔离级别下,防止脏读和不可重复读,但允许幻读。
- 在串行化隔离级别下,防止脏读、不可重复读和幻读,但性能可能受到一定的影响。
7、Mysql事务隔离级别,怎么解决,MVCC+next-key lock可以解决幻读吗?
- MVCC(多版本并发控制): MVCC 是 MySQL 使用的一种机制,它通过在数据库中保存不同版本的数据来解决并发读写问题。在 MVCC 中,每个事务在启动时会获得一个事务 ID,读操作只能看到比当前事务 ID 小或等于该事务 ID 的数据版本,这样可以实现数据的隔离。MVCC 主要用于可重复读和读提交隔离级别下,以解决不可重复读和幻读的问题。但在可重复读隔离级别下,仍然可能发生幻读问题。
- Next-Key Locks: Next-Key Locks 是 MySQL 中的锁机制,它在索引上创建了锁,包括了满足条件的记录以及下一个记录的锁。这可以防止其他事务在当前事务读取或修改记录时插入新的记录,从而避免了幻读问题。Next-Key Locks 主要用于可重复读隔离级别下,以解决幻读问题。
需要注意的是,MVCC 和 Next-Key Locks 能够有效减轻幻读问题,但无法在所有情况下完全解决幻读。在某些特定情况下,如涉及范围查询的情况,仍然可能发生幻读。在这种情况下,应根据应用的需求和数据一致性的要求来选择事务隔离级别,以便在性能和一致性之间进行权衡。如果特别关注数据的一致性,可以选择串行化隔离级别,但要注意性能可能会受到影响。
8、算法题:树的先中后序遍历
前序遍历
前序遍历是一种深度优先遍历方式,它的访问顺序是先访问根节点,然后递归地访问左子树和右子树。
递归:
思路:
- 访问当前节点(根节点)。
- 递归遍历左子树。
- 递归遍历右子树。
示例代码:
#include <iostream>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void preorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 1. 访问当前节点
std::cout << root->val << " ";
// 2. 递归遍历左子树
preorderTraversal(root->left);
// 3. 递归遍历右子树
preorderTraversal(root->right);
}
int main() {
// 构建一个示例二叉树
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
// 执行先序遍历
std::cout << "Preorder Traversal: ";
preorderTraversal(root);
return 0;
}非递归:
思路:
- 创建一个栈,以及一个指向树根节点的指针。
- 将根节点入栈。
- 弹出栈顶节点,访问它。
- 如果该节点有右子节点,将右子节点入栈,再将左子节点入栈(这样确保左子节点在栈顶)。
- 重复步骤3和4,直到栈为空。
示例代码:
void preorderTraversal(TreeNode* root) {
std::stack<TreeNode*> s;
TreeNode* current = root;
while (current || !s.empty()) {
while (current) {
std::cout << current->val << " ";
if (current->right) {
s.push(current->right);
}
current = current->left;
}
if (!s.empty()) {
current = s.top();
s.pop();
}
}
}中序遍历
中序遍历也是一种深度优先遍历方式,它的访问顺序是先遍历左子树,然后访问根节点,最后遍历右子树。
递归:
思路:
- 递归遍历左子树。
- 访问当前节点(根节点)。
- 递归遍历右子树。
示例代码:
#include <iostream>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void inorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 1. 递归遍历左子树
inorderTraversal(root->left);
// 2. 访问当前节点
std::cout << root->val << " ";
// 3. 递归遍历右子树
inorderTraversal(root->right);
}
int main() {
// 构建一个示例二叉树
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
// 执行中序遍历
std::cout << "Inorder Traversal: ";
inorderTraversal(root);
return 0;
}非递归:
思路:
- 创建一个栈,以及一个指向树根节点的指针。
- 从根节点开始,将所有左子节点入栈,直到达到最左边的叶子节点。
- 出栈一个节点,访问它。
- 如果该节点有右子节点,将右子节点设为当前节点,然后重复步骤2。
- 重复步骤3和4,直到栈为空且当前节点为空。
示例代码:
void inorderTraversal(TreeNode* root) {
std::stack<TreeNode*> s;
TreeNode* current = root;
while (current || !s.empty()) {
while (current) {
s.push(current);
current = current->left;
}
current = s.top();
s.pop();
std::cout << current->val << " ";
current = current->right;
}
}后序遍历
后序遍历同样是一种深度优先遍历方式,它的访问顺序是先遍历左子树,然后遍历右子树,最后访问根节点。
递归:
思路:
- 递归遍历左子树。
- 递归遍历右子树。
- 访问当前节点(根节点)。
示例代码:
#include <iostream>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void postorderTraversal(TreeNode* root) {
if (root == nullptr) {
return;
}
// 1. 递归遍历左子树
postorderTraversal(root->left);
// 2. 递归遍历右子树
postorderTraversal(root->right);
// 3. 访问当前节点
std::cout << root->val << " ";
}
int main() {
// 构建一个示例二叉树
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
// 执行后序遍历
std::cout << "Postorder Traversal: ";
postorderTraversal(root);
return 0;
}非递归:
思路:
- 创建两个栈,一个用于节点的遍历,另一个用于保存遍历结果。
- 将根节点压入栈1。
- 从栈1弹出一个节点,然后将它压入栈2。
- 将当前节点的左子节点和右子节点分别压入栈1。
- 重复步骤3和4,直到栈1为空。
- 此时栈2中的元素就是后序遍历的顺序,将它们依次出栈并访问。
示例代码:
void postorderTraversal(TreeNode* root) {
std::stack<TreeNode*> s1, s2;
s1.push(root);
while (!s1.empty()) {
TreeNode* current = s1.top();
s1.pop();
s2.push(current);
if (current->left) {
s1.push(current->left);
}
if (current->right) {
s1.push(current->right);
}
}
while (!s2.empty()) {
std::cout << s2.top()->val << " ";
s2.pop();
}
}9、二进制加法
描述:
给定两个 01 字符串 a 和 b ,请计算它们的和,并以二进制字符串的形式输出。
输入为 非空 字符串且只包含数字 1 和 0。
思路:
- 创建一个空字符串
result用于保存结果,以及两个变量carry(用于表示进位,初始化为0)和i(用于从字符串末尾向前遍历)。 - 从字符串
a和b的末尾开始,逐位取出字符并转换为整数,分别为numA和numB。如果字符串已经遍历完,则对应的数字设为0。 - 计算当前位的和,即
sum = numA + numB + carry。注意,carry初始为0,但在每次迭代后可能会变为1。 - 计算当前位的结果和进位:
currentResult = sum % 2和carry = sum / 2。 - 将
currentResult转换为字符并插入到result的前面。 - 将
i向前移动一位。 - 重复步骤2到步骤6,直到遍历完字符串
a和b。 - 如果最后一次迭代后
carry为1,表示有进位,将"1"插入到result的前面。 - 返回
result作为最终结果。
示例代码:
#include <iostream>
#include <string>
using namespace std;
string addBinary(string a, string b) {
string result = "";
int carry = 0;
int i = a.length() - 1, j = b.length() - 1;
while (i >= 0 || j >= 0) {
int numA = (i >= 0) ? (a[i] - '0') : 0;
int numB = (j >= 0) ? (b[j] - '0') : 0;
int sum = numA + numB + carry;
int currentResult = sum % 2;
carry = sum / 2;
result = to_string(currentResult) + result;
i--;
j--;
}
if (carry == 1) {
result = "1" + result;
}
return result;
}
int main() {
string a = "1011";
string b = "1101";
string sum = addBinary(a, b);
cout << "Sum: " << sum << endl;
return 0;
}