Java业务开发常见错误100例

极客时间

Java业务开发常见错误100例

代码篇

线程安全错误

原因:错用或者不会用集合导致的oom

解决方案:

代码加锁的问题

第一,使用 synchronized 加锁虽然简单,但我们首先要弄清楚共享资源是类还是实例级别的、会被哪些线程操作,synchronized 关联的锁对象或方法又是什么范围的。

第二,加锁尽可能要考虑粒度和场景,锁保护的代码意味着无法进行多线程操作。对于 Web 类型的天然多线程项目,对方法进行大范围加锁会显著降级并发能力,要考虑尽可能地只为必要的代码块加锁,降低锁的粒度;而对于要求超高性能的业务,还要细化考虑锁的读写场景,以及悲观优先还是乐观优先,尽可能针对明确场景精细化加锁方案,可以在适当的场景下考虑使用 ReentrantReadWriteLock、StampedLock 等高级的锁工具类。

第三,业务逻辑中有多把锁时要考虑死锁问题,通常的规避方案是,避免无限等待和循环等待。 此外,如果业务逻辑中锁的实现比较复杂的话,要仔细看看加锁和释放是否配对,是否有遗漏释放或重复释放的可能性;并且对于分布式锁要考虑锁自动超时释放了,而业务逻辑却还在进行的情况下,如果别的线线程或进程拿到了相同的锁,可能会导致重复执行。

​ 必要的压测——如果你的业务代码涉及复杂的锁操作,强烈建议 Mock 相关外部接口或数据库操作后对应用代码进行压测,通过压测排除锁误用带来的性能问题和死锁问题。

管理宝贵的资源——线程

原因:线程池的使用

注意:

  1. 线程池过多造成OOM 因为活跃线程过多和线程池不会被回收
  2. Java Stream Api异步分流 公用一个默认forkjion线程池,使用时要注意
  3. 线程池创建时要分析执行任务是IO资源型还是CPU资源型
  4. IO资源型或者说执行较长时间任务,并且拒绝策略为Call时,会在线程池满状态后交给调用者线程执行,如果是Web服务跑在tomcat⬆️的话,就导致整体吞吐量下降

管理宝贵的资源——连接池

  1. 池化技术的核心在于,在鱼塘养好一群鱼,需要的时候就从里面拿一条,用完再放回去。而不是自己生产一条鱼,然后用完就销毁。从而减少了开销。
  2. 大多已经实现的连接池,都是有线程安全处理的。通常比个人创建管理连接更加安全。
  3. 使用了连接池技术,就要保证连接池能够被有效复用。频繁创建连接池比频繁创建链接更加耗费资源。
  4. 连接池的参数配置要根据实际情况,并不存在多多益善
  5. 连接池的主要好处:(1)减少资源消耗,(2)利用现有的线城安全实现,(3)提升并发量

Http调用超时、重试、并发问题

  1. 超时:
    1. 连接超时代表建立 TCP 连接的时间
    2. 读取超时代表了等待远端返回数据的时间,也包括远端程序处理的时间。
    3. 在解决问题时,要考虑清楚连接的对象是谁(用户一般连接的是nginx),根据下游服务和自身服务设定合适的读取超时时间;
    4. 根据FeignClient配置规则修改配置后,检验是否生效
  2. 重试:
    1. 原因:网络丢包是比较常见的、查询是无状态的;
    2. 注意:考虑上下游接口的幂等性是否关闭自动重试;
  3. 并发:包括 HttpClient 在内的 HTTP 客户端以及浏览器,都会限制客户端调用的最大并发数。

Spring声明式事务的使用

总结: ① 事务不生效的情况,事务注解加在private方法上、事务方法中调用的是内部this调用的方法而不是self ②事务生效却出异常不回滚的情况,事务异常没有被传播出注解方法而是被捕获了、被事务注解的方法抛出的是受检异常导致不回滚 ③主方法提交,子方法出错不提交的做法: 子方法上注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务

数据库索引问题

过早的优化,是万恶之源。不需要提前考虑建立索引。等性能出现问题,真正的需求降临的时候再考虑优化。 建立索引之前,需要考虑索引带来的副作用:维护成本,空间成本,回表成本。 更重要的是还要考虑,你的查询是否能用到索引。如果花费大量成本建立的索引,最后还用不上。那就赔了夫人又折兵了。 索引又牵扯到了很多注意事项,例如:尽量使用前缀匹配,而避免使用后缀匹配。因为后缀匹配会使得索引失效,走全表匹配。

数值计算的精度、舍入和溢出问题

手机计算器把 10%+10% 算成了 0.11 而不是 0.2。 出现这种问题的原因在于,国外的计算程序使用的是单步计算法。在单步计算法中,a+b% 代表的是 a*(1+b%)。所以,手机计算器计算 10%+10% 时,其实计算的是 10%*(1+10%),所以得到的是 0.11 而不是 0.2。

  1. 使用 BigDecimal 表示和计算浮点数,应该使用 String 入参的构造方法或者 BigDecimal.valueOf 方法来初始化。
  2. 对于浮点数的格式化
  3. 如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法;如果结合 HashSet 或 HashMap 使用的话,使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法,所以不会有问题。

文件读写中的问题

第一,如果需要读写字符流,那么需要确保文件中字符的字符集和字符流的字符集是一致的,否则可能产生乱码。

第二,使用 Files 类的一些流式处理操作,注意使用 try-with-resources 包装 Stream,确保底层文件资源可以释放,避免产生 too many open files 的问题。

第三,进行文件字节流操作的时候,一般情况下不考虑进行逐字节操作,使用缓冲区进行批量读写减少 IO 次数,性能会好很多。一般可以考虑直接使用缓冲输入输出流 BufferedXXXStream,追求极限性能的话可以考虑使用 FileChannel 进行流转发。

OOM有关的问题

  1. 我们的程序确实需要超出 JVM 配置的内存上限的内存。
  2. 内存泄漏,其实就是我们认为没有用的对象最终会被 GC,但却没有。强引用的选择使用;
  3. 不合理的资源需求配置,在业务量小的时候可能不会出现问题,但业务量一大可能很快就会撑爆内存。Tomacat

解决办法:为生产系统的程序配置 JVM 参数启用详细的 GC 日志,方便观察垃圾收集器的行为,并开启 HeapDumpOnOutOfMemoryError,以便在出现 OOM 时能自动 Dump 留下第一问题现场。

1
XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=100M

反射、注解、泛型的坑

IOC和AOP

  1. spring容器管理对象默认是单例模式,可能会导致内存泄露问题;

设计篇

解决代码重复

  1. 利用工厂设计模式和模版设计方法

    ​ 我们可以考虑提取相同逻辑在父类中实现,差异逻辑通过抽象方法留给子类实现。

  2. 通过反射+注解的方式

    ​ 使用硬编码的方式重复实现相同的数据处理算法。我们可以考虑把规则转换为自定义注解,作为元数据对类或对字段、方法进行描述,然后通过反射动态读取这些元数据、字段或调用方法,实现规则参数和规则定义的分离。也就是说,把变化的部分也就是规则的参数放入注解,规则的定义统一处理。

  3. 利用属性拷贝工具消除重复代码

注意:可以把代码重复度作为评估一个项目质量的重要指标,如果一个项目几乎没有任何重复代码,那么它内部的抽象一定是非常好的。在做项目重构的时候,可以以消除重复为第一目标去考虑实现。

接口设计

开发一个服务的第一步就是设计接口。接口的设计需要考虑的点非常多,比如接口的命名、参数列表、包装结构体、接口粒度、版本策略、幂等性实现、同步异步处理方式等。

  1. 针对响应体的设计混乱、响应结果的不明确问题,服务端需要明确响应体每一个字段的意义,以一致的方式进行处理,并确保不透传下游服务的错误。
  2. 接口版本控制问题。解决不兼容问题
  3. 针对接口的处理方式,需要明确要么是同步要么是异步。

缓存设计

  1. 不要把Redis作为数据库使用;Redis 的特点是,处理请求很快,但无法保存超过内存大小的数据。
  2. 把reids作为缓存使用时的注意事项:
    1. 从客户端的角度来说,缓存数据的特点一定是有原始数据来源,且允许丢失;当数据丢失后,我们需要从原始数据重新加载数据,不能认为缓存系统是绝对可靠的,更不能认为缓存系统不会删除没有过期的数据。
    2. 从 Redis 服务端的角度来说,缓存系统可以保存的数据量一定是小于原始数据的。首先,我们应该限制 Redis 对内存的使用量,也就是设置 maxmemory 参数;其次,我们应该根据数据特点,明确 Redis 应该以怎样的算法来驱逐数据。image-20230306151954911
  3. 缓存雪崩问题
    • 原因:第一是缓存系统本身不可用;第二是应用设计层面大量的 Key 在同一时间过期,导致大量的数据回源。
    • 解决办法:
      • 方案一:差异化设置缓存TTL。增加扰动值;
      • 方案二:让缓存永不过期。设置一个后台进程,30s一次把全量数据更新到缓存
    • 注意:不管是方案一还是方案二,在把数据从数据库加入缓存的时候,都需要判断来自数据库的数据是否合法,比如进行最基本的判空检查。生产事故eg:DBA归档数据库(类似删除),从数据库中查询到了空数据加入缓存,爆发了大面积的事故。
  4. 缓存击穿问题
    • 原因:某个热点key失效,大量并发请求昂贵回源;
    • 方案:
      • 方案一,使用进程内的锁进行限制,这样每一个节点都可以以一个并发回源数据库;
      • 方案二,不使用锁进行限制,而是使用类似 Semaphore 的工具限制并发数,比如限制为 10,这样既限制了回源并发数不至于太大,又能使得一定量的线程可以同时回源。
  5. 缓存穿透问题
    • 原因:缓存没有起到压力缓冲的作用;
    • 方案:
      • 方案一:对于不存在的数据,设置一个特殊的 Value 到缓存中;但是可能会导致大量无效key。
      • 方案二:我们需要同步所有可能存在的值并加入布隆过滤器,这是比较麻烦的地方。如果业务规则明确的话,你也可以考虑直接根据业务规则判断值是否存在。
  6. 缓存数据同步问题
    • 原因:更新缓存信息/和更新数据库先后顺序?
    • 注意:考虑数据一致性问题;删除、更新操作的幂等性;
    • 方案:缓存中的数据不由数据更新操作主动触发,统一在需要使用的时候按需加载,数据更新后及时删除缓存中的数据即可。
  7. 使用缓存系统的时候,要监控缓存系统的内存使用量、命中率、对象平均过期时间等重要指标,以便评估系统的有效性,并及时发现问题。

业务上生产线的问题

完整的应用监控体系一般由三个方面构成,包括日志 Logging、指标 Metrics 和追踪 Tracing。

管理者 :面向过程管理——背结果

Other

  1. 直接运行课件代码:

    1. pom文件:删除spring-boot-starter-actuator 依赖;redisson-spring-boot-starter 里exclude redisson-spring-boot-starter 依赖。
    2. 对应的@SpringBootApplication 注解上添加exclude = { DataSourceAutoConfiguration.class, RedissonAutoConfiguration.class }
  2. 解决IDEA配置.gitignore不生效的问题

    1. 原因:**.gitignore只能忽略未被track的文件,而git本地缓存。如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的**。

    2. 解决办法:

      1
      2
      3
      git rm -r --cached .
      git add .
      git commit -m 'update .gitignore'
-------------本文结束感谢您的阅读-------------