记一次国企Java面试

Java应用系统的稳定性保证

(1)代码层面

健壮性编码:进行充分的参数校验、边界条件判断、空指针规避。

异常处理:不要吞掉异常(空的catch块),要合理记录日志并向上抛出或进行转换。使用自定义异常来区分业务异常和系统异常。

资源管理:使用`try-with-resources`确保连接(数据库、网络、文件)等资源被正确关闭,避免泄漏。

避免内存泄漏:谨慎使用静态集合,及时清理无用的对象引用,注意监听器的注销。

(2)设计与架构层面

微服务容错:采用Spring Cloud Alibaba Sentinel、Hystrix等组件实现熔断、降级、限流。防止因某个依赖服务故障导致整个系统雪崩。

熔断:依赖服务不可用时,快速失败,避免积压请求。

降级:服务压力过大时,关闭非核心功能,保障核心业务畅通。

限流:控制单位时间内的请求量,保护自身和下游系统。

超时与重试:为所有外部调用(HTTP、RPC、数据库)设置合理的超时时间,并配合重试机制(如Spring Retry),但要注意重试的幂等性。

解耦与异步:使用消息队列(如Kafka)将耗时操作异步化,削峰填谷,提高响应速度和吞吐量。

(3)运维与监控层面

全面监控:腾讯云监控系统等。

应用监控:使用APM工具(如SkyWalking, Pinpoint)监控JVM(GC、内存、线程)、接口响应时间、QPS、错误率。

系统监控:监控服务器的CPU、内存、磁盘、网络。

业务监控:监控核心业务指标,如订单量、支付成功率。

日志聚焦化:使用ELK(Elasticsearch, Logstash, Kibana)或类似方案收集和查询日志,便于快速定位问题。

压力测试与容量规划:在上线前进行全链路压测,了解系统瓶颈,并据此规划合理的机器资源。

(4)流程与管理层面

代码审查:保证代码质量,分享知识。

自动化测试:建立完善的单元测试、集成测试、端到端测试流水线。

灰度发布:先发布一小部分机器,观察无误后再全量发布,降低故障影响面。

Kafka异常消费的处理

Kafka消费端的稳定性核心在于如何优雅地处理消费失败。

(1)自动提交 vs 手动提交

自动提交:简单但不安全。可能在消息处理完之前就提交了偏移量,如果此时消费者崩溃,消息会丢失。

手动提交:生产环境推荐。在处理完业务逻辑后,再手动提交偏移量。这保证了“至少一次”投递。

(2)重试机制

内部重试:在消费者内部,当单条消息处理失败时,进行有限次数的重试(如3次)。适用于由网络抖动等导致的瞬时故障。

@KafkaListener(topics = "my-topic")
public void consume(String message, Acknowledgment ack) {
    int maxRetries = 3;
    
    for (int i = 0; i < maxRetries; i++) {
        try {
            // 处理业务逻辑
            processMessage(message);
            ack.acknowledge(); // 手动提交
            break; // 成功则跳出循环
            
        } catch (BusinessException e) {
            // 业务异常,记录日志并进入重试或死信
            if (i == maxRetries - 1) {
                // 发送到死信队列
                sendToDlt(message, e);
                ack.acknowledge(); // 原消息依旧要确认,避免阻塞
            }
            
        } catch (Exception e) {
            // 系统异常,可能需要进行重试
            if (i == maxRetries - 1) {
                // 最终失败处理
                ack.acknowledge();
            }
        }
    }
}

(3)死信队列(DLQ – Dead Letter Queue)

处理那些经过多次重试后依旧失败的消息。将这些消息投递到一个专门的Topic(DLQ)中,避免它们阻塞主业务Topic的消费。

Spring-Kafka提供了 `
DeadLetterPublishingRecoverer` 和 `DefaultErrorHandler` 的实现。

@Configuration
public class KafkaConfig {

    @Bean
    public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
            ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
            ConsumerFactory<Object, Object> kafkaConsumerFactory,
            KafkaTemplate<Object, Object> template) {
        
        ConcurrentKafkaListenerContainerFactory<Object, Object> factory = 
            new ConcurrentKafkaListenerContainerFactory<>();
        configurer.configure(factory, kafkaConsumerFactory);

        // 设置重试机制
        DefaultErrorHandler errorHandler = new DefaultErrorHandler(
            new DeadLetterPublishingRecoverer(template), // 发布到死信
            new FixedBackOff(1000L, 2) // 重试2次,间隔1秒
        );
        
        factory.setCommonErrorHandler(errorHandler);
        return factory;
    }
}

后续处理:需要有独立的服务或人工来监控和处理DLQ中的消息,分析失败缘由并进行修复或补偿。

(4)消费幂等性

确保即使同一条消息被消费多次,也不会产生负面效果。这是实现可靠消息系统的基石。可以通过数据库唯一约束、乐观锁、或记录已处理消息ID等方式实现。

MySQL, ES, Redis, MongoDB 数据与事务一致性

没有银弹,需要根据业务场景在一致性、可用性、性能之间做权衡。核心思想:放弃强一致性,追求最终一致性,通过补偿等手段解决中间状态。

(1)单一数据源(如MySQL)

本地事务:使用Spring的`@Transactional`保证ACID。

多数据源:可以使用JTA或Seata等分布式事务框架实现强一致性,但性能损耗大。

(2)多数据源之间的同步(如:MySQL -> ES/Redis)

双写:在业务代码中,先写数据库,再写ES/Redis。非原子操作,再写失败会导致数据不一致。

优化:先写DB后删缓存,这是Cache-Aside模式的推荐做法。写数据时,先更新数据库,然后删除(而非更新)缓存。下次读请求会自动从DB加载最新数据到缓存。

异步重试:如果删除缓存失败,将删除操作放入消息队列异步重试,确保最终一致。

(3)基于Binlog的CDC

最可靠、对业务无侵入的方案。

通过Canal、Debezium等工具监听并解析MySQL的binlog,将数据变更事件发送到Kafka,然后由消费者将数据同步到ES、Redis、MongoDB等。

优点:解耦业务逻辑,保证数据顺序,高可靠性。

流程:`MySQL -> Binlog -> CDC Connector -> Kafka -> Consumer -> ES/Redis/MongoDB`

(4)特定场景的“事务”

Redis Lua脚本:可以将多个操作封装在一个Lua脚本中执行,保证原子性。

MongoDB多文档事务:在4.0+版本支持副本集的多文档事务,4.2+支持分片集群事务。用法类似传统数据库事务。

ES:本身不支持跨索引的事务。其“事务”主要通过版本号控制来实现乐观锁,保证单个文档的原子更新。

(5)最终一致性的补偿机制

当同步过程失败时,必须有手段来修复数据。

对账:定期(如每天凌晨)跑Job,比对MySQL和ES/Redis中的核心数据,将不一致的记录修复。

校验与修复:提供管理后台,允许运营或开发人员通过某个ID手动触发数据同步。

TTL+自愈:为缓存数据设置TTL,短期的不一致可以通过缓存过期后重新加载来自动修复。

注册中心下线对业务的影响

注册中心下线不会导致正在进行的业务请求失败,但会严重影响后续服务的发现、更新和未来的调用。系统会进入一个“静默”的僵化状态。

(1)对服务提供者的影响

服务提供者会定期向注册中心发送心跳,以声明“我还活着”。

注册中心下线期间:

新服务实例无法注册:新启动的微服务实例无法在注册中心注册自己,因此不会被其他服务发现和调用。这会导致水平扩展失效。

健康检查中断:注册中心无法接收现有服务实例的心跳,但它不会立即将这些服务剔除。

注册中心恢复后:

所有服务实例会重新注册和发送心跳,状态会逐渐恢复正常。

如果注册中心下线时间过长,超过了配置的心跳超时时间,那么在恢复后,注册中心可能会认为那些在此期间无法上报心跳的实例是**不健康的**并将其剔除。但实际上这些实例本身可能是健康的。

(2)对服务消费者的影响

服务消费者会从注册中心拉取服务列表并本地缓存,同时也会定期更新。

服务发现:

本地缓存救命:大多数客户端(如Spring Cloud Netflix的Ribbon、Nacos Client、Spring Cloud Alibaba)都会在本地缓存一份服务提供者列表。

短期内无影响:在注册中心下线后的短时间内,消费者会继续使用本地缓存的服务列表进行调用,所有基于现有缓存的服务调用都能正常进行。

服务列表更新:

无法获取新实例:消费者无法从注册中心获取到最新的服务列表。因此任何新上线的服务提供者实例都不会被消费者感知到,流量不会被分发到这些新实例上。

无法感知实例下线:这是最危险的情况。如果某个服务提供者实例崩溃或主动下线了,由于注册中心已经下线,它无法通知消费者“这个实例已失效”。消费者本地缓存的服务列表里依旧包含这个已经下线的实例。

当消费者尝试调用这个已下线的实例时,就会发生连接超时或调用失败,导致部分请求失败。

(3)对配置中心的影响

应用无法动态获取最新配置:配置的动态刷新功能会失效。

应用启动可能失败:如果应用启动时需要从配置中心拉取必要的配置,那么此时应用将无法启动。

(4)如何提高注册中心鲁棒性?

客户端缓存与负载均衡:

确保客户端有完整的服务列表缓存。这是抵御注册中心故障的第一道防线。

Ribbon等组件默认就有缓存。

注册中心集群高可用:

绝不能单点部署。必须搭建注册中心集群(如Nacos集群、Eureka集群),通过多个节点来避免单点故障。

合理的客户端配置:

拉取间隔与缓存:适当调整客户端从注册中心拉取服务列表的时间间隔。不要太频繁,增加注册中心压力;也不要太慢,影响发现速度。

心跳超时时间:适当调大服务实例的心跳超时时间,让注册中心在短暂故障期间不会轻易将健康的服务实例剔除。例如,Eureka的 `
lease-expiration-duration-in-seconds` 配置。

服务调用容错:

在消费者端使用熔断器,当调用某个失败实例时,熔断器会快速将其熔断,避免持续请求超时。这可以与本地缓存结合,自动从负载均衡列表中剔除故障节点(尽管是暂时的)。Hystrix/Sentinel在这方面作用巨大。

手动应急措施:

在极端情况下,可以思考为消费者配置静态服务列表,绕过注册中心,直接指向健康的服务提供者实例,作为一种临时的应急方案。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
哼我就是介么萌的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容