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

0 阅读14分钟

猫眼娱乐演出业务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. 数据结构设计
    1. 使用Redis的Hash结构存储锁信息:
      • Key:锁名称(如myLock
      • Field:线程唯一标识(格式为UUID + 线程ID,如b9834f1e-Thread-1
      • Value:重入次数(计数器)
    2. 例如:HSET myLock b9834f1e-Thread-1 2 表示该线程已重入2次。
  2. 加锁逻辑(Lua脚本保证原子性)
    1. 首次加锁:若Hash不存在,则创建并设置计数器为1,同时设置锁超时时间。
    2. 重入加锁:若Hash中已存在当前线程的Field,则将计数器递增1,并刷新超时时间。
  3. 解锁逻辑
    1. 每次解锁将计数器减1,计数器归零时删除Key释放锁:

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

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

  1. 自动续期原理
    1. 默认行为:未显式设置leaseTime(锁超时时间)时,Redisson启动WatchDog线程。
    2. 续期规则:
      • 每隔锁超时时间 / 3(默认10秒)检查锁是否仍被持有。
      • 若持有则通过pexpire命令将锁超时时间重置为初始值(默认30秒)。
    3. 终止条件:锁被释放或显式设置了leaseTime
  2. 配置参数
    1. 锁超时时间:通过Config.lockWatchdogTimeout修改(默认30,000毫秒)。
    2. 显式设置超时:指定leaseTime参数后,WatchDog不生效,锁到期自动释放:
  3. 续期流程
    1. graph TD
    2. ​ A[业务线程持有锁] --> B[WatchDog启动]
    3. ​ B --> C{是否仍持有锁?}
    4. ​ C -- 是 --> D[pexpire重置为30秒]
    5. ​ C -- 否 --> E[停止续期]
    6. ​ D --> C -- 每10秒循环检查

三、关键注意事项

  1. 锁标识安全
    1. 必须使用UUID+线程ID组合作为标识,避免不同机器或线程冲突。
  2. 避免死锁
    1. 显式设置leaseTime时需确保业务能在超时前完成,否则锁自动释放可能导致并发问题。
  3. 集群环境
    1. Redisson的红锁(RedLock) 需跨多个Redis节点加锁,半数以上成功才算获取锁,避免单点故障。

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

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

  1. 事务与补偿机制
    1. 原子操作:守护线程执行任务时,需将任务状态更新(如“执行中”→“完成”)与数据库操作置于同一事务中,确保失败时状态回滚。
    2. 补偿日志:记录任务关键步骤到独立日志表,异常时通过定时任务扫描日志进行状态修复(如重试或标记失败)。
    3. 超时回滚:设置任务最大执行时长,超时自动触发状态回滚,避免僵尸任务。
  2. 状态同步设计
    1. 双写校验:任务开始前从数据库加载状态到内存,执行中定期比对内存与数据库状态,差异时触发告警并终止任务。
    2. 版本号控制:为任务记录添加版本号字段,更新状态时校验版本号,防止并发覆盖(乐观锁)。

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

  1. 全局唯一场景
    1. 单例模式部署:通过进程锁(如文件锁或端口绑定)确保单节点唯一性,例如K8s的StatefulSet配合ReadinessProbe检测。
    2. 选举机制:集群中使用ZooKeeper/Etcd实现Leader选举,仅Leader节点运行守护线程,故障时自动切换。
  2. 非唯一场景(多实例)的并发控制
    1. 分布式锁协调:
      • 使用Redis红锁(RedLock)或数据库行锁,在任务获取阶段加锁,确保同一任务仅被一个线程处理。
    2. 任务分片:按任务ID哈希分片,不同实例处理不同分片,避免重叠(如ShardingKey= task_id % instance_count)。
    3. 数据库唯一约束:在任务表中为(task_id, status)组合设置唯一索引,防止并发插入重复任务。

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

  1. 状态机设计
    1. 定义任务状态流转规则(如待处理→执行中→完成/失败),禁止非法跳转(如“完成”不可回退至“执行中”)。
    2. 更新状态时校验前置状态:
  2. CAS(Compare-And-Set)操作
    1. 更新状态时附带旧值校验,确保并发安全:

四、部署实践建议

  1. 唯一性部署
    1. 容器化:通过K8s的StatefulSet + PodDisruptionBudget保障单实例高可用。
    2. 守护进程:Linux系统使用systemd配置Restart=always,崩溃后自动重启。
  2. 多实例部署
    1. 负载均衡:结合Nginx或Spring Cloud Gateway分发请求,后台任务由独立微服务处理,通过消息队列(如Kafka)触发。
    2. 心跳上报:实例定期向数据库写入心跳时间,中心调度器分配任务时排除失活实例。

总结

  • 一致性核心:通过事务 + 补偿日志 + 状态机设计,确保任务状态与数据库强一致。
  • 唯一性选择:
    • 单实例:简单可靠,适合轻量任务;
    • 多实例:扩展性强,需依赖分布式锁或分片。
  • 并发规避:乐观锁(版本号/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,备注:面试群。