当前位置: 首页 > 后端技术 > Java

Java业务开发100个常见错误(code-1)

时间:2023-04-01 23:35:37 Java

Java业务开发100个常见错误(code-1)大部分分为代码、设计和安全三个章节。01丨使用并发工具库,线程安全就安全了吗?使用ThreadLocal来缓存数据,认为ThreadLocal在线程之间进行了隔离,不会有线程安全问题。没想到线程重用导致数据串。请记得在业务逻辑结束前清理ThreadLocal中的数据。相信使用ConcurrentHashMap可以解决线程安全的问题,不加锁复合逻辑会导致业务逻辑错误。如果想在整个业务逻辑中保持容器运行的整体一致性,就需要加锁。对并发工具的特性了解不足,或者以旧方式使用新工具,导致无法发挥其性能。比如使用了ConcurrentHashMap,但是没有充分利用其提供的基于CAS的安全方式,仍然使用锁来实现逻辑。CopyOnWriteArrayList虽然是一个线程安全的ArrayList,但是其原理是写时复制,因此适用于读多写少的业务场景。我们首先要弄清楚共享资源是类级还是实例级,会操作哪些线程,synchronized关联的锁对象或方法的作用域是什么。加锁要尽可能考虑粒度和场景。受锁保护的代码意味着无法执行多线程操作。对于Web类型的天然多线程项目,大规模的加锁方式会显着降低并发能力。尽可能考虑只加锁必要的代码块,减少加锁的粒度;而对于需要超高性能的业务,还需要详细考虑锁的读写场景,悲观还是乐观优先。尝试尽可能细化特定场景的锁定方案。在合适的场景下,可以考虑使用ReentrantReadWriteLock、StampedLock等高级锁定工具。当业务逻辑中有很多锁时,就要考虑死锁问题。通常的回避方案是避免无限等待和循环等待。另外,如果业务逻辑中加锁的实现比较复杂,需要仔细检查加锁和释放是否配对,是否存在漏放或重复释放的可能;并且考虑到随着时间的推移,锁会自动释放,但是业务逻辑还在进行的情况下,如果其他线程或者进程拿到了同样的锁,可能会导致重复执行。如果您的业务代码涉及到复杂的锁操作,强烈建议在mock完相关的外部接口或数据库操作后,对应用代码进行压测,通过压测来排除误用锁导致的性能问题和死锁问题。03丨线程池:最常用也是最容易出错的业务代码组件Executors提供了一些快速声明线程池的方法。虽然简单,但是隐藏了线程池的参数细节。因此,我们在使用线程池时,一定要根据场景和需求配置合理的线程数量、任务队列、拒绝策略、线程回收策略,并明确命名线程,以方便排查问题。既然使用了线程池,那么就要保证线程池的复用。每次新的线程池出来,可能还不如不用线程池。如果没有直接声明线程池,而是使用其他同学提供的类库获取线程池,请务必查看源码,确认线程池的实例化方式和配置是否符合预期。重用线程池并不意味着应用程序总是使用同一个线程池。我们应该根据任务的性质选择不同的线程池。特别注意IO绑定任务和CPU绑定任务对线程池属性的偏好。如果要减少任务之间的相互干扰,可以考虑按需使用隔离线程池。04丨连接池:别让连接池帮你。连接池结构示意图:(常用的连接池包括:Redis连接池、HTTP连接池、数据库连接池)连接池实现方式:客户端SDK实现连接池的方式,包括池与连接分离、内连接池和非连接池。要正确使用连接池,首先要确定连接池的实现。比如JedisAPI实现了池和连接的分离,而ApacheHttpClient则是内置了连接池的API。使用姿势:一是保证连接池是多路复用的,二是在程序退出前显式关闭连接池尽可能释放资源。连接池参数配置:最重要的是最大连接数。很多高并发的应用往往会因为最大连接数不够用而导致性能问题。但是最大连接数并不是设置的越大越好,而是够用就行了。05丨HTTP调用:你考虑过超时、重试、并发吗?了解连接超时和读取超时之间的区别,并学习如何设置合适的超时参数。另外,在使用SpringCloudFeign等框架时,一定要确认连接和读取超时参数的配置是否正确。对于重试,由于HTTP协议认为Get请求是一个数据查询操作,是无状态的,并且考虑到网络丢包比较普遍,所以一些HTTP客户端或者代理服务器会自动重试Get/Head请求。如果你的界面设计不支持幂等性,你需要关闭自动重试。但更好的解决方案是使用HTTP协议建议的适当HTTP方法。06丨20%业务代码的Spring声明式事务可能因为配置不正确而没有正确处理,导致方法上的事务不生效。@Transactional生效原则:除非特殊配置(比如使用AspectJ静态织入实现AOP),否则只有定义在public方法上的@Transactional才能生效。原因:Spring默认通过动态代理实现AOP,增强了target方法。私有方法不能代理,Spring自然不能动态增强事务处理逻辑。必须通过代理类从外部调用目标方法才能生效。由于异常处理不正确,导致异常发生时事务生效但不回滚。默认情况下,Spring只会在出现RuntimeException和Error时响应标有@Transactional注解的方法。回滚,如果我们的方法捕获到异常,那么我们需要手动编写代码来处理事务回滚。如果希望Spring回滚其他异常,可以相应地配置@Transactional注解的rollbackFor和noRollbackFor属性来覆盖它们的默认设置。如果方法中涉及到多个数据库操作,希望将它们作为独立的事务进行提交或回滚,那么我们就需要考虑进一步细化事务传播的配置,即@Transactional注解的Propagation属性。07丨数据库索引:索引不是万能的InnoDB是如何存储数据的?尽管数据保存在磁盘上,但其处理是在内存中进行的。为了减少磁盘随机读次数,InnoDB采用页而不是行粒度来保存数据,即将数据分成若干页,以页为单位存储在磁盘上。InnoDB的页面大小一般为16KB。每个数据页组成一个双向链表,每个数据页中的记录按照主键的顺序组成一个单向链表;每个数据页都有一个页目录,方便根据主键查询记录。数据页的结构如下:页目录通过槽把记录分成不同的组,每个组有若干条记录。如图,记录中第一个小方块中的数字代表当前组中的记录数,最小槽和最大槽分别指向2条特殊的伪记录。有了slot,我们在根据主键查找页面中的记录时,就可以使用二分法快速查找,而不用从最小的记录开始遍历整个页面中的记录列表。聚集索引和二级索引InnoDB使用B+树,既可以保存实际数据,又可以加快数据查找速度。这就是聚簇索引。由于物理上只保存了一份数据副本,因此只能有一个包含实际数据的聚簇索引。为了实现非主键字段的快速搜索,引入了二级索引,也叫非聚集索引、辅助索引。二级索引也使用了B+树的数据结构。并非所有对索引列的查询都可以使用索引。首先是索引只能匹配列前缀。左边的列匹配解决了几个误区:考虑到索引的维护成本、空间占用、查询时的回表成本,不能认为索引越多越好。索引必须按需创建,并且尽可能轻量级。不能假设索引建立了就一定有效。该索引不能用于后缀匹配查询、不包括联合索引第一列的查询以及涉及函数计算的查询条件。另外,即使SQL本身满足使用索引的条件,MySQL也会对各种查询方式的开销进行评估,以决定是否使用索引以及使用哪个索引。08丨判断等问题:如何判断你是程序中的你?首先,我们需要注意equals和==的区别。业务代码中的内容比较,对于基本类型只能使用==,对于包括Integer、String在内的引用类型应该使用equals。Integer和String的坑是使用==判断有时可以得到正确的结果(JVM缓存,例如:Integer会缓存[-128,127])。其次,对于自定义类型,如果类型需要参与判断,需要同时实现equals和hashCode方法,保证逻辑一致。如果我们想快速实现equals和hashCode方法,可以使用IDE的代码生成功能或者使用Lombok来生成。如果类型也参与比较,compareTo方法的逻辑也需要和equals、hashCode方法保持一致。最后,Lombok的@EqualsAndHashCode注解实现equals和hashCode时,默认使用该类型的所有非静态非瞬态字段,与父类无关。如果想改变这种默认行为,可以使用@EqualsAndHashCode.Exclude排除一些字段,设置callSuper=true让子类的equals和hashCode调用父类对应的方法。09丨数值计算:注意精度、四舍五入和溢出问题。一定不要使用Double作为货币数值计算,因为浮点计算会造成精度损失。BigDecimal比较值。请先使用compareTo。请记住,要准确表示浮点数,您应该使用BigDecimal。另外,使用BigDecimal的Double输入参数的构造方法也存在精度损失的问题。应该使用String入参的构造方法或者BigDecimal.valueOf方法进行初始化。在使用BigDecimal时,所有的计算都必须通过BigDecimal方法进行,所以不要让BigDecimal走过场。如果任何一个环节出现精度损失,最终的计算结果都可能出现错误。第三,对于浮点数的格式化,如果使用String.format,需要意识到它使用了四舍五入。您可以考虑使用DecimalFormat来显式指定舍入方式。但是考虑到精度问题,我推荐使用BigDecimal来表示浮点数,使用它的setScale方法来指定位数和四舍五入的方法。第四,进行数值运算时要注意溢出。虽然溢出后不会出现异常,但是得到的计算结果是完全错误的。我们考虑使用Math.xxxExact方法进行计算,溢出时可以抛出异常,对于可能溢出的大数计算建议使用BigInteger类。.subList使用Arrays.asList获取ArrayList的内部类ArrayList,使用List.subList获取ArrayList的内部类SubList。这两个内部类不能转为ArrayList使用。Arrays.asList直接使用原始数组,可以认为是共享“存储”,不支持元素的增删改;List.subList直接引用原List,也可以认为是共享“存储”,直接实现原List的结构修改会导致SubList异常。Arrays.asList和List.subList容易忽略的一点是,新的List持有对原始数据的引用,这可能会导致原始数据无法被GC的问题,最终导致OOM。Arrays.asList可能无法将所有数组转换为正确的列表。当传入一个基本类型的数组时,List的元素就是数组本身,而不是数组中的元素。搜索大型ArrayList时会遇到性能问题。我们考虑使用HashMap哈希表随机查找的时间复杂度为O(1)来优化性能,同时也考虑HashMap存储空间的成本,来平衡时间和空间90%,LinkedList在读写性能上,是不如ArrayList