java业务开发注意事项 摘录

108 阅读11分钟

java业务开发注意事项 摘录

  1. ThreadLocal (java线程重用 导致存储信息出错)
    1. 线程池会重用固定的几个线程,一旦线程重用,则很可能获取到ThreadLocal中上个线程残留的数据,所以使用ThreadLocal时要注意set null
  2. ConcurrentHashMap (只能保证提供的原子性读写操作是线程安全的。)
    1. 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。
    2. 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。
    3. 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。
    4. 利用computeIfAbsent方法和线程安全的累加器 (在阿里开发手册中推荐用LongAdder替代AtomicLong
  3. CopyOnWriteArrayList (虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。)
    1. 在10w写操作CopyOnWriteArrayList 会比 ArrayList性能花费多百倍,而在100w的get操作中CopyOnWriteArrayList会比ArrayList快5倍
  4. 锁 (加锁前要清楚锁和被保护的对象是不是一个层面的)
    1. 静态字段属于类,类级别的锁才能保护;而非静态字段属于类实例,实例级别的锁就可以保护。
    2. 加锁要考虑锁的粒度和场景问题 (在Spring里bean默认是单例的,加上synchronized会导致整个程序几乎只能用单线程执行,造成极大的性能问题)
    3. 即使我们确实有一些共享资源需要保护,也要尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁。
    4. 如果性能要求高的情况下,可以考虑:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。
      1. 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。
      2. 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。
      3. JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。
    5. 多把锁要小心死锁问题
      1. 如果存在多把锁获取,可以把锁先排序在获取,避免死锁
    6. 超时自动释放锁后怎么避免重复逻辑
      1. 1. 避免超时,单独开一个线程给锁延长有效期。比如设置锁有效期30s,有个线程每隔10s重新设置下锁的有效期。
      2. 2. 避免重复,(保证幂等性)业务上增加一个标记是否被处理的字段。或者开一张新表,保存已经处理过的流水号。
  5. 线程池
    1. 线程池的工作行为
      1. 不会初始化 corePoolSize 个线程,有任务来了才创建工作线程;(可以通过prestartAllCoreThreads方法来初始化所有 corePoolSize 线程)
      2. 当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;
      3. 当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止;
      4. 如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;
      5. 当线程数大于核心线程数时,线程等待 keepAliveTime 后还是没有任务需要处理的话,收缩线程到核心线程数。
    2. 谨慎使用getThreadPool方法,每个getThreadPool方法都会返回一个新的线程池
    3. cpu密集型:
      1. cpu使用率较高(也就是一些复杂运算,逻辑处理),所以线程数一般只需要cpu核数的线程就可以了。 这一类型的在开发中多出现的一些业务复杂计算和逻辑处理过程中。
    4. I/O密集型:
      1. cpu使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余时间出来,所以通常就需要开cpu核数的两倍的线程, 当线程进行I/O操作cpu空暇时启用其他线程继续使用cpu,提高cpu使用率 通过上述可以总结出:线程的最佳数量: 最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。这一类型在开发中主要出现在一些读写操作频繁的业务逻辑中。
  6. http 调用
    1. 三个注意事项
      1. 超时时间:框架设置的默认超时是否合理;
      2. 失败重试:考虑到网络的不稳定,超时后的请求重试是一个不错的选择,但需要考虑服务端接口的幂等性设计是否允许我们重试;
      3. 重试次数:需要考虑框架是否会像浏览器那样限制并发连接数,以免在服务并发很大的情况下,HTTP 调用的并发数限制成为瓶颈。
    2. 超时时间细分:
      1. 连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间;
      2. 读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。
    3. Feign 和 Ribbon (feign 和 ribbon 是 Spring Cloud 的 Netflix 中提供的两个实现软负载均衡的组件,Ribbon 和 Feign 都是用于调用其他服务的,方式不同。)
      1. 结论一,默认情况下 Feign 的读取超时是 1 秒,如此短的读取超时算是坑点一;
      2. 结论二,也是坑点二,如果要配置 Feign 的读取超时,就必须同时配置连接超时,才能生效;
      3. 结论三,单独的超时可以覆盖全局超时,这符合预期;
      4. 结论四,除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。这里的坑点三是,参数首字母要大写,和 Feign 的配置不同;
      5. 结论五,同时配置 Feign 和 Ribbon 的超时,以 Feign 为准;
  7. Spring 声明式事务
    1. @Transactional 生效规则
      1. 1,除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。
        1. Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。
        2. 必须通过代理过的类从外部调用目标方法才能生效。
      2. 自己捕获异常可能会导致回滚不生效
        1. 如果想要生效可以在catch里 手动回滚事物TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        2. 在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):
      3. 如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。
  8. mysql 索引
    1. mysql数据存储形式
      1. 了减少磁盘随机读取次数,InnoDB 采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB 的页大小,一般是 16KB。
      2. 各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录
      3. 页目录通过槽把记录分成不同的小组。有了槽之后,我们按照主键搜索页中记录时,就可以采用二分法快速搜索,无需从最小记录开始遍历整个页中的记录链表。
    2. 聚簇索引和二级索引
      1. B+树的特点
        1. 最底层的节点叫作叶子节点,用来存放数据;
        2. 其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引;
        3. 非叶子节点分为不同层次,通过分层来降低每一层的搜索量;
        4. 所有节点按照索引键大小排序,构成一个双向链表,加速范围查找。
      2. 二级索引中只存储主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。
      3. 二级索引的代价
        1. 维护代价:创建 N 个二级索引,就需要再创建 N 棵 B+ 树,新增数据时不仅要修改聚簇索引,还需要修改这 N 个二级索引。
          1. 页中的记录都是按照索引值从小到大的顺序存放的,新增记录就需要往页中插入数据,现有的页满了就需要新创建一个页,把现有页的部分数据移过去,这就是页分裂;如果删除了许多数据使得页比较空闲,还需要进行页合并。页分裂和合并,都会有 IO 代价,并且可能在操作过程中产生死锁。
        2. 空间代价:虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间。(索引的空间占用可能远大于数据的存储空间)
        3. 回表的代价。二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们要的数据。
          1. 那么查询索引本身已经“覆盖”了需要的数据,不再需要回表查询。因此,这种情况也叫作索引覆盖。
    3. 索引的最佳实践
      1. 第一,无需一开始就建立索引,可以等到业务场景明确后,或者是数据量超过 1 万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用 EXPLAIN 命令,确认查询是否可以使用索引。我会在下一小节展开说明。
      2. 第二,尽量索引轻量级的字段,比如能索引 int 字段就不要索引 varchar 字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用 Elasticsearch 等专门用于文本搜索的索引数据库。
      3. 第三,尽量不要在 SQL 语句中 SELECT *,而是 SELECT 必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段,既能实现索引加速,又可以避免回表的开销。
    4. 索引匹配规则
      1. 第一,索引只能匹配列前缀。比如下面的 LIKE 语句,搜索 name 后缀为 name123 的用户无法走索引,执行计划的 type=ALL 代表了全表扫描:
      2. 第二,条件涉及函数操作无法走索引。
      3. 第三,联合索引只能匹配左边的列。
      4. 在 MySQL 5.6 及之后的版本中,我们可以使用 optimizer trace 功能查看优化器生成执行计划的整个过程。
SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";
  1. double 精度会越缺失
    1. 所以对于金钱的运算要用BigDecimal大数进行计算
    2. 对于钱精准度高的运算最好实现 money模式
  2. List坑
  3. Arrays.asList
    1. 不能直接使用 Arrays.asList 来转换基本类型数组
      1. 会将整个数组当成一个