Java开发常见问题分析

发布时间 2023-11-21 13:51:48作者: hviger

程序Bug的产生,通常分为三种类型

  1. 逻辑漏洞
    低级错误,程序执行后无法达到想要效果。

  2. 越界访问
    访问了非法区域,造成程序崩溃。

  3. 条件考虑不全面
    你以为你万无一失,但你永远都不知道输入参数究竟是什么!

如何防范未知Bug:异常捕获

异常捕获一般依靠try,catch语句。很好理解:try(尝试)一下,如果有问题,直接捕获 (catch)住,防止程序崩溃。

如果核心流程处理到一半服务器崩了怎么处理?

这里同时存在三个问题:

  • 问题排查以及快速恢复
  • 异常数据修复
  • 服务高可用,规避服务宕机

1、先抢通业务

当发现服务器宕机后,最关键的是抢通业务,而不是抢修服务器。

因此,需要做应急方案。最好准备2个网站服务器,他们存放的内容相同,而ip不同,并且机房的地理位置不同。这样第一时间发现宕机问题后,可以迅速的通过修域名记录,指向目前正常的网站空间。而且2个主机,同时宕机的可能性就大大降低了。

2、定位服务器崩溃原因

  • 内存溢出,磁盘资源耗尽
  • 线程死锁,进程过多或者不断创建,耗尽资源导致
  • 数据库慢查询,连接数过多,临时表不够用,程序死锁
  • 主备数据不一致
  • 应用程序异常
  • 流量负载过大
  • DOSS攻击
  • 散热问题

3、异常数据修复

  • 写数据做事务控制,保障数据安全。
  • 磁盘备份,重启服务时恢复数据。
  • 记录关键日志。

4、服务高可用

  • 服务多实例集群部署,负载均衡策略访问,做好服务降级、服务限流。
  • 数据库读写分离、分库分表方案。
  • 做好服务性能测试、压力测试。

开发中常见问题

空指针异常

在访问或操作对象之前,检查对象是否为null

类型转换异常

在进行类型转换之前,先检查对象的类型是否与目标类型兼容。

金额数值计算精度问题

金额一般都是用BigDecimal

循环条件错误

可能会导致死循环或者无法执行

文件操作异常

文件操作之前,先检查文件是否存在、是否可读写等。

事务不起作用没有回滚

异常被try.catch吃了,手动抛了别的异常Exception,默认情况下只会回滚RuntimeException
访问权限不是public;方法用final,static修饰;方法内部调用;
未开启事务;未被spring管理,需要创建bean实例。

内存溢出异常

在编写程序时,要注意控制内存的使用,及时释放不需要的对象。

性能问题

比如使用缓存、减少对象创建、优化算法等。

大数据量导出Excel存在问题及优化方案

问题1:一次性获取全部数据到内存当中,容易引起系统OOM。
解决方案:分页查询数据,分批处理。

问题2:分页查询存在深度分页问题,数据偏移量越大导致sqL查询变慢。
解决方案:使用标签记录优化(id自增且连续)、索引覆盖优化。

问题3:查询数据串行耗时长。
解决方案:可以通过线程编排,并行执行sql查询,最后顺序导出到excel。

问题4:一个excel文件数据过大,用户存在打不开的情况。
解决方案:通过easyexcel多sheet页导出数据。

实际开发中,如何正确使用多线程?

  1. 处理并行任务:多线程可以同时处理多个任务,提高程序的执行效率。比如批量处理数据、同时上传多个文件等。
  2. 事件驱动的编程:Java多线程可以用于事件驱动的编程,如GUI、网络编程等。
  3. 并发访问共享资源:多线程可以应用于并发访问共享资源的场景,如数据库连接池。
  4. 高效的IO操作:在网络编程中,Java多线程可以提供高效的IO操作,如同时读写多个Socket。
  5. 多任务协同处理:在复杂的任务中,不同的任务可以以各自独立的方式并行运行,最终合并结果。
  6. 节约资源:多线程可以提高CPU和内存的使用效率,使得我们能够更好地利用系统资源。
  7. 提高用户体验:在一些高并发场景下,如网站、游戏等,使用多线程可以提高用户体验,使得用户能够更快地得到反馈。

常见的多线程问题包括

  1. 线程安全问题
    多个线程同时访问共享资源时可能导致数据不一致或异常。解决方案包括使用同步机制(如synchronized关键字、Lock对象)、使用线程安全的数据结构、避免共享状态等。
  2. 死锁问题
    多个线程因相互等待对方释放资源而无法继续执行。解决方案包括避免循环等待资源、按照固定顺序获取资源、设置超时时间等。
  3. 上下文切换问题
    线程切换需要耗费一定的时间和资源,如果线程频繁切换,会降低程序性能。解决方案包括合理设计线程数量、减少线程间的竞争、使用线程池等。
  4. 数据同步问题
    多个线程访问共享数据时,可能出现数据不一致的问题。解决方案包括使用锁来保证数据的原子性、使用volatile关键字保证可见性、使用线程安全的数据结构等。
  5. 过度创建线程问题
    创建线程需要消耗系统资源,如果过度创建线程,可能导致系统资源耗尽。解决方案包括使用线程池来复用线程、合理设置线程池大小等。

定时任务如果集群,如何保证不被重复执行?

  1. 独立部署,将定时任务独立出来,成为一个单独的项目工程,单一部署
  2. 配置实现,配置文件设置一个标识符号,定时任务读取此配置文件此属性, 读取到ture执行定时任务,否则不执行
  3. 利用分布式锁,虽然两个机器都会运行定时任务,但是一个时刻只有一台机器会真正的执行定时任务的核心方法

一个订单30分钟未支付自动取消功能,有几种实现方案?

  1. 数据库轮询
    该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作。

    优点:简单易行,支持集群操作。
    缺点:对服务器内存消耗大;
    存在延迟,比如你每隔 3 分钟扫描一次,那最坏的延迟时间就是 3 分钟;
    假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大。

  2. JDK 的延迟队列
    该方案是利用 JDK 自带的 DelayQueue 来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入 DelayQueue 中的对象,是必须实现 Delayed 接口的。

    优点:效率高,任务触发时间延迟低。
    缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;代码复杂度较高;
    因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常。

  3. 时间轮算法
    时间轮算法可以类比于时钟,按某一个方向按固定频率轮动,每一次跳动称为一个 tick。

    优点:效率高,任务触发时间延迟时间比 delayQueue 低,代码复杂度比 delayQueue 低。
    缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;
    因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常。

  4. Redis 缓存
    思路一:利用 Redis 的 zset,zset 是一个有序集合,每一个元素 (member) 都关联了一个 score, 通过 score 排序来取集合中的值。
    思路二:使用 Redis 的 Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在 key 失效之后,提供一个回调,实际上是 Redis 会给客户端发送一个消息。是需要 Redis 版本 2.8 以上。

    优点:由于使用 Redis 作为消息通道,消息都存储在 Redis 中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性;做集群扩展相当方便;时间准确度高。
    缺点:需要额外进行 Redis 维护。

  5. 使用消息队列
    可以采用 RabbitMQ 的延时队列。RabbitMQ可以实现延迟队列。

    优点:高效,可以利用 RabbitMQ 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
    缺点:本身的易用度要依赖于 RabbitMQ 的运维。因为要引用 RabbitMQ, 所以复杂度和成本变高。

为什么不用eureka非要用nacos作为注册中心?

nacos在自动或手动下线服务,使用消息机制通知客户端,服务实例的修改很快响应;Eureka只能通过任务定时剔除无效的服务。
nacos可以根据namespace命名空间,DataId,Group分组,来区分不同环境(dev,test,prod),不同项目的配置。

Mq如何保证消息不丢失?

丢数据一般分为两种,一种是mq把消息丢了,一种就是消费时将消息丢了。

Mq如何保证消息顺序的一致性?

如何避免消息一直堆积在mq服务器端?

在遇到消息堆积的时候,先检查下导致堆积的原因,可能有如下几种:

  1. 消费失败时大量重试导致消息堆积。
  2. 消费者程序的故障:如 程序死锁,io阻塞等。
  3. 消费者资源瓶颈:目前的主流消息队列,单个节点消息收发的性能可以达到万级别甚至10万级+的水平。除非容量预估没有做好,一般不会出现这种问题。即使出现这种问题,通过Scale Out Broker 的实例数也是比较轻松可以解决的。

消息堆积的解决方案:

  1. 提高消费者数量;更多的消费者将允许同时处理更多的消息,并减少消息堆积。
  2. 调整超时设置;例如,在某些情况下,因为某些原因(例如网络延迟),MQ 消费者需要等待更长时间才能接收到新的消息。
  3. 批量操作;例如,在生产者端,您可以使用管道来一次性发送多条信息。在消费者端,您可以使用批处理来一次性处理多个消息。
  4. 数据结构优化;例如,在使用MQ时,可以通过在消息中添加一些元信息来优化处理流程,或者采用更合适的数据结构存储消息,以减少在 MQ 中积累的消息数量。

Mq异步消费,如何获取到返回结果?

以下用Rabbit为例:

  1. 异步操作,获取回调消费结果,需要实现RabbitTemplate.ConfirmCallback 接口,然后重写 confirm()方法。
  2. 获取回调结果,指的是获取消息是否被消费端正常消费而返回的结果,并不是消费端返回的处理结果,这一点得注意,如果需要等待消费端返回处理结果,则需要做同步操作,而不是做回调操作。
  3. 需要做同步操作时,应该rabbitTemplate.convertSendAndReceive()方法,返回结果类型是Object,需要根据消费端返回的数据类型来决定强转的类型。
  4. 异步则使用rabbitTemplate.convertAndSend()方法。