写点什么

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

作者:王中阳Go
  • 2025-06-26
    北京
  • 本文字数:5473 字

    阅读完需:约 18 分钟

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 并发包:如 SemaphoreCountDownLatchConcurrentHashMap

  • 无锁数据结构:如原子类(AtomicInteger)和 CAS 操作。

  • 优势:工具内部已优化锁机制,降低开发复杂度[1][6]。


六、检测与恢复机制


在代码中集成死锁检测逻辑,必要时强制释放资源。


  • 实现方式:

  • 定期检查线程等待链是否形成环路。

  • 检测到死锁后,强制释放部分资源或终止线程。

  • 适用场景:复杂系统需容错处理时。

2. 幻读?mvcc 机制是如何避免幻读的?锁机制又是如何避免幻读的?

幻读指的是在同一个事务中,两次相同的查询返回了不同的数据集行数。

一、MVCC 如何避免快照读的幻读

  1. 快照读与 ReadView 机制


  • 在可重复读(RR)隔离级别下,事务启动时生成一个全局一致的 ReadView(数据快照),后续所有普通 SELECT 操作均基于该快照,而非实时数据[6][9]。

  • ReadView 记录事务启动时活跃事务的 ID 集合(trx_ids)、最小活跃事务 ID(up_limit_id)和最大事务 ID(low_limit_id),用于判断数据版本的可见性。


  1. 可见性规则


  • 当读取某行数据时,MVCC 会遍历 Undo Log 中的版本链,根据以下规则判断版本是否可见:

  • 若数据版本的事务 ID 小于 up_limit_id(即已提交),则可见;

  • 若事务 ID 在 trx_ids 中(即未提交),则不可见;

  • 若事务 ID 大于 low_limit_id(即事务启动后新生成的事务),则不可见[9][5]。

  • 通过该规则,事务只能看到启动前已提交的数据,或自身修改的数据,因此其他事务插入的新行(即使已提交)也不会出现在快照中,从而避免幻读[6][8]。

二、MVCC 与间隙锁协同解决当前读的幻读

  1. 当前读的局限性


  • 当前读(如 SELECT ... FOR UPDATE)需要获取最新数据并加锁,此时仅依赖 MVCC 无法避免幻读。

  • 示例:事务 A 执行范围查询并加锁,事务 B 若在范围内插入新数据,可能导致事务 A 再次查询时出现幻读。


  1. 间隙锁的作用


  • InnoDB 在当前读操作中引入间隙锁(Gap Lock),锁定索引记录之间的间隙,阻止其他事务在范围内插入新数据。

  • 例如,事务 A 执行 SELECT * FROM t WHERE id > 10 FOR UPDATE 时,会锁住 id > 10 的间隙,事务 B 插入 id=15 的操作将被阻塞。

三、MVCC 的底层实现

  1. 版本链与 Undo Log


  • 每行数据包含隐藏字段 trx_id(最后修改的事务 ID)和 roll_pointer(指向旧版本的 Undo Log 指针)。

  • 更新操作会生成新版本,旧版本存入 Undo Log 形成版本链,供快照读回溯历史数据[9][10]。


  1. 事务 ID 与隔离级别


  • 在 RR 级别下,事务首次快照读时生成 ReadView 并复用至事务结束;而在 RC 级别下,每次快照读均生成新 ReadView,导致可能读到新提交的数据。

四、MVCC 的局限性

  1. 无法完全消除幻读的场景


  • 混合使用快照读与当前读时,若当前读操作修改了其他事务插入的新数据,新数据的版本会更新为当前事务 ID,导致后续快照读可见该行。

  • 示例:事务 A 快照读后,事务 B 插入新行并提交;事务 A 通过当前读修改该行,则后续快照读会看到该行。


  1. 完全避免幻读的解决方案


  • 使用串行化(SERIALIZABLE)隔离级别,强制所有操作串行执行;

  • 在 RR 级别下,对关键范围查询显式加锁(如 SELECT ... FOR UPDATE)。


TIPS:时间充裕的话可以读一下这篇文章

3. redis 分布式锁如何进行锁重入,如何做锁预期

Redis 分布式锁实现锁重入和锁续期(即"锁预期",通常指锁的超时自动续期)主要依赖特定的数据结构和后台监控机制

一、锁重入的实现

锁重入指同一线程多次获取同一把锁时无需阻塞,通过计数机制实现。Redisson 的核心方案如下:


  1. 数据结构设计

  2. 使用 Redis 的 Hash 结构存储锁信息:

  3. Key:锁名称(如myLock

  4. Field:线程唯一标识(格式为UUID + 线程ID,如b9834f1e-Thread-1

  5. Value:重入次数(计数器)

  6. 例如:HSET myLock b9834f1e-Thread-1 2 表示该线程已重入 2 次。

  7. 加锁逻辑(Lua 脚本保证原子性)

  8. 首次加锁:若 Hash 不存在,则创建并设置计数器为 1,同时设置锁超时时间。

  9. 重入加锁:若 Hash 中已存在当前线程的 Field,则将计数器递增 1,并刷新超时时间。

  10. 解锁逻辑

  11. 每次解锁将计数器减 1,计数器归零时删除 Key 释放锁:

二、锁续期机制(WatchDog 看门狗)

锁续期用于解决业务执行时间超过锁超时时间的问题,防止锁提前释放导致数据不一致。


  1. 自动续期原理

  2. 默认行为:未显式设置leaseTime(锁超时时间)时,Redisson 启动 WatchDog 线程。

  3. 续期规则:

  4. 每隔锁超时时间 / 3(默认 10 秒)检查锁是否仍被持有。

  5. 若持有则通过pexpire命令将锁超时时间重置为初始值(默认 30 秒)。

  6. 终止条件:锁被释放或显式设置了leaseTime

  7. 配置参数

  8. 锁超时时间:通过Config.lockWatchdogTimeout修改(默认 30,000 毫秒)。

  9. 显式设置超时:指定leaseTime参数后,WatchDog 不生效,锁到期自动释放:

  10. 续期流程

  11. graph TD

  12. ​ A[业务线程持有锁] --> B[WatchDog 启动]

  13. ​ B --> C{是否仍持有锁?}

  14. ​ C -- 是 --> D[pexpire 重置为 30 秒]

  15. ​ C -- 否 --> E[停止续期]

  16. ​ D --> C -- 每 10 秒循环检查

三、关键注意事项

  1. 锁标识安全

  2. 必须使用UUID+线程ID组合作为标识,避免不同机器或线程冲突。

  3. 避免死锁

  4. 显式设置leaseTime时需确保业务能在超时前完成,否则锁自动释放可能导致并发问题。

  5. 集群环境

  6. Redisson 的红锁(RedLock) 需跨多个 Redis 节点加锁,半数以上成功才算获取锁,避免单点故障。

4. 守护线程如何保证执行任务状态和数据库任务状态的一致性?守护线程是全局唯一的嘛?如果不唯一,如何避免多线程并发翻转问题?如果是唯一的是如何部署的?

一、守护线程的任务状态一致性保障

  1. 事务与补偿机制

  2. 原子操作:守护线程执行任务时,需将任务状态更新(如“执行中”→“完成”)与数据库操作置于同一事务中,确保失败时状态回滚。

  3. 补偿日志:记录任务关键步骤到独立日志表,异常时通过定时任务扫描日志进行状态修复(如重试或标记失败)。

  4. 超时回滚:设置任务最大执行时长,超时自动触发状态回滚,避免僵尸任务。

  5. 状态同步设计

  6. 双写校验:任务开始前从数据库加载状态到内存,执行中定期比对内存与数据库状态,差异时触发告警并终止任务。

  7. 版本号控制:为任务记录添加版本号字段,更新状态时校验版本号,防止并发覆盖(乐观锁)。

二、守护线程的唯一性与部署模式

  1. 全局唯一场景

  2. 单例模式部署:通过进程锁(如文件锁或端口绑定)确保单节点唯一性,例如 K8s 的StatefulSet配合ReadinessProbe检测。

  3. 选举机制:集群中使用 ZooKeeper/Etcd 实现 Leader 选举,仅 Leader 节点运行守护线程,故障时自动切换。

  4. 非唯一场景(多实例)的并发控制

  5. 分布式锁协调:

  6. 使用 Redis 红锁(RedLock)或数据库行锁,在任务获取阶段加锁,确保同一任务仅被一个线程处理。

  7. 任务分片:按任务 ID 哈希分片,不同实例处理不同分片,避免重叠(如ShardingKey= task_id % instance_count)。

  8. 数据库唯一约束:在任务表中为(task_id, status)组合设置唯一索引,防止并发插入重复任务。

三、避免并发状态翻转的关键措施

  1. 状态机设计

  2. 定义任务状态流转规则(如待处理→执行中→完成/失败),禁止非法跳转(如“完成”不可回退至“执行中”)。

  3. 更新状态时校验前置状态:

  4. CAS(Compare-And-Set)操作

  5. 更新状态时附带旧值校验,确保并发安全:

四、部署实践建议

  1. 唯一性部署

  2. 容器化:通过 K8s 的StatefulSet + PodDisruptionBudget保障单实例高可用。

  3. 守护进程:Linux 系统使用systemd配置Restart=always,崩溃后自动重启。

  4. 多实例部署

  5. 负载均衡:结合 Nginx 或 Spring Cloud Gateway 分发请求,后台任务由独立微服务处理,通过消息队列(如 Kafka)触发。

  6. 心跳上报:实例定期向数据库写入心跳时间,中心调度器分配任务时排除失活实例。

总结

  • 一致性核心:通过事务 + 补偿日志 + 状态机设计,确保任务状态与数据库强一致。

  • 唯一性选择:

  • 单实例:简单可靠,适合轻量任务;

  • 多实例:扩展性强,需依赖分布式锁或分片。

  • 并发规避:乐观锁(版本号/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,备注:面试群。

发布于: 16 小时前阅读数: 16
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
20~30K * 15薪,可惜挂了_Java_王中阳Go_InfoQ写作社区