Java业务开发常见错误100例(代码篇-1)

Java业务开发常见错误100例(代码篇-1) 此博客记录自己学习极客教程上《Java业务开发常见错误100例》的个人总结,将按照课程体系三大部分,分成代码篇、设计篇和安全篇 三个章节。
01丨使用了并发工具类库,线程安全就高枕无忧了吗?

  1. 使用 ThreadLocal 来缓存数据,以为 ThreadLocal 在线程之间做了隔离不会有线程安全问题,没想到线程重用导致数据串了。请务必记得,在业务逻辑结束之前清理ThreadLocal 中的数据。
  2. 认为使用了 ConcurrentHashMap 就可以解决线程安全问题,没对复合逻辑加锁导致业务逻辑错误。如果你希望在一整段业务逻辑中,对容器的操作都保持整体一致性的话,需要加锁处理。
  3. 没有充分了解并发工具的特性,还是按照老方式使用新工具导致无法发挥其性能。比如,使用了ConcurrentHashMap,但没有充分利用其提供的基于 CAS 安全的方法,还是使用锁的方式来实现逻辑。
  4. CopyOnWriteArrayList虽是一个线程安全的ArrayList,它原理是 写时复制,因此适用于 读多写少的 业务场景下
02丨代码加锁:不要让“锁”事成为烦心事
  1. 使用 synchronized 加锁虽然简单,但我们首先要弄清楚共享资源是类还是实例级别 的、会被哪些线程操作,synchronized 关联的锁对象或方法又是什么范围的。
  2. 加锁尽可能要考虑粒度和场景,锁保护的代码意味着无法进行多线程操作。对于 Web 类型的天然多线程项目,对方法进行大范围加锁会显著降级并发能力,要考虑尽可能 地只为必要的代码块加锁,降低锁的粒度;而对于要求超高性能的业务,还要细化考虑锁的 读写场景,以及悲观优先还是乐观优先,尽可能针对明确场景精细化加锁方案,可以在适当 的场景下考虑使用 ReentrantReadWriteLock、StampedLock 等高级的锁工具类。
  3. 业务逻辑中有多把锁时要考虑死锁问题,通常的规避方案是,避免无限等待和循环等 待。
  4. 此外,如果业务逻辑中锁的实现比较复杂的话,要仔细看看加锁和释放是否配对,是否有遗 漏释放或重复释放的可能性;并且要考虑锁自动超时释放了,而业务逻辑却还在进行的情况 下,如果别的线线程或进程拿到了相同的锁,可能会导致重复执行。
  5. 如果你的业务代 码涉及复杂的锁操作,强烈建议 Mock 相关外部接口或数据库操作后对应用代码进行压 测,通过压测排除锁误用带来的性能问题和死锁问题。
03丨线程池:业务代码最常用也最容易犯错的组件
  1. Executors 类提供的一些快捷声明线程池的方法虽然简单,但隐藏了线程池的参数细节。因此,使用线程池时,我们一定要根据场景和需求配置合理的线程数、任务队列、拒绝策略、线程回收策略,并对线程进行明确的命名方便排查问题。
  2. 既然使用了线程池就需要确保线程池是在复用的,每次 new 一个线程池出来可能比不用线程池还糟糕。如果你没有直接声明线程池而是使用其他同学提供的类库来获得一个线程池,请务必查看源码,以确认线程池的实例化方式和配置是符合预期的。
  3. 复用线程池不代表应用程序始终使用同一个线程池,我们应该根据任务的性质来选用
    不同的线程池。特别注意 IO 绑定的任务和 CPU 绑定的任务对于线程池属性的偏好,如果
    希望减少任务间的相互干扰,考虑按需使用隔离的线程池。
04丨连接池:别让连接池帮了倒忙
连接池结构示意图:(常用连接池有:Redis 连接池、HTTP 连接池、数据库连接池)
Java业务开发常见错误100例(代码篇-1)
文章图片

  1. 连接池实现方式:客户端 SDK 实现连接池的方式,包括池和连接分离、内部带有连接池和非连接池三种。要
    正确使用连接池,就必须首先鉴别连接池的实现方式。比如,Jedis 的 API 实现的是池和连接分离的方式,而 Apache HttpClient 是内置连接池的 API。
  2. 使用姿势:一是确保连接池是复用的,二是尽可能在程序退出之前显式关闭连接池释放资源。
  3. 连接池参数配置:最重要的是最大连接数,许多高并发应用往往因为最大连接数不
    够导致性能问题。但最大连接数不是设置得越大越好,而是够用就好。
05丨HTTP调用:你考虑到超时、重试、并发了吗?
  1. 理解 连接超时 和 读取超时的区别,学会如何设置 合适的 超时参数。此外,在使用诸如 Spring Cloud Feign 等框架时务必确认,连接和读取超时参数的配 置是否正确生效。
  2. 对于重试,因为HTTP 协议认为 Get 请求是数据查询操作,是无状态的,又考虑到网络出 现丢包是比较常见的事情,有些 HTTP 客户端或代理服务器会自动重试 Get/Head 请求。 如果你的接口设计不支持幂等,需要关闭自动重试。但更好的解决方案是,应该是 遵从 HTTP 协议的建议来使用合适的 HTTP 方法。
06丨20%的业务代码的Spring声明式事务,可能都没处理正确
  1. 因为配置不正确,导致方法上的事务没生效,@Transactional 生效原则 :
    1. 除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。
      原因:Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到, Spring 自然也无法动态增强事务处理逻辑。
    2. 必须通过代理过的类从外部调用目标方法才能生效
  2. 因为异常处理不正确,导致事务虽然生效但出现异常时没回滚,Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和noRollbackFor 属性来覆盖其默认设置。
  3. 如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的Propagation 属性。
07丨数据库索引:索引并不是万能药
  1. InnoDB 是如何存储数据的?
    1. 虽然数据保存在磁盘中,但其处理是在内存中进行的。为了减少磁盘随机读取次数,
      InnoDB 采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘
      中。InnoDB 的页大小,一般是 16KB。
      各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数
      据页中有一个页目录,方便按照主键查询记录。数据页的结构如下:
      Java业务开发常见错误100例(代码篇-1)
      文章图片

    2. 页目录通过槽把记录分成不同的小组,每个小组有若干条记录。如图所示,记录中最前面的 小方块中的数字,代表的是当前分组的记录条数,最小和最大的槽指向 2 个特殊的伪记 录。有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小 记录开始遍历整个页中的记录链表。
  2. 聚簇索引和二级索引
    1. InnoDB 使用 B+ 树,既可以保存实际数据,也可以加速数据搜索,这就是聚簇索引。由于数据在物理上只会保存一份,所以包含实际数据的聚簇索引只能有一个
    2. 为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级 索引,也是利用的 B+ 树的数据结构
  3. 不是所有针对索引列的查询都能用上索引
    1. 第一,索引只能匹配列前缀
    2. 第二,条件涉及函数操作无法走索引
    3. 第三,联合索引只能匹配左边的列
  4. 解决几个误区:
    1. 考虑到索引的维护代价、空间占用和查询时回表的代价,不能认为索引越多 越好。索引一定是按需创建的,并且要尽可能确保足够轻量。
    2. 不能认为建了索引就一定有效,对于后缀的匹配查询、查询中不包含联合索 引的第一列、查询条件涉及函数计算等情况无法使用索引。此外,即使 SQL 本身符合索引 的使用条件,MySQL 也会通过评估各种查询方式的代价,来决定是否走索引,以及走哪个 索引。
08丨判等问题:程序里如何确定你就是你?
  1. 首先,我们要注意 equals 和 == 的区别。业务代码中进行内容的比较,针对基本类型只能使用 ==,针对 Integer、String 在内的引用类型,需要使用 equals。Integer 和 String的坑在于,使用 == 判等有时也能获得正确结果(JVM缓存,例如:Integer会缓存[-128,127])。
  2. 其次,对于自定义类型,如果类型需要参与判等,那么务必同时实现 equals 和 hashCode方法,并确保逻辑一致。如果希望快速实现 equals、hashCode 方法,我们可以借助 IDE的代码生成功能,或使用 Lombok 来生成。如果类型也要参与比较,那么 compareTo 方法的逻辑同样需要和 equals、hashCode 方法一致。
  3. 最后,Lombok 的 @EqualsAndHashCode 注解实现 equals 和 hashCode 的时候,默认使用类型所有非 static、非 transient 的字段,且不考虑父类。如果希望改变这种默认行为,可以使用 @EqualsAndHashCode.Exclude 排除一些字段,并设置 callSuper = true来让子类的 equals 和 hashCode 调用父类的相应方法。
09丨数值计算:注意精度、舍入和溢出问题
  1. 务必不要使用Double作为金钱数值计算,因为浮点数计算会造成精度损失
  2. BigDecimal比较value,请使用compareTo
  3. 第一,切记,要精确表示浮点数应该使用 BigDecimal。并且,使用 BigDecimal 的Double 入参的构造方法同样存在精度丢失问题,应该使用 String 入参的构造方法或者BigDecimal.valueOf 方法来初始化
  4. 第二,对浮点数做精确计算,参与计算的各种数值应该始终使用 BigDecimal,所有的计算都要通过 BigDecimal 的方法进行,切勿只是让 BigDecimal 来走过场。任何一个环节出现精度损失,最后的计算结果可能都会出现误差
  5. 第三,对于浮点数的格式化,如果使用 String.format 的话,需要认识到它使用的是四舍五入,可以考虑使用 DecimalFormat 来明确指定舍入方式。但考虑到精度问题,我更建议使用 BigDecimal 来表示浮点数,并使用其 setScale 方法指定舍入的位数和方式
  6. 第四,进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但得到的计算结果是完全错误的。我们考虑使用 Math.xxxExact 方法来进行运算,在溢出时能抛出异常,更建议对于可能会出现溢出的大数运算使用 BigInteger 类
10丨集合类:坑满地的List列表操作
  1. 【Java业务开发常见错误100例(代码篇-1)】Arrays.asList 和 List.subList的使用
    1. Arrays.asList 得到的是 Arrays 的内部类 ArrayList,List.subList 得到的是 ArrayList 的
      内部类 SubList,不能把这两个内部类转换为 ArrayList 使用。
    2. Arrays.asList 直接使用了原始数组,可以认为是共享“存储”,而且不支持增删元素;
      List.subList 直接引用了原始的 List,也可以认为是共享“存储”,而且对原始 List 直接
      进行结构性修改会导致 SubList 出现异常。
    3. 对 Arrays.asList 和 List.subList 容易忽略的是,新的 List 持有了原始数据的引用,可能
      会导致原始数据也无法 GC 的问题,最终导致 OOM。
  2. Arrays.asList 不一定可以把所有数组转换为正确的 List。当传入基本类型数组的时候,List 的元素是数组本身,而不是数组中的元素
  3. 搜索超大 ArrayList 的时候遇到性能问题。我们考虑利用 HashMap 哈希表随机查找的时间复杂度为 O(1) 这个特性来优化性能,不过也要考虑 HashMap 存储空间上的代价,要平衡时间和空间
  4. 百分之九十的情况下,LinkedList在读写性能都没有ArrayList好

    推荐阅读