20~30K * 15 薪,可惜挂了

猫眼娱乐演出业务 Java 岗
今天分享组织内部的朋友在猫眼的 Java 一面,薪资开的挺高的有 2 到 3 个 w,可惜没发挥好,在一面就挂了,下面看看他分享的面经吧:
面经详解
1. mysql 什么情况下会产生死锁?在代码设计层面上如何避免死锁
1. 循环锁等待(最常见)
场景:两个事务互相持有对方需要的锁,形成循环等待。
示例:
事务 A:先锁表 A,再尝试锁表 B。
事务 B:先锁表 B,再尝试锁表 A。
结果:双方互相等待对方释放锁,导致死锁。
2. 锁的粒度冲突
行锁与表锁冲突:例如,事务 A 锁定一行数据,事务 B 尝试锁定全表。
间隙锁冲突(在可重复读隔离级别下):插入数据时因间隙锁互相等待。
3. 索引问题
无索引查询:全表扫描会锁定大量数据,增加死锁风险。
索引失效:导致锁范围过大(如扫描多个索引页)。
在代码设计层面上避免死锁,需通过破坏死锁的必要条件(互斥、请求与保持、不可剥夺、循环等待)来实现。
一、固定锁顺序
通过强制所有线程以相同的顺序获取锁,可避免循环等待条件。
实现方式:
为所有共享资源定义全局获取顺序(例如按资源 ID 升序)。
确保每个线程在请求多个锁时遵循该顺序。
优势:简单高效,适用于资源数量可控的场景。
二、超时与中断机制
通过设置锁获取的超时时间,避免无限等待。
实现方式:使用
tryLock
方法替代传统锁机制,结合超时和中断策略。适用场景:高并发环境下需快速失败并重试的情况[7][13]。
三、资源预分配
一次性获取所有所需资源,避免持有资源时请求其他资源。
实现方式:
在业务逻辑开始前,预先申请所有可能需要的资源。
若无法一次性获取,则释放已占资源并重试。
常用于数据库事务设计。
四、缩小锁粒度与作用域
减少锁的持有时间和范围,降低冲突概率。
实现方式:
仅对共享数据的关键部分加锁,避免全局锁。
使用细粒度锁(如分段锁)代替粗粒度锁。
优势:提升并发性能,减少死锁风险。
五、使用高级并发工具
利用现成并发库减少手动锁管理。
推荐工具:
Java 并发包:如
Semaphore
、CountDownLatch
、ConcurrentHashMap
。无锁数据结构:如原子类(
AtomicInteger
)和 CAS 操作。优势:工具内部已优化锁机制,降低开发复杂度[1][6]。
六、检测与恢复机制
在代码中集成死锁检测逻辑,必要时强制释放资源。
实现方式:
定期检查线程等待链是否形成环路。
检测到死锁后,强制释放部分资源或终止线程。
适用场景:复杂系统需容错处理时。
2. 幻读?mvcc 机制是如何避免幻读的?锁机制又是如何避免幻读的?
幻读指的是在同一个事务中,两次相同的查询返回了不同的数据集行数。
一、MVCC 如何避免快照读的幻读
快照读与 ReadView 机制
在可重复读(RR)隔离级别下,事务启动时生成一个全局一致的 ReadView(数据快照),后续所有普通 SELECT 操作均基于该快照,而非实时数据[6][9]。
ReadView 记录事务启动时活跃事务的 ID 集合(trx_ids)、最小活跃事务 ID(up_limit_id)和最大事务 ID(low_limit_id),用于判断数据版本的可见性。
可见性规则
当读取某行数据时,MVCC 会遍历 Undo Log 中的版本链,根据以下规则判断版本是否可见:
若数据版本的事务 ID 小于 up_limit_id(即已提交),则可见;
若事务 ID 在 trx_ids 中(即未提交),则不可见;
若事务 ID 大于 low_limit_id(即事务启动后新生成的事务),则不可见[9][5]。
通过该规则,事务只能看到启动前已提交的数据,或自身修改的数据,因此其他事务插入的新行(即使已提交)也不会出现在快照中,从而避免幻读[6][8]。
二、MVCC 与间隙锁协同解决当前读的幻读
当前读的局限性
当前读(如 SELECT ... FOR UPDATE)需要获取最新数据并加锁,此时仅依赖 MVCC 无法避免幻读。
示例:事务 A 执行范围查询并加锁,事务 B 若在范围内插入新数据,可能导致事务 A 再次查询时出现幻读。
间隙锁的作用
InnoDB 在当前读操作中引入间隙锁(Gap Lock),锁定索引记录之间的间隙,阻止其他事务在范围内插入新数据。
例如,事务 A 执行 SELECT * FROM t WHERE id > 10 FOR UPDATE 时,会锁住 id > 10 的间隙,事务 B 插入 id=15 的操作将被阻塞。
三、MVCC 的底层实现
版本链与 Undo Log
每行数据包含隐藏字段 trx_id(最后修改的事务 ID)和 roll_pointer(指向旧版本的 Undo Log 指针)。
更新操作会生成新版本,旧版本存入 Undo Log 形成版本链,供快照读回溯历史数据[9][10]。
事务 ID 与隔离级别
在 RR 级别下,事务首次快照读时生成 ReadView 并复用至事务结束;而在 RC 级别下,每次快照读均生成新 ReadView,导致可能读到新提交的数据。
四、MVCC 的局限性
无法完全消除幻读的场景
混合使用快照读与当前读时,若当前读操作修改了其他事务插入的新数据,新数据的版本会更新为当前事务 ID,导致后续快照读可见该行。
示例:事务 A 快照读后,事务 B 插入新行并提交;事务 A 通过当前读修改该行,则后续快照读会看到该行。
完全避免幻读的解决方案
使用串行化(SERIALIZABLE)隔离级别,强制所有操作串行执行;
在 RR 级别下,对关键范围查询显式加锁(如 SELECT ... FOR UPDATE)。
TIPS:时间充裕的话可以读一下这篇文章
3. redis 分布式锁如何进行锁重入,如何做锁预期
Redis 分布式锁实现锁重入和锁续期(即"锁预期",通常指锁的超时自动续期)主要依赖特定的数据结构和后台监控机制
一、锁重入的实现
锁重入指同一线程多次获取同一把锁时无需阻塞,通过计数机制实现。Redisson 的核心方案如下:
数据结构设计
使用 Redis 的 Hash 结构存储锁信息:
Key:锁名称(如
myLock
)Field:线程唯一标识(格式为
UUID + 线程ID
,如b9834f1e-Thread-1
)Value:重入次数(计数器)
例如:
HSET myLock b9834f1e-Thread-1 2
表示该线程已重入 2 次。加锁逻辑(Lua 脚本保证原子性)
首次加锁:若 Hash 不存在,则创建并设置计数器为 1,同时设置锁超时时间。
重入加锁:若 Hash 中已存在当前线程的 Field,则将计数器递增 1,并刷新超时时间。
解锁逻辑
每次解锁将计数器减 1,计数器归零时删除 Key 释放锁:
二、锁续期机制(WatchDog 看门狗)
锁续期用于解决业务执行时间超过锁超时时间的问题,防止锁提前释放导致数据不一致。
自动续期原理
默认行为:未显式设置
leaseTime
(锁超时时间)时,Redisson 启动 WatchDog 线程。续期规则:
每隔
锁超时时间 / 3
(默认 10 秒)检查锁是否仍被持有。若持有则通过
pexpire
命令将锁超时时间重置为初始值(默认 30 秒)。终止条件:锁被释放或显式设置了
leaseTime
。配置参数
锁超时时间:通过
Config.lockWatchdogTimeout
修改(默认 30,000 毫秒)。显式设置超时:指定
leaseTime
参数后,WatchDog 不生效,锁到期自动释放:续期流程
graph TD
A[业务线程持有锁] --> B[WatchDog 启动]
B --> C{是否仍持有锁?}
C -- 是 --> D[pexpire 重置为 30 秒]
C -- 否 --> E[停止续期]
D --> C -- 每 10 秒循环检查
三、关键注意事项
锁标识安全
必须使用
UUID+线程ID
组合作为标识,避免不同机器或线程冲突。避免死锁
显式设置
leaseTime
时需确保业务能在超时前完成,否则锁自动释放可能导致并发问题。集群环境
Redisson 的红锁(RedLock) 需跨多个 Redis 节点加锁,半数以上成功才算获取锁,避免单点故障。
4. 守护线程如何保证执行任务状态和数据库任务状态的一致性?守护线程是全局唯一的嘛?如果不唯一,如何避免多线程并发翻转问题?如果是唯一的是如何部署的?
一、守护线程的任务状态一致性保障
事务与补偿机制
原子操作:守护线程执行任务时,需将任务状态更新(如“执行中”→“完成”)与数据库操作置于同一事务中,确保失败时状态回滚。
补偿日志:记录任务关键步骤到独立日志表,异常时通过定时任务扫描日志进行状态修复(如重试或标记失败)。
超时回滚:设置任务最大执行时长,超时自动触发状态回滚,避免僵尸任务。
状态同步设计
双写校验:任务开始前从数据库加载状态到内存,执行中定期比对内存与数据库状态,差异时触发告警并终止任务。
版本号控制:为任务记录添加版本号字段,更新状态时校验版本号,防止并发覆盖(乐观锁)。
二、守护线程的唯一性与部署模式
全局唯一场景
单例模式部署:通过进程锁(如文件锁或端口绑定)确保单节点唯一性,例如 K8s 的
StatefulSet
配合ReadinessProbe
检测。选举机制:集群中使用 ZooKeeper/Etcd 实现 Leader 选举,仅 Leader 节点运行守护线程,故障时自动切换。
非唯一场景(多实例)的并发控制
分布式锁协调:
使用 Redis 红锁(RedLock)或数据库行锁,在任务获取阶段加锁,确保同一任务仅被一个线程处理。
任务分片:按任务 ID 哈希分片,不同实例处理不同分片,避免重叠(如
ShardingKey= task_id % instance_count
)。数据库唯一约束:在任务表中为
(task_id, status)
组合设置唯一索引,防止并发插入重复任务。
三、避免并发状态翻转的关键措施
状态机设计
定义任务状态流转规则(如
待处理→执行中→完成/失败
),禁止非法跳转(如“完成”不可回退至“执行中”)。更新状态时校验前置状态:
CAS(Compare-And-Set)操作
更新状态时附带旧值校验,确保并发安全:
四、部署实践建议
唯一性部署
容器化:通过 K8s 的
StatefulSet
+PodDisruptionBudget
保障单实例高可用。守护进程:Linux 系统使用
systemd
配置Restart=always
,崩溃后自动重启。多实例部署
负载均衡:结合 Nginx 或 Spring Cloud Gateway 分发请求,后台任务由独立微服务处理,通过消息队列(如 Kafka)触发。
心跳上报:实例定期向数据库写入心跳时间,中心调度器分配任务时排除失活实例。
总结
一致性核心:通过事务 + 补偿日志 + 状态机设计,确保任务状态与数据库强一致。
唯一性选择:
单实例:简单可靠,适合轻量任务;
多实例:扩展性强,需依赖分布式锁或分片。
并发规避:乐观锁(版本号/CAS)和分布式锁是解决多线程翻转的关键。
部署建议:单实例用进程锁/选举,多实例结合分片和心跳检测。 实际架构需根据任务量级与容错要求权衡设计。
5. 守护线程如果挂了,怎么能快速感知到?
我们处理守护线程健康状态主要靠三个手段。第一是埋点心跳检测,比如有个负责清理 Redis 过期数据的守护线程,我们让它每分钟往数据库写一条心跳记录,如果连续 3 分钟没更新,监控系统就会触发电话告警。第二是异常日志捕获,像订单对账的守护线程,核心逻辑用 try-catch 包裹,一旦抛异常就通过 Kafka 发送错误事件到监控大屏,5 秒内就能在 Grafana 看到红色预警。第三是线程池托管,比如用 Java 的 ScheduledExecutorService 管理定时任务线程,通过覆写 afterExecute 钩子函数,只要线程异常退出就立刻回调通知服务治理中间件,触发自动重启。
不过实际遇到过坑:有个监控 ES 集群的守护线程因为 OOM 挂了,但没触发任何报警。后来发现是心跳检测代码写在业务逻辑之后,线程崩溃时根本没执行到心跳写入。后来改成用 finally 块写心跳,就算崩溃也能执行。现在我们会给关键守护线程配"双保险"——既在代码里埋健康检查,又通过 K8s 的 liveness 探针从外部监控进程存活状态,这样即便代码层漏报,基础设施层也能兜底。
1. job 的操作是如何幂等的
一、状态机与流程管控
通过状态流转约束,防止重复执行:
状态机设计
定义明确的 Job 状态流(如 待执行 → 执行中 → 成功/失败),禁止非法状态跳转(如“成功”状态不可再次触发执行)。
更新状态时校验前置状态(SQL 示例):
UPDATE jobs SET status = '执行中'
WHERE job_id = 123 AND status = '待执行'; -- 仅当当前状态符合预期才更新
优点:天然避免重复调度,适用于定时任务场景。
原子状态更新
采用数据库事务保证状态更新与业务逻辑的原子性,失败时自动回滚
二、分布式锁协调
控制多节点并发,确保全局唯一执行:
Redis 分布式锁
任务触发时尝试获取锁(Key=任务 ID),锁有效期覆盖任务执行周期。
实现逻辑(Redisson 示例):
RLock lock = redisson.getLock("job_lock_" + jobId);
if (lock.tryLock(0, 30, TimeUnit.SECONDS)) { // 非阻塞获取锁
try {
// 执行业务逻辑
} finally { lock.unlock(); }
}
适用场景:集群环境下多实例竞争执行。
数据库唯一索引
为任务执行记录表添加 (job_id, status) 组合唯一索引,插入重复记录时报错拦截。
三、版本号/Token 机制
标识请求唯一性,拦截重复提交:
Token 方案
调用方申请 Token(如 UUID),Job 服务缓存 Token 至 Redis(有效期 5 分钟)。
执行前校验 Token 是否存在,存在则执行业务并删除 Token;不存在则拒绝。
乐观锁版本号
任务记录携带版本号字段,执行前校验版本一致性:
UPDATE jobs SET result = #{data}, version = version + 1
WHERE job_id = 123 AND version = #{oldVersion};
版本冲突时自动放弃执行。
四、业务层幂等设计
针对数据处理逻辑的防护:
唯一键约束
业务表设置唯一索引(如订单号+操作类型),重复写入时触发数据库报错。
增量更新
使用 UPDATE ... SET count = count + 1 代替先查询后更新,避免并发计数错误。
日志追踪
记录操作流水(如操作 ID、时间戳),执行前查询流水表校验是否已处理。
五、失败补偿与熔断
异常场景下的兜底策略:
Misfire 机制
若任务执行超时,标记为 misfire 状态,由独立线程延迟补偿执行(如 ElasticJob)。
死信队列
重试 N 次仍失败的任务转入死信队列,人工或定时 Job 扫描处理。
熔断降级
监控失败率(如 10 分钟内失败 50%),自动暂停调度并告警(Hystrix/Sentinel)。
欢迎关注 ❤
我们搞了一个免费的面试真题共享群,互通有无,一起刷题进步。
没准能让你能刷到自己意向公司的最新面试题呢。
感兴趣的朋友们可以加我微信:wangzhongyang1993,备注:面试群。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/1575b7b8e594b968b7f6b26c8】。文章转载请联系作者。
评论