秋招面试错题记录

1.僵尸进程和孤儿进程的区别是什么?

1. 孤儿进程(Orphan Process)

  • 定义:当父进程先于子进程终止,子进程就会成为 “孤儿进程”。
  • 产生原因:父进程因异常崩溃、被手动终止(如 kill 命令)等原因提前退出,导致子进程失去父进程。
  • 系统处理:操作系统会将孤儿进程 “过继” 给 init 进程(进程号为 1,或现代系统中的 systemd 等),init 进程会成为其新的父进程,并在孤儿进程终止后负责回收其资源。

2. 僵尸进程(Zombie Process)

  • 定义:当子进程先于父进程终止,但父进程未调用 wait() 或 waitpid() 等系统调用来回收子进程的退出状态(如退出码、资源使用情况)时,子进程的进程控制块(PCB) 会残留在系统中,成为 “僵尸进程”。
  • 产生原因:父进程未正确处理子进程的终止事件(如编程时遗漏了 wait() 调用),导致子进程的残留信息(如进程号、退出状态)未被释放。

2.自己实现 RPC 框架应包含哪几个部分?

1.注册中心:注册中心负责服务信息的注册与查找。服务端在启动的时候,扫描所有的服务,然后将自己的服务地址和服务名注册到注册中心。客户端在调用服务之前,通过注册中心查找到服务的地址,就可以通过服务的地址调用到服务啦。常见的注册中心有 Zookeeper、Eureka 等。


2.动态代理:客户端调用接口,需要框架能自己根据接口去远程调用服务,这一步是用户无感知的。这样一来,就需要使用到动态代理,用户调用接口,实际上是在调用动态生成的代理类。常见的动态代理有:JDK Proxy,CGLib,Javassist 等。


3.网络传输:RPC 远程调用实际上就是网络传输,所以网络传输是 RPC 框架中必不可少的部分。网络框架有 Java NIO、Netty 框架等。网络传输协议:基于TCP或HTTP


4.自定义协议:网络传输需要制定好协议,一个良好的协议能提高传输的效率,也就是报文格式的设计。


5.序列化:网络传输肯定会涉及到序列化,常见的序列化有Json、Protostuff、Kyro 等。


6.负载均衡:当请求调用量大的时候,需要增加服务端的数量,一旦增加,就会涉及到符合选择服务的问题,这就是负载均衡。常见的负载均衡策略有:轮询、随机、加权轮询、加权随机、一致性哈希等等。


7.集群容错:当请求服务异常的时候,我们是应该直接报错呢?还是重试?还是请求其他服务?这个就是集群容错策略啦。


8.SPI机制,作为一个框架,支持插件化的必要功能


9.支持rpc 同步调用和异步调用

3.微服务架构

微服务间调用有鉴权吗

鉴权方式原理适用场景优缺点
服务身份认证(mTLS)基于 SSL/TLS 的双向认证:服务端和客户端都需提供证书,互相验证对方身份。安全要求极高的场景(如金融、支付、核心业务服务间通信)。优点:安全性强(证书难伪造)、无需额外令牌;缺点:证书管理复杂(签发、更新、吊销)。
内部令牌认证(如 JWT/Service Token)1. 调用方从 “服务注册中心 / 认证中心” 获取内部令牌(如 JWT,包含服务 ID、权限范围、过期时间);
2. 调用时在请求头(如Authorization: Bearer {token})携带令牌;
3. 被调用方验证令牌有效性(签名、过期时间、权限)。
无状态微服务间通信(如 Spring Cloud、K8s 微服务),且安全要求中等。优点:无状态(无需存储会话)、易于扩展;缺点:令牌泄露有风险(需短期过期 + 刷新机制)。
API 密钥认证(Service API Key)1. 每个服务在认证中心注册时,分配唯一的Service IDAPI Secret
2. 调用方用Service ID+API Secret生成签名(如 HMAC-SHA256),携带在请求头;
3. 被调用方用相同规则验证签名。
简单内部服务通信(如非核心业务、跨部门但信任度较高的服务)。优点:实现简单;缺点:密钥需安全存储(避免硬编码)、难以细粒度权限控制。
服务账户与 RBAC 权限1. 为每个服务分配 “服务账户”(类似 K8s 的ServiceAccount);
2. 基于 RBAC(角色权限控制)模型,给服务账户绑定 “可调用接口的角色”;
3. 调用时验证服务账户的角色权限。
基于 K8s 等容器化平台的微服务集群,需细粒度权限控制的场景。优点:权限管理灵活(可动态调整角色)、与平台生态集成好;缺点:依赖平台组件(如 K8s RBAC)。

API网关的鉴权方式是什么

JWT验证

无状态架构(如分布式微服务)、需减少网关与认证服务器交互的场景(提高性能)。

OAuth 2.0

原理:

适用场景:适用于授权登陆场景

API 密钥 / APPID

原理

1. 第三方合作方在网关注册,获取唯一APPIDAPP Secret
2. 调用时携带APPID,并通过APP Secret生成请求签名(如 HMAC-SHA256,包含时间戳防重放);
3. 网关验证签名有效性。

适用场景:外部合作方调用内部 API(如第三方物流服务调用电商的订单接口),无需用户身份,仅验证 “合作方身份”。

双向 TLS(mTLS)

原理:与微服务间 mTLS 原理一致:客户端(如设备、第三方服务)需提供证书,网关验证证书合法性后才允许请求。

4.详细说说OAuth框架

OAuth 是一个开放标准的授权框架,全称为 “Open Authorization”。它的核心作用是:允许第三方应用(如小程序、第三方网站)在不获取用户账号密码的情况下,安全地访问用户在某服务(如微信、GitHub)上的受保护资源(如用户信息、相册、订单等)。

一、OAuth 解决的核心问题

场景:你用 “小红书” App 时,选择 “用微信登录”,并允许小红书获取你的微信昵称和头像。这里的关键需求是:

  • 小红书需要访问你的微信用户信息(资源);
  • 你不想把微信的账号密码告诉小红书(安全需求);
  • 微信需要确认 “你允许小红书访问这些信息”(授权需求)。

OAuth 就是为这类场景设计的:它通过一套标准化流程,让第三方应用在用户明确授权的前提下,合法访问用户资源,同时全程不接触用户的核心凭证(如密码)。

二、OAuth 的关键角色

理解 OAuth 流程,首先要明确四个核心角色:

  1. 资源所有者(Resource Owner):通常是用户,拥有被访问的资源(如你的微信账号信息)。
  2. 客户端(Client):第三方应用,需要访问用户资源(如小红书 App)。
  3. 资源服务器(Resource Server):存储用户资源的服务(如微信的用户信息服务器,负责返回用户昵称、头像)。
  4. 授权服务器(Authorization Server):验证用户身份并颁发 “授权凭证” 的服务(如微信的授权服务器,负责处理用户授权并生成令牌)。

三、OAuth 2.0(主流版本)的核心流程

目前广泛使用的是 OAuth 2.0(OAuth 1.0 因复杂已很少用),其核心是 “通过令牌(Token)实现授权”。最常用的是授权码模式(安全级别最高,适用于有服务器的客户端,如 Web 应用、App),流程如下:

授权码模式步骤(以 “小红书用微信登录” 为例):
  1. 用户触发授权
    你在小红书点击 “微信登录”,小红书(客户端)会跳转到微信的授权页面(由微信的授权服务器提供),并携带参数:

    • client_id:小红书在微信开放平台注册的唯一标识(证明 “我是小红书”);
    • redirect_uri:授权完成后跳转回小红书的地址(如https://xiaohongshu.com/callback);
    • scope:请求的权限范围(如scope=userinfo,表示需要访问用户基本信息);
    • response_type=code:声明使用 “授权码模式”,需要返回授权码。
  2. 用户确认授权
    你在微信授权页面输入微信账号密码(仅微信可见,小红书看不到),并点击 “允许”(确认授权小红书获取你的昵称和头像)。

  3. 授权服务器返回授权码
    微信授权服务器验证通过后,会跳转到redirect_uri(小红书的回调地址),并在 URL 后附加一个授权码(code),如:
    https://xiaohongshu.com/callback?code=abc123(code 是临时的,通常 5 分钟内有效)。

  4. 客户端用授权码换令牌
    小红书的服务器(后端)收到code=abc123后,会向微信的授权服务器发送请求,携带:

    • code=abc123(刚获取的授权码);
    • client_id(小红书的标识);
    • client_secret(小红书在微信注册时的密钥,证明 “我确实是小红书”,此参数仅在后端传输,不暴露给前端);
    • grant_type=authorization_code(声明用授权码换令牌)。
  5. 授权服务器颁发令牌
    微信授权服务器验证codeclient_secret无误后,返回两个关键令牌:

    • 访问令牌(access_token):用于访问资源服务器的凭证(如 “允许用这个令牌获取用户信息”);
    • 刷新令牌(refresh_token):当访问令牌过期时,用它获取新的访问令牌(避免再次让用户授权)。
      示例返回:
    {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "refresh_token": "def456...",
      "expires_in": 3600,  // 访问令牌有效期(1小时)
      "scope": "userinfo"
    }
    

  6. 客户端用访问令牌访问资源
    小红书服务器携带access_token向微信的资源服务器请求你的信息:
    GET https://api.weixin.qq.com/userinfo?access_token=eyJhbGciOiJIUzI1Ni...
    资源服务器验证令牌有效后,返回你的昵称、头像等信息。

  7. 令牌过期时刷新
    access_token过期(如 1 小时后),小红书用refresh_token向授权服务器请求新令牌,避免再次打扰用户授权:
    POST https://api.weixin.qq.com/token?grant_type=refresh_token&refresh_token=def456...

5.MySQL场景题

数据量大且多表关联、用like模糊查询导致查询慢(10-20 秒),如何优化查询?

从优化查询语言层面来看

1. 优化 like 模糊查询(核心痛点)

like 查询慢的根本原因是:like '%xxx%'(前后模糊)或like '%xxx'(后缀模糊)无法使用普通索引,导致全表扫描。优化方向:

  • 改用前缀索引(适合前缀模糊场景)
    若查询是like 'xxx%'(前缀匹配),可对字段建立前缀索引(如ALTER TABLE t ADD INDEX idx_name (name(20))),仅索引字段前 20 个字符(平衡索引大小和区分度)。
    注意:仅适用于前缀匹配,且需评估字段前缀的区分度(区分度低的字段不适合,如性别)。

  • 使用 MySQL 全文索引(适合全文模糊)
    对文本字段(如titlecontent)建立FULLTEXT 索引ALTER TABLE t ADD FULLTEXT INDEX idx_ft_title (title)),查询时用MATCH...AGAINST替代 like:

    -- 替代 SELECT * FROM t WHERE title LIKE '%关键词%'
    SELECT * FROM t WHERE MATCH(title) AGAINST('关键词' IN BOOLEAN MODE);
    

     

    优势:全文索引支持分词匹配(英文默认分词,中文需配合插件如ngram),性能远高于 like 全表扫描。

  • 限制返回数据量
    若业务允许,通过LIMIT减少返回行数(如分页查询),避免一次性扫描大量数据:

    SELECT * FROM t WHERE content LIKE '%xxx%' LIMIT 100; -- 仅返回前100条

2. 优化多表关联查询

多表关联慢通常是因为关联条件无索引关联顺序不合理,优化措施:

  • 确保关联字段有索引
    关联条件(如a.id = b.a_id)中的字段必须建立索引(a.id为主键默认有索引,需给b.a_id建索引),避免关联时全表扫描。

  • 优化关联顺序
    MySQL 默认按 “小表驱动大表”(即外层循环用小表,内层循环用大表),可通过EXPLAIN查看执行计划,若顺序不合理,可通过STRAIGHT_JOIN强制指定关联顺序:

    -- 强制t1(小表)驱动t2(大表)
    SELECT * FROM t1 STRAIGHT_JOIN t2 ON t1.id = t2.t1_id;
    

  • 减少关联表数量
    仅关联必要的表,删除无关字段或表。例如:若查询只需a.nameb.value,无需关联c表(即使bc有关联)。

  • 用子查询或临时表预过滤数据
    先通过子查询过滤出小数据集,再关联其他表,减少关联的数据量:

    -- 先过滤t1中符合条件的小数据集,再关联t2
    SELECT * FROM (SELECT * FROM t1 WHERE create_time > '2023-01-01') t1 
    JOIN t2 ON t1.id = t2.t1_id;

3. 其他查询层优化

  • 避免SELECT *:只查询必要字段,减少 IO 和内存消耗,同时可能触发覆盖索引(索引包含所有查询字段,无需回表)。
  • 分析执行计划:通过EXPLAIN查看是否有type=ALL(全表扫描)、rows值过大的情况,针对性优化索引。

从数据库设计层面,如何避免多表关联查询的性能问题?

1. 适当反范式化(增加冗余字段)

在不严重影响写入性能的前提下,将关联表的常用字段冗余到主表,避免频繁关联。例如:

  • 订单表(orders)需频繁关联用户表(users)获取用户名(username),可在orders表中增加username字段,下单时同步写入,查询时直接从orders读取,无需关联users
  • 注意:冗余字段需保证一致性(如用户改名时同步更新订单表的username,可通过触发器或业务代码实现)。

2. 垂直拆分(按业务维度拆表)

将大表中 “不常一起查询” 的字段拆分到子表,减少单表字段数,同时降低关联需求。例如:

  • 用户表(users)包含基本信息(idnamephone)和详情信息(addressbirthavatar),可拆分为users_base(基本信息)和users_ext(详情信息)。
  • 日常查询用户基本信息时,只需查users_base,无需关联;需详情时再关联users_ext,减少高频查询的关联次数。

3. 水平拆分(按数据量拆表)

当单表数据量过大(如超过 1000 万行),即使有索引,查询和关联效率也会下降,需按规则拆分:

  • 范围拆分:按时间(如orders_2023orders_2024)或 ID 范围(如users_0users_1,ID%2=0 放 0 表)拆分,查询时仅访问目标分表,减少单表数据量。
  • 拆分后关联:若需跨分表关联,可通过中间表记录分表映射关系,或在应用层按拆分规则分别查询再聚合。

4. 优化外键设计

  • 避免过度使用外键:外键会增加写入时的约束检查开销,且多表关联时可能导致锁冲突,可在应用层保证数据一致性(如插入订单时检查用户 ID 是否存在)。
  • 多对多关系用中间表优化:例如 “商品 - 标签” 多对多关系,通过product_tag中间表(product_id+tag_id)关联,避免直接关联导致的笛卡尔积过大。

6.页面白屏的排查思路

先确认现象→再定位层→最后解决问题,核心依赖浏览器开发者工具(F12)和基础网络检测。

步骤 1:初步确认白屏类型(全白 / 部分白屏)

首先区分 “真白屏” 和 “假白屏”,缩小排查范围:

  • 全白屏:浏览器标签页标题正常,但页面完全空白(右键 “查看页面源代码” 无内容,或有内容但未渲染)→ 优先排查网络、HTML/JS 核心资源;
  • 部分白屏:页面头部 / 导航正常,仅内容区空白→ 优先排查接口数据(如接口返回空)、局部组件渲染错误。

步骤 2:检查网络层(关键资源是否能加载)

打开浏览器开发者工具(F12)→ 切换到 Network 面板,刷新页面,观察资源加载情况

检查HTML文件、CSS、JS文件加载情况;检查接口请求。

步骤 3:检查代码执行层(是否有报错阻断渲染)

切换到开发者工具的 Console 面板,查看是否有错误日志(红色报错信息),这是定位代码问题的核心

JS语法错误、JS运行时错误

步骤4:排查浏览器环境

1.浏览器打开 “无痕窗口”(Chrome:Ctrl+Shift+N;Firefox:Ctrl+Shift+P),重新访问页面:

  • 若无痕模式正常→ 原浏览器的缓存或插件问题(解决方案:清除缓存Ctrl+Shift+Del,禁用所有插件后逐个排查);
  • 若无痕模式仍白屏→ 排除缓存 / 插件,继续排查其他层。

2.测试其他浏览器是否白屏、浏览器的版本问题

步骤5:排查服务器层

7.第三方服务不稳定时如何保障自身服务稳定性?

事前预防:降低第三方不稳定的 “触发概率”

1. 减少直接依赖:缓存高频静态数据

2.避免单点依赖:多服务商 / 多实例容灾

核心第三方服务(如支付、短信、实名认证),采用 “多服务商备选” 或 “多实例调用” 策略,避免单一第三方故障导致功能不可用

3. 提前约定契约:接口规范与测试

通过 “契约测试” 和 “接口规范约定”,避免第三方接口变更(如参数增减、返回格式变化)导致自身服务报错

事中应对:第三方不稳定时的 “熔断、降级、重试”

1. 控制调用风险:超时 + 重试 + 幂等

  • 严格超时控制 对第三方服务的调用设置超时时间,避免线程被长时间阻塞

  • 有限重试机  确认什么情况下重试、重试策略:采用“指数退避”、重试次数

  • 确保幂等性(重试的前提):

    • 若重试第三方接口(如支付、下单),必须确保接口支持幂等(即重复调用结果一致),避免 “重复支付”“重复发短信”;
    • 实现:调用时携带唯一幂等标识(如requestId,由自身服务生成并唯一),第三方通过requestId去重。

2.阻断风险扩散:熔断机制

当第三方调用失败率超过阈值(如 50%),继续调用会导致 “自身线程耗尽” 和 “第三方雪上加霜”,此时需触发熔断(暂时停止调用第三方,直接走降级逻辑)

3.保障核心体验:降级兜底

熔断或第三方调用失败时,需根据 “功能重要性” 定义降级策略,保障核心功能可用

4.隔离调用资源:线程池 / 信号量

将第三方调用的线程与自身核心业务线程物理隔离,避免第三方的慢请求占满核心线程池,导致自身服务 “假死”。

  • 线程池隔离(推荐,适合高并发场景):

    • 为每个第三方服务单独创建线程池(如 “支付第三方线程池”“短信第三方线程池”),设置核心线程数、最大线程数和队列容量;
    • 当第三方调用超时,仅消耗该线程池的线程,不影响 “订单创建”“用户登录” 等核心线程池;
    • 实现:使用 Spring 的ThreadPoolTaskExecutor或 Resilience4j 的ThreadPoolBulkhead
  • 信号量隔离(轻量,适合低并发场景):

    • 为第三方调用设置最大并发数(如同时最多 100 个请求调用第三方短信接口),超过则直接降级;
    • 优点:无线程切换开销,实现简单;缺点:无法隔离慢请求(信号量释放需等待请求完成)。

事后优化:从 “被动应对” 到 “主动预防”

建立全链路监控,实时感知第三方状态,及时发现第三方服务告警。

8.ArrayList和LinkedList为什么不是线程安全,不安全体现在哪里,该如何解决呢?由此引出线程安全的本质是什么,一般有什么解决思路

为什么不是线程安全的?

核心原因在于:它们的内部方法(如 addremoveset 等)在执行时都不是原子操作(atomic operation),而是由多个步骤组成。在多线程环境下,如果一个线程正在执行这些方法的中间步骤,而另一个线程也来操作同一个集合,就会破坏数据的一致性、完整性和正确性。

可以将“非线程安全”理解为:多个线程同时操作同一个集合对象时,无法保证最终结果的正确性

不安全的具体体现

 ArrayList 的线程不安全体现

ArrayList的不安全主要体现在扩容机制元素赋值两个环节。其add方法简化后的步骤如下:

  1. 检查当前数组容量是否足够 (ensureCapacityInternal)

  2. 如果不够,则进行扩容(创建一个新数组,并将老数组的元素拷贝过去)

  3. 在数组的下一个空位 (size位置) 赋值 elementData[size] = e

  4. size的值增加1 (size++)

问题就出在多个线程可能同时执行这些步骤:

  • 情况一:元素覆盖(丢失)

    • 场景:假设数组当前容量为10,size为9(即已有9个元素,还剩1个空位)。

    • 过程

      1. 线程A执行add,发现容量足够(步骤1),无需扩容。它准备执行步骤3,但在赋值elementData[9] = eA之前被挂起。

      2. 线程B也执行add,同样发现容量足够,成功执行了步骤3:elementData[9] = eB

      3. 接着线程B执行步骤4,将size增加到了10。

      4. 线程A恢复运行,它从挂起处继续,执行步骤3:elementData[9] = eA覆盖了线程B刚刚写入的值eB

      5. 线程A执行步骤4,将size增加到11。

    • 结果size变成了11,但实际只成功添加了10个元素(eBeA覆盖了),导致元素丢失,且size与实际元素数量不符。

  • 情况二:扩容导致的数组越界(ArrayIndexOutOfBoundsException)

    • 场景:数组已满,需要扩容。

    • 过程

      1. 线程A执行add,发现容量不足,触发扩容(步骤2)。假设它创建了一个新数组,容量为原来的1.5倍(比如15),并正在拷贝旧数据。

      2. 此时线程B也执行add,它发现当前的size(比如10)仍然小于elementData.length(旧数组长度10),它并不知道线程A正在扩容,于是它尝试直接赋值elementData[10] = eB。但旧数组的长度只有10,索引10已经越界,于是抛出ArrayIndexOutOfBoundsException

  • 情况三:size++ 的非原子性

    • size++看似一行代码,但实际是三个操作:1. 读取size的值;2. 将值+1;3. 写回size

    • 如果两个线程同时读取到相同的size值(比如都是5),都加1后写回,最终size的结果是6而不是7。这也会导致元素数量统计错误。

LinkedList 的线程不安全体现

LinkedList的不安全主要体现在修改链表结构上。其add方法简化后的步骤(在尾部添加):

  1. 找到当前尾节点last

  2. 创建一个新节点newNode,并将其prev指针指向last

  3. 将新节点newNode设置为新的尾节点 (last = newNode)。

  4. 如果原来的lastnull(即空链表),则将头节点first也指向newNode;否则,将原尾节点lastnext指针指向newNode

问题同样在于多个线程同时修改链表结构:

  • 情况:链表结构破坏

    • 场景:两个线程A和B同时向尾部添加元素。

    • 过程

      1. 线程A和B都找到了当前的尾节点oldLast

      2. 线程A创建了新节点nodeAnodeA.prev = oldLast,然后线程A被挂起。

      3. 线程B执行完了整个过程:创建nodeBnodeB.prev = oldLast,设置last = nodeB,并成功将oldLast.next指向了nodeB

      4. 线程A恢复运行,它继续执行:它仍然认为当前的尾节点是oldLast(但实际已经是nodeB了),于是它设置last = nodeA。然后它尝试将oldLast.next指向nodeA

    • 结果:链表结构被彻底破坏。尾节点last指向了nodeA,而oldLast.next本应指向nodeB,却被改为了指向nodeA。这会导致遍历时丢失数据(nodeB)、出现循环引用甚至无限循环等问题。

如何解决ArrayList和LinkedList线程安全问题

1.使用Collections.synchronizedList()

2.使用CopyOnWriteArrayList

3.自己通过加锁解决

线程安全的本质

线程安全本质就是在多线程环境下,对共享资源(数据)的“读”和“写”操作,不会造成数据的不一致、脏读、幻读或丢失更新等问题,最终保证程序运行结果的正确性。

主要由以下三个核心问题引起的:

原子性(Atomicity)问题

一个或多个操作要么全部执行成功,要么全部不执行,中间不会被任何线程打断。

可见性(Visibility)问题

当一个线程修改了共享变量的值,其他线程能够立即看到修改后的最新值。

有序性(Ordering)问题

程序执行的顺序不一定等于代码编写的顺序。为了优化性能,编译器和处理器可能会对指令进行重排序

通用的解决思路

隔离变量, - “不共享”

核心思想:根本不让资源被多个线程共享,从而从源头上杜绝问题。

局部变量、ThreadLocal

适用场景:需要避免重复创建昂贵对象或需要传递线程上下文信息时。

同步/互斥- “排队用”

核心思想:既然共享无法避免,那就保证同一时刻只有一个线程能访问共享资源。这是最直观、最传统的解决方案。

Synchronized、Lock接口、Volatile关键字

使用线程安全的工具类

JUC包中提供了大量高性能的线程安全容器和工具

不可变 - “只读不写”

核心思想:如果一个对象在创建后其状态就永不改变,那么它天生就是线程安全的,因为所有线程都只能读,不会出现写冲突。

final关键字修饰

适用场景:配置信息、常量、享元对象等所有不需要变化的数据。

9.如何利用MySQL实现分布式锁,包括乐观锁、悲观锁

MySQL分布式锁的设计原则

  1. 原子性:确保锁的获取和释放操作是原子的,以防止竞态条件。
  2. 唯一性:确保每个锁在系统中是唯一的,以便不同的服务实例可以正确地识别和获取锁。
  3. 超时机制:设置锁的超时时间,避免死锁。
  4. 锁的续期:对于长时间运行的操作,可能需要续期锁,以防止锁在操作过程中过期。

使用MySQL实现分布式锁

1.创建锁表

首先,我们需要在MySQL中创建一个用于存储锁信息的表。这个表可以很简单,只包含锁的标识和持有者信息。例如:

	CREATE TABLE `locks` (  

	  `id` INT NOT NULL AUTO_INCREMENT,  

	  `lock_key` VARCHAR(255) NOT NULL,  

	  `lock_value` VARCHAR(255) NOT NULL,  

	  `expire_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  

	  PRIMARY KEY (`id`),  

	  UNIQUE KEY `lock_key_unique` (`lock_key`)  

	);

其中,lock_key用于标识不同的锁,lock_value用于存储锁的持有者信息(如服务实例的ID或唯一标识),expire_time用于存储锁的过期时间。

2.获取锁

获取锁的操作可以通过一条INSERT语句来实现。为了确保原子性,我们可以使用INSERT IGNOREINSERT ... ON DUPLICATE KEY UPDATE语句。以下是一个示例:

	# 尝试插入新锁
    INSERT INTO locks (lock_key, lock_value, expire_time)  
	VALUES ('my_lock_key', 'my_lock_value', NOW() + INTERVAL 30 SECOND)  
    # 若锁已存在,则更新锁信息
	ON DUPLICATE KEY UPDATE  
    -- 仅当锁已过期(expire_time < NOW())时,才更新持有者和过期时间
    lock_value = IF(expire_time < NOW(), VALUES(lock_value), lock_value),  
    expire_time = IF(expire_time < NOW(), VALUES(expire_time), expire_time);  

如果插入成功(即没有产生DUPLICATE KEY错误),则表示成功获取了锁。如果插入失败(即产生了DUPLICATE KEY错误),则表示锁已经被其他服务实例持有。

3. 判断是否成功获取锁

由于MySQL的INSERT ... ON DUPLICATE KEY UPDATE语句本身不会返回受影响的行数(因为即使有DUPLICATE KEY错误,也会更新一行),我们需要通过其他方式来判断是否成功获取了锁。一种常见的方法是使用SELECT ... FOR UPDATE语句来查询锁的状态,并检查lock_value字段的值。但是,这种方法可能会产生额外的开销,并且可能导致死锁。

一个更好的方法是在应用程序层面进行判断。具体来说,我们可以将INSERT语句放在一个事务中,并检查事务是否成功提交。如果事务成功提交,则表示成功获取了锁;否则,表示获取锁失败。

4. 释放锁

释放锁的操作可以通过DELETE语句来实现:

DELETE FROM locks WHERE lock_key = 'my_lock_key' AND lock_value = 'my_lock_value';
5. 锁的续期

对于长时间运行的操作,我们可能需要续期锁以防止其过期。这可以通过更新expire_time字段来实现:

UPDATE locks SET expire_time = NOW() + INTERVAL 30 SECOND WHERE lock_key = 'my_lock_key' AND lock_value = 'my_lock_value';

10.Java中的泛型,核心作用、底层原理

泛型是什么

在 Java 中,泛型(Generics)是 JDK 5 引入的核心特性,它允许在定义类、接口、方法时使用类型参数(Type Parameter),并在使用时指定具体类型。

泛型的本质是 “参数化类型”—— 即把类型作为参数传递,让类、接口或方法可以操作 “不确定的类型”,在使用时再明确具体类型。

核心作用

编译时类型安全检查

泛型可以在编译阶段强制检查 “数据类型是否匹配”,避免运行时出现ClassCastException

  • 没有泛型时,集合可以存储任意类型,取出时需要强制转换,容易出错:
    ArrayList list = new ArrayList();
    list.add("hello");
    list.add(123); // 编译不报错(允许添加任意类型)
    String s = (String) list.get(1); // 运行时报错:Integer不能转String
  • 有泛型时,编译器会提前拦截错误,从源头避免类型转换问题。

消除强制类型转换

泛型允许编译器在编译时自动推断类型,使用时无需手动转换,简化代码。

ArrayList<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 无需强制转换,编译器自动确认类型

提高代码复用性

泛型可以让类 / 方法适配多种类型,无需为每种类型重复编写逻辑。

底层原理:类型擦除(Type Erasure)

Java 的泛型是 "伪泛型"—— 它只在编译阶段有效,在运行时会被 “擦除”,不会保留泛型参数的具体类型。这种设计是为了兼容 JDK 5 之前的非泛型代码(向后兼容)。

1. 类型擦除的过程

编译时,编译器会将泛型参数替换为其 “上限类型”(如果未指定上限,默认替换为Object),并生成相应的字节码。

// 定义泛型类
class Box<T> {
    private T value;
    public T getValue() { return value; }
    public void setValue(T value) { this.value = value; }
}

// 编译后(类型擦除):T被替换为Object
class Box {
    private Object value;
    public Object getValue() { return value; }
    public void setValue(Object value) { this.value = value; }
}

// 定义带上限的泛型类(T必须是Number的子类)
class NumberBox<T extends Number> {
    private T value;
    public T getValue() { return value; }
}

// 编译后(类型擦除):T被替换为上限Number
class NumberBox {
    private Number value;
    public Number getValue() { return value; }
}
2. 类型擦除后的 “补偿机制”

类型擦除会导致泛型信息丢失,为了保证代码正确性,编译器会自动生成 “桥接方法”(Bridge Method)和隐式类型转换

  • 桥接方法:解决泛型擦除后的多态问题

当子类重写泛型父类的方法时,类型擦除可能导致方法签名不匹配,编译器会生成桥接方法维持多态性。

// 泛型父类
class Parent<T> {
    public void set(T t) { ... }
}

// 子类指定具体类型
class Child extends Parent<String> {
    @Override
    public void set(String s) { ... } // 实际重写的方法
}

// 类型擦除后,父类的set方法变为set(Object)
// 编译器会为Child生成桥接方法,保证多态调用:
class Child extends Parent {
    // 桥接方法(由编译器自动生成)
    public void set(Object obj) {
        set((String) obj); // 调用实际的set(String)方法
    }
    
    public void set(String s) { ... } // 子类自己的方法
}
  • 隐式类型转换:使用泛型时自动插入转换代码

虽然泛型被擦除为Object,但编译器会在取值时自动添加类型转换,保证使用时的类型正确:

ArrayList<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 编译后会自动插入 (String) 转换

11.跳表里的一个数排第几,怎么算的?请从底层原理的角度说一说

跳表的核心结构

跳表(Skip List)是一种 “多层有序链表”,其结构有两个关键特征,直接决定了排名计算的逻辑:

1.分层设计

  • 跳表由若干层(Level)组成,记为 Level 0, Level 1, ..., Level h(h 为最高层索引的高度)。
  • 底层(Level 0):是一个完整的有序链表,包含跳表中所有元素,这是排名的 “基准链表”(元素的真实顺序和位置只看 Level 0)。
  • 上层(Level ≥ 1):是底层链表的稀疏索引,层数越高,索引越稀疏(节点越少),作用是快速 “跳过” 大量无关元素,提升查询效率。

2.节点的核心字段
跳表的每个节点(Node)除了存储value(元素值)和next(指向同层下一个节点的指针),还必须包含一个 span(跨度) 字段 —— 这是计算排名的 “关键钥匙”。

  • 跨度(Span)定义:当前节点的span值 = 从当前节点到同层下一个节点之间,底层链表(Level 0)包含的元素个数(包括下一个节点本身)。
  • 例:若 Level 1 中节点 A 的next指向节点 B,且 A 到 B 在 Level 0 中包含 5 个元素(A 的下一个、下下一个…… 直到 B),则 A 的span为 5。

排名计算的底层原理:“顶层优先,累加跨度”

跳表的排名查询遵循 **“从顶层到低层,优先横向跳,跳不动则降级”** 的逻辑,核心动作是累加经过的节点跨度。最终累加的总跨度,就是目标元素在底层链表中的排名。

核心逻辑拆解(以 “查询元素 x 的排名” 为例)

假设跳表的最高层为Level h,有一个头节点(Head Node) ,头节点在每一层都存在(不存储实际元素,仅作为起始入口)。

1.初始化状态

  • 当前节点 current = 跳表的头节点(从最高层Level h开始);
  • 排名计数器 rank = 0(累加的跨度总和,初始为 0);
  • 当前层级 level = 最高层h

2.逐层遍历与跨度累加(核心步骤)
从最高层开始,循环执行以下逻辑,直到降到底层(Level 0):

  • Step 1:尝试横向移动
    查看当前节点currentLevel level的下一个节点next_node
    • next_node存在,且next_node.value < x(下一个节点的值比目标小,说明目标在next_node之后):
      • 累加跨度:rank += current.span(把当前节点到next_node的 “底层元素个数” 加入排名);
      • 移动当前节点:current = next_node(横向跳到next_node,继续在当前层尝试跳转)。
    • next_node不存在,或next_node.value ≥ x(下一个节点的值大于等于目标,说明当前层无法再横向跳):
      • 不移动、不累加,直接降一层level -= 1

3.底层确认与最终排名
当降到Level 0(底层完整链表)后,还需最后一步:

  • 此时current在 Level 0 中,其下一个节点next_node若恰好是x(找到目标元素),则最终排名 = rank + 1(因为rank是到current的累加跨度,xcurrent之后第一个位置,需 + 1)。
  • (若next_node不是x,则 x 不在跳表中,排名不存在)。

Kubernetes有了解过吗

Kubernetes(简称 k8s)是目前最流行的容器编排平台,用于自动化容器的部署、扩展、管理和运维。它的核心价值在于解决了容器化应用在大规模部署时的复杂性(如服务发现、负载均衡、自愈、滚动更新等)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小猹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值