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 ID 和API 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. 第三方合作方在网关注册,获取唯一APPID
和APP Secret
;
2. 调用时携带APPID
,并通过APP Secret
生成请求签名(如 HMAC-SHA256,包含时间戳防重放);
3. 网关验证签名有效性。
适用场景:外部合作方调用内部 API(如第三方物流服务调用电商的订单接口),无需用户身份,仅验证 “合作方身份”。
双向 TLS(mTLS)
原理:与微服务间 mTLS 原理一致:客户端(如设备、第三方服务)需提供证书,网关验证证书合法性后才允许请求。
4.详细说说OAuth框架
OAuth 是一个开放标准的授权框架,全称为 “Open Authorization”。它的核心作用是:允许第三方应用(如小程序、第三方网站)在不获取用户账号密码的情况下,安全地访问用户在某服务(如微信、GitHub)上的受保护资源(如用户信息、相册、订单等)。
一、OAuth 解决的核心问题
场景:你用 “小红书” App 时,选择 “用微信登录”,并允许小红书获取你的微信昵称和头像。这里的关键需求是:
- 小红书需要访问你的微信用户信息(资源);
- 你不想把微信的账号密码告诉小红书(安全需求);
- 微信需要确认 “你允许小红书访问这些信息”(授权需求)。
OAuth 就是为这类场景设计的:它通过一套标准化流程,让第三方应用在用户明确授权的前提下,合法访问用户资源,同时全程不接触用户的核心凭证(如密码)。
二、OAuth 的关键角色
理解 OAuth 流程,首先要明确四个核心角色:
- 资源所有者(Resource Owner):通常是用户,拥有被访问的资源(如你的微信账号信息)。
- 客户端(Client):第三方应用,需要访问用户资源(如小红书 App)。
- 资源服务器(Resource Server):存储用户资源的服务(如微信的用户信息服务器,负责返回用户昵称、头像)。
- 授权服务器(Authorization Server):验证用户身份并颁发 “授权凭证” 的服务(如微信的授权服务器,负责处理用户授权并生成令牌)。
三、OAuth 2.0(主流版本)的核心流程
目前广泛使用的是 OAuth 2.0(OAuth 1.0 因复杂已很少用),其核心是 “通过令牌(Token)实现授权”。最常用的是授权码模式(安全级别最高,适用于有服务器的客户端,如 Web 应用、App),流程如下:
授权码模式步骤(以 “小红书用微信登录” 为例):
-
用户触发授权:
你在小红书点击 “微信登录”,小红书(客户端)会跳转到微信的授权页面(由微信的授权服务器提供),并携带参数:client_id
:小红书在微信开放平台注册的唯一标识(证明 “我是小红书”);redirect_uri
:授权完成后跳转回小红书的地址(如https://xiaohongshu.com/callback
);scope
:请求的权限范围(如scope=userinfo
,表示需要访问用户基本信息);response_type=code
:声明使用 “授权码模式”,需要返回授权码。
-
用户确认授权:
你在微信授权页面输入微信账号密码(仅微信可见,小红书看不到),并点击 “允许”(确认授权小红书获取你的昵称和头像)。 -
授权服务器返回授权码:
微信授权服务器验证通过后,会跳转到redirect_uri
(小红书的回调地址),并在 URL 后附加一个授权码(code),如:
https://xiaohongshu.com/callback?code=abc123
(code 是临时的,通常 5 分钟内有效)。 -
客户端用授权码换令牌:
小红书的服务器(后端)收到code=abc123
后,会向微信的授权服务器发送请求,携带:code=abc123
(刚获取的授权码);client_id
(小红书的标识);client_secret
(小红书在微信注册时的密钥,证明 “我确实是小红书”,此参数仅在后端传输,不暴露给前端);grant_type=authorization_code
(声明用授权码换令牌)。
-
授权服务器颁发令牌:
微信授权服务器验证code
和client_secret
无误后,返回两个关键令牌:- 访问令牌(access_token):用于访问资源服务器的凭证(如 “允许用这个令牌获取用户信息”);
- 刷新令牌(refresh_token):当访问令牌过期时,用它获取新的访问令牌(避免再次让用户授权)。
示例返回:
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "def456...", "expires_in": 3600, // 访问令牌有效期(1小时) "scope": "userinfo" }
-
客户端用访问令牌访问资源:
小红书服务器携带access_token
向微信的资源服务器请求你的信息:
GET https://api.weixin.qq.com/userinfo?access_token=eyJhbGciOiJIUzI1Ni...
资源服务器验证令牌有效后,返回你的昵称、头像等信息。 -
令牌过期时刷新:
当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 全文索引(适合全文模糊):
对文本字段(如title
、content
)建立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.name
和b.value
,无需关联c
表(即使b
和c
有关联)。 -
用子查询或临时表预过滤数据:
先通过子查询过滤出小数据集,再关联其他表,减少关联的数据量:-- 先过滤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
)包含基本信息(id
、name
、phone
)和详情信息(address
、birth
、avatar
),可拆分为users_base
(基本信息)和users_ext
(详情信息)。 - 日常查询用户基本信息时,只需查
users_base
,无需关联;需详情时再关联users_ext
,减少高频查询的关联次数。
3. 水平拆分(按数据量拆表)
当单表数据量过大(如超过 1000 万行),即使有索引,查询和关联效率也会下降,需按规则拆分:
- 范围拆分:按时间(如
orders_2023
、orders_2024
)或 ID 范围(如users_0
、users_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为什么不是线程安全,不安全体现在哪里,该如何解决呢?由此引出线程安全的本质是什么,一般有什么解决思路
为什么不是线程安全的?
核心原因在于:它们的内部方法(如 add
, remove
, set
等)在执行时都不是原子操作(atomic operation),而是由多个步骤组成。在多线程环境下,如果一个线程正在执行这些方法的中间步骤,而另一个线程也来操作同一个集合,就会破坏数据的一致性、完整性和正确性。
可以将“非线程安全”理解为:多个线程同时操作同一个集合对象时,无法保证最终结果的正确性。
不安全的具体体现
ArrayList 的线程不安全体现
ArrayList
的不安全主要体现在扩容机制和元素赋值两个环节。其add
方法简化后的步骤如下:
-
检查当前数组容量是否足够 (
ensureCapacityInternal
) -
如果不够,则进行扩容(创建一个新数组,并将老数组的元素拷贝过去)
-
在数组的下一个空位 (
size
位置) 赋值elementData[size] = e
-
将
size
的值增加1 (size++
)
问题就出在多个线程可能同时执行这些步骤:
-
情况一:元素覆盖(丢失)
-
场景:假设数组当前容量为10,
size
为9(即已有9个元素,还剩1个空位)。 -
过程:
-
线程A执行
add
,发现容量足够(步骤1),无需扩容。它准备执行步骤3,但在赋值elementData[9] = eA
之前被挂起。 -
线程B也执行
add
,同样发现容量足够,成功执行了步骤3:elementData[9] = eB
。 -
接着线程B执行步骤4,将
size
增加到了10。 -
线程A恢复运行,它从挂起处继续,执行步骤3:
elementData[9] = eA
,覆盖了线程B刚刚写入的值eB
。 -
线程A执行步骤4,将
size
增加到11。
-
-
结果:
size
变成了11,但实际只成功添加了10个元素(eB
被eA
覆盖了),导致元素丢失,且size
与实际元素数量不符。
-
-
情况二:扩容导致的数组越界(ArrayIndexOutOfBoundsException)
-
场景:数组已满,需要扩容。
-
过程:
-
线程A执行
add
,发现容量不足,触发扩容(步骤2)。假设它创建了一个新数组,容量为原来的1.5倍(比如15),并正在拷贝旧数据。 -
此时线程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
方法简化后的步骤(在尾部添加):
-
找到当前尾节点
last
。 -
创建一个新节点
newNode
,并将其prev
指针指向last
。 -
将新节点
newNode
设置为新的尾节点 (last = newNode
)。 -
如果原来的
last
是null
(即空链表),则将头节点first
也指向newNode
;否则,将原尾节点last
的next
指针指向newNode
。
问题同样在于多个线程同时修改链表结构:
-
情况:链表结构破坏
-
场景:两个线程A和B同时向尾部添加元素。
-
过程:
-
线程A和B都找到了当前的尾节点
oldLast
。 -
线程A创建了新节点
nodeA
,nodeA.prev = oldLast
,然后线程A被挂起。 -
线程B执行完了整个过程:创建
nodeB
,nodeB.prev = oldLast
,设置last = nodeB
,并成功将oldLast.next
指向了nodeB
。 -
线程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分布式锁的设计原则
- 原子性:确保锁的获取和释放操作是原子的,以防止竞态条件。
- 唯一性:确保每个锁在系统中是唯一的,以便不同的服务实例可以正确地识别和获取锁。
- 超时机制:设置锁的超时时间,避免死锁。
- 锁的续期:对于长时间运行的操作,可能需要续期锁,以防止锁在操作过程中过期。
使用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 IGNORE
或INSERT ... 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:尝试横向移动
查看当前节点current
在Level 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
的累加跨度,x
在current
之后第一个位置,需 + 1)。 - (若
next_node
不是x
,则 x 不在跳表中,排名不存在)。
Kubernetes有了解过吗
Kubernetes(简称 k8s)是目前最流行的容器编排平台,用于自动化容器的部署、扩展、管理和运维。它的核心价值在于解决了容器化应用在大规模部署时的复杂性(如服务发现、负载均衡、自愈、滚动更新等)。