腾讯CSIG暑期实习

一面 (60min)3.31

1. 是否了解Go语言?

Go 语言的特点

  1. 并发模型
    • Go 使用 goroutinechannel 来实现并发,goroutine 是轻量级的线程,启动和切换的开销非常小。
    • 与 Java 的线程模型相比,Go 的并发更加轻量和高效。
  2. 简洁的语法
    • Go 的语法设计非常简洁,去掉了许多复杂的特性(如继承、泛型等),降低了学习曲线。
    • 与 Java 的冗长语法相比,Go 更加直观和易读。
  3. 内存管理
    • Go 内置了垃圾回收机制(GC),开发者无需手动管理内存。
    • 与 Java 的 GC 类似,但 Go 的 GC 优化更适合短时任务和高并发场景。
  4. 标准库
    • Go 提供了丰富的标准库,包括 HTTP、JSON、数据库连接等,开箱即用。
    • 与 Java 的生态系统相比,Go 的标准库更轻量化,但功能足够强大。
  5. 编译速度快
    • Go 的编译速度非常快,适合快速开发和迭代。
    • 与 Java 的编译速度相比,Go 更加高效。
  6. 跨平台支持
    • Go 支持多种操作系统和架构,可以轻松编译为不同平台的可执行文件。
    • 与 Java 的跨平台能力类似,但 Go 的可执行文件是静态编译的,运行时无需额外的虚拟机。

Go 的应用场景

  1. 微服务
    • Go 的高性能和并发能力使其非常适合构建微服务架构。
    • 与 Java 的 Spring Boot 框架相比,Go 的启动速度更快,资源占用更少。
  2. 云原生开发
    • Go 是 Kubernetes 的核心开发语言,广泛用于云原生应用开发。
    • 与 Java 的云原生框架(如 Spring Cloud)相比,Go 更加轻量和高效。
  3. 网络服务
    • Go 的并发模型非常适合处理高并发的网络请求。
    • 与 Java 的 Netty 框架相比,Go 的代码更加简洁。
  4. 工具开发
    • Go 的编译速度快、运行效率高,适合开发各种命令行工具。

2. HashMap和ConcurrentHashMap的区别?

1. 线程安全性

  • HashMap
    • 非线程安全HashMap 是一个单线程环境下的集合类,它在多线程环境下可能会导致数据不一致或并发问题(如死循环)。
    • 如果需要在多线程环境下使用 HashMap,可以通过 Collections.synchronizedMap() 包装它,但这会显著降低性能,因为整个 HashMap 会被加锁。
  • ConcurrentHashMap
    • 线程安全ConcurrentHashMap 是为多线程环境设计的,它通过分段锁(Segment)机制实现了高效的并发操作。
    • 它允许多个线程同时读写,而不会导致数据不一致或死锁问题。

2. 性能

  • HashMap
    • 在单线程环境下,HashMap 的性能非常高,因为它没有额外的线程安全开销。
    • 在多线程环境下,如果不进行额外的同步处理,可能会导致数据不一致。
  • ConcurrentHashMap
    • 在多线程环境下,ConcurrentHashMap 的性能优于 HashMap 的同步包装版本(如 Collections.synchronizedMap())。
    • 它通过分段锁机制降低了锁的粒度,允许多个线程同时访问不同的段(Segment),从而提高了并发性能。

3. 数据一致性

  • HashMap
    • 在迭代过程中,如果对 HashMap 进行结构修改(如插入或删除元素),会抛出 ConcurrentModificationException
    • 这是因为 HashMap 的迭代器是快速失败(fail-fast)的。
  • ConcurrentHashMap
    • 在迭代过程中,允许其他线程对集合进行插入或删除操作,而不会抛出 ConcurrentModificationException
    • 它的迭代器是弱一致性的(weakly consistent),可以容忍并发修改。

4. 内部实现

  • HashMap
    • 基于数组和链表(或红黑树)的实现。
    • 当链表长度超过一定阈值时,会转换为红黑树以提高查找效率。
  • ConcurrentHashMap
    • 在 Java 7 中,基于分段锁(Segment)和链表的实现。
    • 在 Java 8 中,改进为基于 synchronized 和红黑树的实现,去掉了分段锁的概念,进一步提高了性能。

5. 初始化和容量

  • HashMap
    • 默认初始容量为 16,负载因子为 0.75。
    • 如果初始容量不足,会动态扩容(通常是原来的两倍)。
  • ConcurrentHashMap
    • 默认初始容量为 16,默认并发级别为 16。
    • 它的并发级别决定了内部的分段锁数量。

6. 适用场景

  • HashMap
    • 适用于单线程环境或读多写少的场景。
    • 如果需要在多线程环境下使用,可以通过 Collections.synchronizedMap() 包装,但性能会受到影响。
  • ConcurrentHashMap
    • 适用于多线程高并发环境。
    • 提供了高效的并发读写操作,适合需要频繁并发访问的场景。

3. JVM中的双亲委派模型?

​ JVM中的双亲委派模型是一种类加载机制,子类加载器在加载类时会先委托父类加载器尝试加载,只有父类加载器无法加载时,子类加载器才会自己加载。这种机制确保了类的唯一性和安全性。

4. IOC和AOP的原理?

1. IOC(控制反转,Inversion of Control)

​ IOC 是 Spring 框架的核心特性之一,它的核心思想是将对象的创建和依赖关系的管理交给 Spring 容器,而不是由开发者手动创建和管理对象。

  • 原理
    • 依赖注入(Dependency Injection, DI):开发者不需要手动创建对象,而是通过 Spring 容器将对象的依赖关系注入到目标对象中。
    • Spring 容器负责管理对象的生命周期,包括对象的创建、初始化、销毁等。
    • 常见的依赖注入方式:
      • 构造器注入:通过构造函数注入依赖。
      • Setter 注入:通过 setter 方法注入依赖。
      • 字段注入:直接在字段上注入依赖(需要使用注解,如 @Autowired)。
  • 工作流程
    1. 定义 Bean(对象)及其依赖关系(通过 XML 配置文件或注解)。
    2. Spring 容器(如 ApplicationContext)读取配置,创建 Bean 并注入依赖。
    3. 开发者通过容器获取 Bean,无需手动管理对象的创建和依赖。
  • 优点
    • 降低代码耦合度,提高代码的可维护性和可测试性。
    • 通过配置文件或注解灵活管理对象的创建和依赖关系。

2. AOP(面向切面编程,Aspect-Oriented Programming)

​ AOP 是一种编程范式,用于将横切关注点(如日志、事务、安全等)与业务逻辑分离,从而提高代码的模块化和可维护性。

  • 原理
    • 横切关注点(Cross-Cutting Concerns):如日志记录、事务管理等,这些功能通常会贯穿多个模块。
    • 切面(Aspect):横切关注点的模块化实现,通常通过注解(如 @Aspect)定义。
    • 连接点(Join Point):程序执行的某个特定点,如方法调用。
    • 通知(Advice):在连接点执行的代码,可以是前置通知、后置通知、环绕通知等。
    • 切点(Pointcut):定义通知在哪些连接点执行的表达式。
  • 工作流程
    1. 定义切面(Aspect),包含通知(Advice)和切点(Pointcut)。
    2. Spring AOP 使用动态代理或 CGLIB 代理,在运行时拦截连接点,并执行通知逻辑。
    3. 通知逻辑可以是前置处理、后置处理、环绕处理等。
  • 优点
    • 将横切关注点与业务逻辑分离,提高代码的模块化和可维护性。
    • 通过注解或 XML 配置灵活管理切面逻辑。

总结

  • IOC:通过依赖注入将对象的创建和依赖管理交给 Spring 容器,降低代码耦合度。
  • AOP:通过切面编程将横切关注点与业务逻辑分离,提高代码的模块化和可维护性。

5. Nacos的服务注册与发现的原理?RPC进行请求时,注册中心如何知道路由到哪台机器?

服务注册

​ 当服务提供者启动时,它会通过 Nacos 客户端向 Nacos 服务器发送注册请求,告知服务器服务的名称、IP 地址、端口号等信息。Nacos 服务器接收到请求后,将服务实例信息存储在内存和数据库中。服务提供者会定期向 Nacos 发送心跳包,以维持服务的有效性。

服务发现

​ 当服务消费者需要调用其他服务时,它会通过 Nacos 客户端向 Nacos 服务器发送服务发现请求。Nacos 服务器根据请求中的服务名称,从内存中查找并返回所有可用的服务实例列表。服务消费者可以根据返回的实例列表选择一个服务实例进行调用。

RPC 进行请求时,注册中心如何知道路由到哪台机器?

​ 当服务消费者需要调用某个服务时,它会通过 Nacos 客户端向 Nacos 服务器发送服务发现请求,获取目标服务的实例列表。Nacos 返回的服务实例列表包含所有可用的 IP 地址和端口号。服务消费者可以根据负载均衡策略(如轮询、随机、权重等)选择一个服务实例进行调用。

总结

  • 服务注册:服务提供者向 Nacos 注册自身信息,Nacos 存储并管理这些信息。
  • 服务发现:服务消费者通过 Nacos 获取目标服务的实例列表,选择合适的服务实例进行调用。
  • 负载均衡:Nacos 提供多种负载均衡策略,确保请求能够均匀地分配到不同的服务实例上。

6. 心跳因为网络异常中断会发生什么,如何处理?

心跳中断的影响

  • 服务实例被标记为不健康:如果心跳时间超过 15 秒,Nacos 会将该服务实例标记为不健康状态。
  • 服务实例被删除:如果心跳时间超过 30 秒,Nacos 会自动删除该服务实例。

7. Mysql的索引?索引的最左匹配原则?

索引的类型

  • 普通索引:最基本的索引类型,用于加速查询。

    CREATE INDEX idx_column ON table_name(column_name);
  • 唯一索引:确保索引列中的值唯一。

    CREATE UNIQUE INDEX idx_column ON table_name(column_name);
  • 主键索引:自动创建的唯一索引,用于标识表中的主键。

  • 全文索引:用于全文搜索,适用于 CHARVARCHARTEXT 类型。

    CREATE FULLTEXT INDEX idx_text ON table_name(text_column);
  • 组合索引(复合索引):在多个列上创建索引,用于加速多列查询。

    CREATE INDEX idx_columns ON table_name(col1, col2, col3);

索引的最左匹配原则

1. 最左匹配原则的定义

最左匹配原则是指在复合索引中,查询条件必须从索引的最左列开始匹配,才能有效利用索引。

2. 工作原理

  • 复合索引的顺序:复合索引的列顺序决定了查询条件的匹配方式。
  • 最左前缀匹配:查询条件必须包含索引的最左列,才能利用索引。

8. 行锁和表锁的区别?触发时机?

行锁和表锁的区别

特性 行锁 表锁
锁定范围 表中的某一行 整个表
并发性能 高,并发能力强 低,并发能力弱
适用场景 高并发场景,如单行更新或删除 批量操作或需要对整个表进行独占访问的场景
实现复杂度 复杂,开销较大 简单,开销较小
锁定粒度 细粒度 粗粒度

行锁和表锁的触发时机

行锁的触发时机

  • 更新或删除单行数据

    • 当事务需要对表中的某一行进行更新或删除时,会触发行锁。

      UPDATE table_name SET column1 = value1 WHERE id = 1;
      DELETE FROM table_name WHERE id = 1;
  • SELECT … FOR UPDATE

    • 在事务中,使用 SELECT ... FOR UPDATE 查询时,会锁定查询结果中的每一行。

      SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
  • 索引锁定

    • 如果查询条件中使用了索引,行锁可能会锁定索引节点。

      SELECT * FROM table_name WHERE indexed_column = 'value' FOR UPDATE;

表锁的触发时机

  • 批量操作

    • 当事务需要对整个表进行批量操作时,会触发表锁。

      ALTER TABLE table_name ADD COLUMN new_column VARCHAR(255);
      TRUNCATE TABLE table_name;
  • 显式锁定

    • 使用 LOCK TABLES 显式锁定表。

      LOCK TABLES table_name WRITE;
  • 事务隔离级别

    • 在某些隔离级别下(如 SERIALIZABLE),查询操作可能会触发表锁。

      SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
      START TRANSACTION;
      SELECT * FROM table_name;

9. 有哪些优化SQL查询效率的方法?

1. 索引优化

  • 创建合适的索引
    • 为经常用于查询条件的列创建索引(如 WHEREJOINORDER BYGROUP BY)。
    • 使用 EXPLAIN 分析查询计划,查看是否使用了索引。
  • 避免索引滥用
    • 不要为低选择性的列创建索引(如性别列)。
    • 避免为频繁更新的列创建索引,因为索引维护会增加写操作的开销。
  • 使用复合索引
    • 如果查询条件涉及多个列,可以创建复合索引(最左匹配原则)。
    • 例如:CREATE INDEX idx_name_age ON users(name, age);
  • 定期维护索引
    • 使用 ANALYZE TABLE 更新表的统计信息,帮助查询优化器选择合适的索引。

2. 查询语句优化

  • **避免使用 SELECT ***:
    • 只查询需要的列,减少数据传输量。
  • 减少子查询
    • 尽量将子查询转换为 JOIN,因为子查询通常性能较差。
  • 使用覆盖索引
    • 确保查询只使用索引中的列,而无需回表查询。
  • 避免模糊查询前缀
    • 避免使用 LIKE '%value',因为这会导致全表扫描。
  • 优化 OR 条件
    • 使用 INUNION 替代 OR,因为 OR 可能导致索引失效。

3. 数据库设计优化

  • 合理设计表结构
    • 使用合适的数据类型(如 INT 替代 VARCHAR 存储数字)。
    • 避免存储大文本或二进制数据(如 BLOB),可以存储外部文件路径。
  • 归一化与反归一化
    • 根据查询需求,适当进行归一化或反归一化,减少数据冗余或查询复杂度。
  • 分区表
    • 对大数据表进行分区(如按时间分区),提高查询效率。

10. 哪些表适用分表?哪些表使用分区?

适用分表的表

  • 垂直分表:适用于字段较多但访问频率不同的表。例如,用户信息表可以拆分为基本登录信息表(如用户名、密码、邮箱)和详细资料信息表(如姓名、性别、出生日期、地址、联系方式等)。
  • 水平分表:适用于数据量大、查询性能要求高的表。例如,订单表可以按照订单ID取模拆分为多个表,如orders_0orders_1等。
  • 分库分表:适用于高并发、大规模数据存储的场景。例如,将订单表拆分到不同的数据库实例上,如db_0.orders_0db_1.orders_1等。

适用分区的表

  • 范围分区(Range Partitioning):适用于处理具有连续数值范围的数据,如时间序列数据、按数值大小划分的数据等。例如,员工入职日期表可以按照年份范围进行分区。
  • 列表分区(List Partitioning):适用于数据值具有明确离散类别且查询经常针对这些特定类别进行的情况。例如,产品信息表可以按照产品类别进行分区。
  • 哈希分区(Hash Partitioning):适用于希望数据在各个分区中均匀分布,且不需要按照特定顺序或范围组织数据的情况。例如,用户登录信息表可以以用户ID作为分区键进行哈希分区。

11. JTW鉴权中如何进行登录态的续期?

1. 双 token 方案(access_token + refresh_token)

工作原理

  1. 初次认证:用户登录成功后,后端生成 access_tokenrefresh_token,并将它们返回给前端。
  2. API 访问:前端使用 access_token 访问后端资源。如果 access_token 未过期,后端正常处理请求。
  3. 续期机制
    • access_token 过期时,后端返回特定的错误码,提示前端使用 refresh_token 刷新 access_token
    • 前端捕捉到错误码后,使用 refresh_token 向后端请求新的 access_token,后端验证 refresh_token 的有效性并返回新的 access_token
  4. 安全性
    • refresh_token 应设置较长的过期时间,但并非永久有效,以防止账号长时间未使用的风险。
    • 当用户登出或检测到安全风险时,后端使 access_tokenrefresh_token 失效,并清空客户端的 token。

2. 单 token 方案(基于 Redis 的自动续期)

工作原理

  1. 登录成功:后端生成 JWT token,并将其存储到 Redis 中,设置过期时间为 token 有效期的两倍。
  2. API 访问:前端携带 token 访问后端资源,后端通过过滤器验证 token 的有效性。
  3. 续期逻辑
    • 如果 token 已过期,后端从 Redis 中获取对应的缓存 token,并验证其有效性。
    • 如果缓存 token 未过期,后端重新生成一个新的 token 并更新 Redis 中的缓存,同时返回新的 token 给前端。
    • 如果缓存 token 已过期,返回用户信息失效,提示重新登录。

12. 使用CAS的好处?

1. 高并发性能

  • 无锁操作:CAS乐观锁不需要显式地获取和释放锁,减少了锁竞争和上下文切换的开销,从而提高了系统的并发性能。
  • 无阻塞:在数据读取时不会加锁,允许多个线程同时读取数据,而不会相互阻塞,特别适合读多写少的场景。

2. 避免死锁

  • 无死锁风险:由于CAS乐观锁不会阻塞其他线程的访问,因此不会出现死锁的情况,保证了系统的稳定性。

3. 灵活性

  • 简单易用:CAS乐观锁的实现相对简单,可以根据具体需求进行定制和优化,也可以与其他同步机制结合使用,提供更加灵活和高效的并发控制手段。

4. 非阻塞性

  • 非阻塞性同步:CAS乐观锁是一种非阻塞性的同步机制,它不会阻塞等待锁的线程,从而提高了系统的吞吐量和响应速度。

13. Nacos如何作为配置中心?拉、推模式?

1. 配置的存储与管理

  • 集中存储:Nacos 将配置信息以键值对(Key-Value)的形式集中存储在服务器端,支持多种数据格式(如 Properties、YAML、JSON)。
  • 版本管理:支持配置的版本发布和回滚操作,方便管理配置的变更历史。

2. 配置的动态更新

  • 实时推送:Nacos 通过监听机制,当配置发生变更时,能够实时将更新后的配置推送给所有订阅了该配置的客户端。
  • 本地缓存:客户端在获取配置后会进行本地缓存,以提高读取效率。

Nacos 的拉模式与推模式

拉模式(Pull Mode)

  • 工作原理:客户端定期向 Nacos 服务器发送请求,拉取最新的配置数据。常见实现方式是轮询。
  • 特点
    • 简单易实现:客户端主动请求配置数据,无需复杂的长连接维护。
    • 实时性较差:无法保证配置变更的即时性,取决于轮询间隔。
    • 服务端压力:频繁的轮询会增加服务端的负载。

推模式(Push Mode)

  • 工作原理:客户端与 Nacos 服务器建立长连接,服务器在配置变更时主动推送更新数据给客户端。
  • 特点
    • 实时性强:配置变更能够立即推送给客户端。
    • 依赖长连接:需要维护长连接,可能存在网络问题导致连接不可用(如“假死”)。
    • 心跳机制:需要通过心跳包保持连接的活跃性,以确保数据推送的可靠性。

14. 使用Redis缓存,如何保证更新实时性?

  1. 写通过策略:在写、改操作时,同时更新数据库和 Redis 缓存,确保数据的一致性,但会增加写操作的延迟。
  2. 读时更新策略:在读操作时,检查缓存是否存在数据,如果缓存不存在,则从数据库中读取数据并更新缓存。
  3. 消息队列:使用消息队列进行异步数据同步,是一种高效且可靠的方法。在写操作时,将数据变更信息发送到消息队列,监听消息队列的消费者接收到消息后,更新数据库和 Redis 缓存。
  4. 定时同步数据:通过定时任务,定期将数据库的数据同步到缓存中,这种方法可以确保缓存中的数据是最新的,同时减少了实时同步的压力。
  5. 利用触发器:在数据库中设置触发器,当数据发生变化时,触发器自动更新 Redis 缓存。

15. 布隆过滤器防止那些缓存的穿透?十亿商品如何使用布隆过滤器?

​ 布隆过滤器是一种高效的数据结构,用于判断一个元素是否可能存在集合中。它通过多个哈希函数将元素映射到位数组中,具有以下特点:

  • 空间效率高:相比其他数据结构,布隆过滤器占用的空间更小。

  • 查询速度快:判断元素是否存在的时间复杂度为 O(1)。

  • 误判可能性:布隆过滤器可能会误判元素存在,但不会误判元素不存在。

    对于十亿商品的场景,布隆过滤器可以通过以下步骤实现:

  1. 初始化布隆过滤器:在应用启动时,将所有商品 ID 加载到布隆过滤器中。可以使用 Redisson 等工具来创建和管理布隆过滤器。
  2. 查询时判断:每次用户查询商品时,先通过布隆过滤器判断商品 ID 是否可能存在。如果不存在,则直接返回空值;如果可能存在,则查询缓存或数据库。
  3. 动态更新:对于商品的增删改操作,需要及时更新布隆过滤器,以保证数据的实时性。

16. 缓存穿透、缓存雪崩、缓存击穿的区别和处理方法?

1. 缓存穿透

  • 定义:大量请求查询不存在的数据,绕过缓存直接访问数据库,导致数据库压力骤增。
  • 处理方法
    • 布隆过滤器:将数据库中所有有效数据的键预加载到布隆过滤器中,查询时先通过布隆过滤器判断键是否存在,如果不存在则直接返回空结果。
    • 空值缓存:将不存在的数据也缓存起来,设置较短的过期时间,避免重复查询。

2. 缓存雪崩

  • 定义:大量缓存同时过期,导致大量请求同时查询数据库,造成数据库压力骤增。
  • 处理方法
    • 分散过期时间:为缓存设置不同的过期时间,避免大量缓存同时过期。
    • 异步预热:在缓存过期前,异步刷新缓存,确保缓存始终有效。
    • 限流降级:在缓存雪崩时,通过限流或降级策略限制数据库的访问量。

3. 缓存击穿

  • 定义:热点数据的缓存过期后,大量请求同时查询数据库,导致数据库压力骤增。
  • 处理方法
    • 互斥锁:在缓存过期时,使用互斥锁确保只有一个线程查询数据库,其他线程等待结果。
    • 双缓存策略:使用两个缓存,一个主缓存和一个备用缓存,主缓存过期时,备用缓存继续提供服务,同时刷新主缓存。

17. TCP三次握手和四次挥手?

1. TCP 三次握手

TCP 三次握手是 TCP/IP 协议中用于建立可靠连接的过程。通过三次握手,通信双方可以确保双方的发送和接收能力正常,并协商通信参数。

  1. 第一次握手:客户端发送一个带有 SYN 标志的 TCP 包到服务器,请求建立连接。
    • 客户端:SYN(同步序列号)
    • 服务器:收到请求后,回复一个带有 SYN-ACK 标志的 TCP 包。
  2. 第二次握手:服务器回复一个带有 SYN-ACK 标志的 TCP 包,确认收到客户端的请求,并发送自己的 SYN
    • 服务器:SYN-ACK
    • 客户端:收到后,回复一个带有 ACK 标志的 TCP 包,确认收到服务器的 SYN
  3. 第三次握手:客户端发送一个带有 ACK 标志的 TCP 包,确认收到服务器的 SYN,连接建立成功。
    • 客户端:ACK

2. TCP 四次挥手

TCP 四次挥手是 TCP/IP 协议中用于终止连接的过程。通过四次挥手,通信双方可以优雅地关闭连接。

  1. 第一次挥手:客户端发送一个带有 FIN 标志的 TCP 包到服务器,请求关闭连接。
    • 客户端:FIN
    • 服务器:收到后,回复一个带有 ACK 标志的 TCP 包,确认收到客户端的关闭请求。
  2. 第二次挥手:服务器发送一个带有 FIN 标志的 TCP 包到客户端,请求关闭连接。
    • 服务器:FIN
    • 客户端:收到后,回复一个带有 ACK 标志的 TCP 包,确认收到服务器的关闭请求。
  3. 第三次挥手:服务器收到客户端的 ACK 后,确认连接关闭。
  4. 第四次挥手:客户端收到服务器的 ACK 后,确认连接关闭。

3. 三次握手和四次挥手的区别

  • 三次握手:用于建立连接,确保双方的发送和接收能力正常。
  • 四次挥手:用于终止连接,确保双方都完成数据传输。

4. 为什么四次挥手而不是三次挥手?

  • 四次挥手的非对称性:客户端和服务器在关闭连接时的角色不同,客户端主动关闭连接,服务器被动关闭连接。
  • 确保双方都完成数据传输:四次挥手确保双方都确认对方的关闭请求,避免数据丢失。

算法题:无重复字符的最长子串问题

​ 使用滑动窗口和哈希表来解决这个问题。滑动窗口用于维护一个不包含重复字符的子串,哈希表用于记录字符的索引,以便快速判断字符是否已经在当前窗口中。