后端大厂面试总结大全四

后端大厂面试总结大全四 目录1、Transactional注解控制事务有哪些不生效的场景2、MySQL的优化3、除了使用try...catch来抛异常还有其他的方法吗4、Feign 第一次调用为什么会很慢5、如何保证数据库和缓存强一致性5.1 整体流程5.2 先选一个基础模式Cache-Aside TTL5.3 常见问题与“真实”工程方案5.3.1 删缓存失败了怎么办5.3.2 并发读写时旧数据会不会重新写回缓存6、索引添加的技巧7、线上频繁full gc该如何排查和处理1、Transactional注解控制事务有哪些不生效的场景数据库引擎不支持事务数据源没有配置事务管理器没有在spring配置文件中开启事务管理器事务注解被覆盖导致事务失效配置错误的 Transactional 注解事务超时时间设置过短使用了错误的事务传播机制Propagation.NOT_SUPPORTED传播特性不支持事务rollbackFor属性配置错误其实rollbackFor属性指定的异常必须是Throwable或者其子类。默认情况下RuntimeException和Error两种异常都是会自动回滚的。但是因为以上的代码例子指定了rollbackFor Error.class但是抛出的异常又是Exception而Exception和Error没有任何什么继承关系因此事务就不生效service类没有被spring管理方法的访问权限不是public的事务的方法被final、static关键字修饰了同一个类中方法调用导致Transactional失效异常被捕获了并处理了没有重新抛出事务中的异常已经被业务代码捕获并处理而没有被正确地传播回事务管理器事务将无法回滚。手动抛了别的异常失效的原因 上面的代码例子中手动抛了Exception异常但是是不会回滚的因为Spring默认只处理RuntimeException和Error对于普通的Exception不会回滚除非用rollbackFor属性指定配置。解决方案添加属性配置Transactional(rollbackFor Exception.class)事务多线程的调用这是因为Spring事务是基于线程绑定的每个线程都有自己的事务上下文 而多线程环境下可能会存在多个线程共享同一个事务上下文的情况导致事务不生效。Spring事务管理器通过使用线程本地变量ThreadLocal来实现线程安全。举例最后一个的解决办法第一种解决办法新建一个类一个方法在a中注入新类再通过新类调用事务注解的方法。第二种解决方法就是在该类中注入自己通过在a方法中通过自己类的对象调用b的事务注解方法。2、MySQL的优化做MySQL优化我们要善用EXPLAIN查看SQL执行计划SQL语句中IN包含的值不应过多SELECT语句务必指明字段名称当只需要一条数据的时候使用limit 1如果限制条件中其他字段没有索引尽量少用or尽量用union all代替union使用union all前提是两张表没有重复数据使用合理的分页方式以提高分页的效率数据量太大分页会越来越慢建议使用id为条件来进行分页处理更快分段查询数据达到百万级可以使用分段查询循环展示数据要的时候再加载数据避免在where子句中对字段进行null值判断不建议使用%前缀模糊查询避免在where子句中对字段进行表达式操作对于联合索引来说要遵守最左前缀法则举列来说索引含有字段id、name、school可以直接用id字段也可以id、name这样的顺序但是name;school都无法使用这个索引。所以在创建联合索引的时候一定要注意索引字段顺序常用的查询字段放在最前面。注意范围查询语句对于联合索引来说如果存在范围查询比如between、、等条件时会造成后面的索引字段失效。JOIN优化尽量使用inner join等值关联查询尽量使用小表来驱动大表。19种优化建议3、除了使用try…catch来抛异常还有其他的方法吗用 Assert(断言) 替换 throw exception想必 Assert(断言) 大家都很熟悉比如 Spring 家族的 org.springframework.util.Assert在我们写测试用例的时候经常会用到使用断言能让我们编码的时候有一种非一般丝滑的感觉比如Testpublicvoidtest1(){...UseruseruserDao.selectById(userId);Assert.notNull(user,用户不存在.);...}Testpublicvoidtest2(){// 另一种写法UseruseruserDao.selectById(userId);if(usernull){thrownewIllegalArgumentException(用户不存在.);}}也非常推荐大家使用断言来抛异常会让代码更加的优雅。4、Feign 第一次调用为什么会很慢首先要了解Feign是如何进行远程调用的这里面包括注册中心、负载均衡、FeignClient之间的关系微服务通过不论是eureka、nacos也好注册到服务端Feign是靠Ribbon做负载的而Ribbon需要拿到注册中心的服务列表将服务进行负载缓存到本地然后FeignClient客户端在进行调用大概就是这么一个过程。我们知道ribbon是靠LoadBalancer做负载的 无非就是ILoadBalancer接口的方法依次是添加新的服务、在负载均衡里选择一个服务、markServerDown服务下线、获取服务列表、获取存活的服务器、获取所有服务器包括健康和不健康的。解决方法开启Ribbon饥饿加载在项目启动的时候可以从日志看到已经把Lxlxxx-system2服务进行加载从而避免了第一次请求超时的情况其实这种饥饿加载模式类似于“客户端负载预热”的一个操作项目启动的时候进行加载防止服务之间调用可以因为数据量、业务逻辑处理复杂性导致接口超时如果你的服务之间调用业务处理比较复杂、且慢不妨可以试试这种解决方式。5、如何保证数据库和缓存强一致性5.1 整体流程5.2 先选一个基础模式Cache-Aside TTL业界最主流、线上用得最多的基础模式就是读先查缓存未命中则查数据库并回写缓存Cache-Aside。写先更新数据库再删除缓存并给缓存设置合理的 TTL。简化代码// 读publicProductgetProduct(Longid){Stringkeyproduct:id;Productpredis.get(key);if(p!null)returnp;pdb.findById(id);if(p!null){// 建议设置随机过期时间避免雪崩longttlBASE_TTL_MINUTESThreadLocalRandom.current().nextInt(0,5);redis.setex(key,p,ttl,TimeUnit.MINUTES);}returnp;}// 写事务内先更库提交后再删缓存TransactionalpublicvoidupdateProduct(Productp){productMapper.update(p);// 注册事务提交后的回调真正提交成功后再删缓存TransactionSynchronizationManager.registerSynchronization(newTransactionSynchronization(){OverridepublicvoidafterCommit(){redis.delete(product:p.getId());}});}5.3 常见问题与“真实”工程方案5.3.1 删缓存失败了怎么办真实项目里Redis 删 key 是可能失败的网络抖动、Redis 宕机、主从切换等。1.立即重试删除失败时在当前线程里立即重试 23 次适合对一致性要求一般、流量不大的场景。2.MQ 异步重试更新数据库成功后发一条“删缓存”消息到 MQ消费者收到后删缓存失败则利用 MQ 的重试机制反复重试。适合中等规模系统组件依赖可控。3.本地消息表 / Outbox 模式在同一个本地事务里写业务数据 “删缓存任务”到本地消息表事务提交后由定时任务或独立进程拉取消息表重试删缓存适合“数据库事务要和外部动作绑在一起”的场景更可靠。4.binlog / CDC 监听删缓存大厂标配业务只写 DB完全不感知缓存Canal/Maxwell/Flink CDC 等监听 MySQL binlog解析变更再异步删除或更新缓存。优点业务零侵入、可靠性高、天然适配多服务修改同一张表。缺点架构复杂需要维护 Canal、MQ 等中间件。5.3.2 并发读写时旧数据会不会重新写回缓存经典问题线程 A 更新 DB线程 B 缓存未命中从 DB 读旧值线程 A 删缓存线程 B 把旧值写回缓存 → 产生短暂脏数据。这个问题发生条件比较苛刻实际概率不高但高并发热点数据确实可能遇到。工程上常用的解法有Delayed Double Deletion (延迟双删)流程删除缓存更新数据库延迟一段时间比如 300500ms甚至 1s再次删除缓存。真实项目里更推荐用 MQ 延迟队列 来做“延迟第二次删除”而不是在线程里 Thread.sleep这样对吞吐更友好。6、索引添加的技巧先看慢查询和真实 SQL再决定加什么索引。用“高选择度 高频查询”列做索引避免在“低选择度、很少查”的列上建索引。组合索引列顺序等值条件 → 范围条件 → 排序/分组 → SELECT 列尽量做成覆盖索引。控制索引数量小表不建、低频不建、写多读少慎建避免“索引冗余”。上线前用 EXPLAIN / 统计信息验证用 invisible index / shadow 测试避免踩坑。区分数据库特性主键/聚集索引、覆盖索引、函数索引、降序索引、不同索引类型B树、GIN、BRIN 等合理选用。举例性别字段不适合单独建索引为啥区分度太低选择度低索引的价值在于“快速缩小扫描范围”。性别通常只有 男/女/未知 三个值。假设表里有 100 万条数据按性别均分每个性别约 33 万条。如果你查 WHERE gender ‘男’即使走了索引数据库还是要回表去读 33 万行数据这和全表扫描没太大区别甚至更慢。优化器可能会“放弃”索引数据库优化器是很聪明的它内部有成本估算机制。当索引筛选后的数据量超过总数据量的 10%30%不同数据库阈值略有不同时优化器会认为回表的随机 IO 成本太高不如直接全表扫描顺序 IO 读起来更快。所以就算你强制加上索引执行计划里大概率也是 type: ALL全表扫描索引根本不会被用到。索引维护成本反而拖慢写入每次插入、更新数据数据库都要维护 B 树索引。给一个几乎用不上的字段加索引不仅浪费磁盘空间还白白增加了写入时的 CPU 和 IO 开销纯属“赔了夫人又折兵”。“三色球别建索引”性别、状态、类型这类低区分度字段不要单独建索引。7、线上频繁full gc该如何排查和处理GC 日志是否开启JDK8/11 参数正确JDK8 及之前旧参数-XX:PrintGCDetails-XX:PrintGCDateStamps-Xloggc:/path/to/gc.log-XX:UseGCLogFileRotation-XX:NumberOfGCLogFiles10-XX:GCLogFileSize10MJDK9统一日志 -Xlog -Xlog:gc*:file/path/to/gc.log:time,uptime:filecount10,filesize10MGC 日志里 Full GC 的 Cause 是什么Allocation Failure / Metadata GC Threshold / System.gc / Concurrent Mode FailureAllocation Failure / Concurrent Mode Failure / Evacuation Failure多数是“堆不够或分配速率太高”对象晋升太快老年代撑不住。Metadata GC ThresholdMetaspace / 类元数据空间不足或 JVM 认为需要回收类数据。System.gc()有代码显式调用 System.gc()常见于一些框架如 RMI或缓存库。GC 降级CMS/G1 失败CMS 的 Concurrent Mode Failure、G1 的 Evacuation Failure 都会导致回退到“Serial Old 做全堆压缩”这是最重的 Full GC。监控里老年代使用是否频繁接近 -Xmx适当增大堆-Xmx或调整年轻代/老年代比例例如 -XX:NewRatio、-Xmn是否有 Metaspace / 类加载相关的异常或持续增长Metaspace类元数据空间不足或触发阈值;显式设置了较小的 MaxMetaspaceSize.是否有显式 System.gc() 调用代码中显式调用 System.gc()某些框架如 RMI会定期调用;-XX:DisableExplicitGC 禁用显式 GC注意某些场景依赖它如 RMI DGC;如果是 RMI可以调大 DGC 间隔减少 GC 调用频率。是否怀疑泄漏Full GC 后堆使用仍高需要堆快照分析在“合适时机”抓堆快照heap dump;定位业务代码修改代码漏洞。是否有大对象/缓存/线程池等业务层面可以优化的点定位业务代码修改代码漏洞来优化。