稳定性可靠性设计--源码篇

Posted 快乐崇拜234

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了稳定性可靠性设计--源码篇相关的知识,希望对你有一定的参考价值。

正文在下面,先打个广告:

文章目录

概述

稳定性可靠性设计计划写4篇文章:

  • 源码篇
  • 网络篇
  • 流程篇
  • 架构篇
  • 存储篇
  • 前端篇
  • 大数据计算

本文是第一篇,源码篇。从源码角度来看如何写出稳定性高的系统。

我这里把可靠性的内容也一起讲了,稳定性和可靠性也没必要分的太清,相辅相成的。

其实目前已经有很多很优秀的代码规范可以很好的约束程序员写出可靠的,健壮的,稳定的代码。比如阿里的《泰山版java开发规范》,还有SQL规范等等。
我这里会挑几个有代表性的案例跟大家介绍一下为什么需要有这些规范,坏味道的代码会带来哪些问题。
更加详细的编程规范读者可以到网上去下载阿里的java相关的开发规范。

以下文章基本都是我的个人博客的内容,可以百度搜索我的名字,第一条就是。

日志与异常告警

参加我的博客: 日志打印规范

空指针异常

  • 基本每个使用到的对象,返回值,都要考虑是否会存在空都情况,做出正确判断。
  • 对于一些情况,当返回空时,可以采用返回非空对象的方式来代替。比如查询数据返回空集合Collections.emptyList()getOrDefault方法。
  • 使用 if(null == obj) 而不是 if(obj == null) 来避免不必要的空指针异常

入参校验

  • 服务端不可以相信前端任何输入。需要对入参做严格都校验。
  • 微服务间,下层服务也需要对上层服务都入参做校验,不要以为调用方都是一个部门或公司的就忽略了
  • 对于批量操作,务必对请求都数据量做校验,避免大SQL或内存撑爆

不变模式

不变模式最主要都优点就是线程安全。目前流行的响应式编程也广泛应用。
缺点:不可变类的每一次“改变”都会产生新的对象,因此在使用中不可避免的会产生很多垃圾。

Integer

反例:用 == 判断是否相等

反例:被锁对象是不变模式都对象

关于Integer数值比较的问题以及不可变对象

重写equals()方法必须要重写hashCode()方法

JDK11源码–HashMap源码分析

线程安全

ThreadLocal

注意有些类不是线程安全的

  • SimpleDateFormat
    SimpleDateFormat不是线程安全的,这是常见的错误。需要使用threadlocal或者每次new。
    建议java8使用 DateTimeFormatter(This class is immutable and thread-safe.)
  • Random
    解决方法仍然是threadlocal,java7以后提供了ThreadLocalRandom

当然还有后很多其他的,这里只是抛砖引玉,确保线程安全性

volatile 不能保证原子性

正确的try-cache-finally

在以下场景中【CountDownLatch/CyclicBarrier/循环任务/线程池】,需要保证每次任务执行,每次循环都要try-cache,确保必要的资源回收以及任务之间不影响(当然还要具体业务具体分析)。

  • 不可变对象
private Integer i;
//代码省略
synchronized (i)...
  • 锁的范围尽可能小,但也不要在for循环内加锁
  • 到底是不是同一把锁?类级别,实例级别, 锁的范围
  • 死锁:如何避免死锁
  • 乐观锁:一定会提升性能吗?适用于什么场景?
  • 读写锁
  • 务必在finally中释放锁

不要使用try-cache控制业务逻辑

特别注意不要使用具体异常都内容来进行一些业务逻辑判断 JIT实时编译优化带来的问题:几千次异常以后取不到错误信息了

事务

  • 事务使用spring注解@Transactional时,务必添加rollbackFor
  • 多数据源时,务必每个@Transactional都指定数据源,避免遗漏出错
  • @Transactional失效场景:6种@Transactional注解的失效场景
  • 事务隔离级别

BigDecimal

很多同学对BigDecimal都使用姿势是不对的。

  • 商业计算使用BigDecimal。构造函数一定要用new BigDecimal(String str)方法。
  • 使用参数类型为String的构造函数或valueOf()方法。但不要传float类型的值,如果不确定,则用new BigDecimal(String str)方法。
  • BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
  • 有很多bigdecimal的计算使用的是【BigDecimal.ROUND_HALF_DOWN】保留小数位。一般来说使用BigDecimal.ROUND_HALF_UP(四舍五入)或者BigDecimal.ROUND_HALF_EVEN(银行家算法)比较好。虽然现在业务对精度要求不高,但是还要养成好的习惯
  • 除法需要使用这个签名都方法:java.math.BigDecimal#divide(java.math.BigDecimal, int, int)
  • 比较使用compareTo而不是equals。除非你如果需要比较小数位都位数

不要以为你用了BigDecimal后,计算结果就一定精确了

正确资源释放

  • try-finally
  • try-with-resources
  • ShutdownHook

设计模式

设计模式教材很多,自己去看书学习吧。

池化

java线程池

建议使用ThreadPoolExecutor来创建线程池。

 public ThreadPoolExecutor(int corePoolSize,
                      int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) 
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);

实例

new ThreadPoolExecutor(5, 10, 2000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));

new ThreadPoolExecutor(4, 8, 20L, TimeUnit.MINUTES, new LinkedBlockingDeque<>())

//默认拒绝策略
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();

CompletableFuture 外部传入线程池。因为默认是fork-join pool,它的默认线程数是CPU核数。

如何确定一个线程池中线程数量?
N threads = N CPU * U CPU * (1 + W/C)
其中:

  • N CPU 是处理器的核的数目,可以通过 Runtime.getRuntime().availableProce-
    ssors() 得到
  • U CPU 是期望的CPU利用率(该值应该介于0和1之间)
  • W/C是等待时间与计算时间的比率

简单来讲,一般的设置线程池的大小规则是:

  • 如果服务是cpu密集型的,设置为电脑的核数
  • 如果服务是io密集型的,设置为电脑的核数*2

其他线程池

tomcat线程池,dubbo线程池等

连接池

数据库连接池
redis连接池

druid网络上推荐配置:

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
        init-method="init" destroy-method="close">
        <!-- 基本属性 url、user、password -->
        <property name="url" value="$jdbc_url" />
        <property name="username" value="$jdbc_user" />
        <property name="password" value="$jdbc_password" />

        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="5" />
        <property name="minIdle" value="5" />
        <property name="maxActive" value="10" />
        <!-- 配置从连接池获取连接等待超时的时间 -->
        <property name="maxWait" value="10000" />

        <!-- 配置间隔多久启动一次DestroyThread,对连接池内的连接才进行一次检测,单位是毫秒。
            检测时:1.如果连接空闲并且超过minIdle以外的连接,如果空闲时间超过minEvictableIdleTimeMillis设置的值则直接物理关闭。2.在minIdle以内的不处理。
        -->
        <property name="timeBetweenEvictionRunsMillis" value="600000" />
        <!-- 配置一个连接在池中最大空闲时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <!-- 设置从连接池获取连接时是否检查连接有效性,true时,每次都检查;false时,不检查 -->
        <property name="testOnBorrow" value="false" />
        <!-- 设置往连接池归还连接时是否检查连接有效性,true时,每次都检查;false时,不检查 -->
        <property name="testOnReturn" value="false" />
        <!-- 设置从连接池获取连接时是否检查连接有效性,true时,如果连接空闲时间超过minEvictableIdleTimeMillis进行检查,否则不检查;false时,不检查 -->
        <property name="testWhileIdle" value="true" />
        <!-- 检验连接是否有效的查询语句。如果数据库Driver支持ping()方法,则优先使用ping()方法进行检查,否则使用validationQuery查询进行检查。(Oracle jdbc Driver目前不支持ping方法) -->
        <property name="validationQuery" value="select 1 from dual" />
        <!-- 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 -->
        <!-- <property name="validationQueryTimeout" value="1" />  -->

        <!-- 打开后,增强timeBetweenEvictionRunsMillis的周期性连接检查,minIdle内的空闲连接,每次检查强制验证连接有效性. 参考:https://github.com/alibaba/druid/wiki/KeepAlive_cn -->
        <property name="keepAlive" value="true" />  

        <!-- 连接泄露检查,打开removeAbandoned功能 , 连接从连接池借出后,长时间不归还,将触发强制回连接。回收周期随timeBetweenEvictionRunsMillis进行,如果连接为从连接池借出状态,并且未执行任何sql,并且从借出时间起已超过removeAbandonedTimeout时间,则强制归还连接到连接池中。 -->
        <property name="removeAbandoned" value="true" /> 
        <!-- 超时时间,秒 -->
        <property name="removeAbandonedTimeout" value="80"/>
        <!-- 关闭abanded连接时输出错误日志,这样出现连接泄露时可以通过错误日志定位忘记关闭连接的位置 -->
        <property name="logAbandoned" value="true" />

        <!-- 根据自身业务及事务大小来设置 -->
        <property name="connectionProperties"
            value="oracle.net.CONNECT_TIMEOUT=2000;oracle.jdbc.ReadTimeout=10000"></property>

        <!-- 打开PSCache,并且指定每个连接上PSCache的大小,Oracle等支持游标的数据库,打开此开关,会以数量级提升性能,具体查阅PSCache相关资料 -->
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
            value="20" />   

        <!-- 配置监控统计拦截的filters -->
        <!-- <property name="filters" value="stat,slf4j" /> -->

        <property name="proxyFilters">
            <list>
                <ref bean="log-filter" />
                <ref bean="stat-filter" />
            </list>
        </property>
        <!-- 配置监控统计日志的输出间隔,单位毫秒,每次输出所有统计数据会重置,酌情开启 -->
        <property name="timeBetweenLogStatsMillis" value="120000" />
    </bean>

其中特别关心testOnBorrow/testOnReturn/testWhileIdle三个配置。

问题1:maxActive这个参数。一个数据库连接池到底应该配置多大?

PostgreSQL提供了一个公式:连接数 = ((核心数 * 2) + 有效磁盘数)
核心数不应包含超线程(hyper thread),即使打开了hyperthreading也是。如果活跃数据全部被缓存了,那么有效磁盘数是0,随着缓存命中率的下降,有效磁盘数逐渐趋近于实际的磁盘数。这一公式作用于SSD时的效果如何尚未有分析。
公理:你需要一个小连接池,和一个充满了等待连接的线程的队列
我之前做过的一个日活300W的应用,单机数据库链接也才配置了20。

问题2:数据库上层的应用集群特别多时,如何配置?
假设数据库最多支持100个链接,你有100台服务器,如何设计呢?难道要每台机器限制1个链接吗?

解决方案一:代理层。

堆内缓存与堆外缓存

计算缓存内容占用内存空间,考虑GC的影响。
数据量大时可以考虑堆外缓存。

Java对象占用堆内存大小计算

常用换成淘汰策略:

  • FIFO先进先出
  • LRU(The Least Recently Used,最近最久未使用算法)
  • LFU(Least Frequently Used ,最近最少使用算法)

LRU算法在MySQL/hbase/Caffeine 中的优化

超时与重试

连接超时,读超时,写超时

微服务调用链路,a–>b–>c 通常,a掉b的超时时间,要大于b掉c的超时时间。
重试几次?不重试?退避算法
幂等

CAS

使用CAS要注意ABA问题。
CAS不适用于高并发的场景,会大大加重CPU负担
只能保证一个共享变量的原子操作。如果想保证对象中多个变量的原子性,可以使用AtomicReference将过个变量合并为一个变量。或者使用锁。

ABA解决:

  • 版本号(适用于持久化的数据库等)
  • AtomicStampedReference

java高并发:CAS无锁原理及广泛应用

redis

db

以上是关于稳定性可靠性设计--源码篇的主要内容,如果未能解决你的问题,请参考以下文章

全链路非功能测试之服务资源监控工具篇

MySQL高性能高可用表设计实战-表设计篇(MySQL专栏启动)

数据结构与算法篇-基数排序

直播源码技术控制直播稳定之消息篇

Android进阶篇最新Android源码精编解析,有效阅读源码的法门

IVI15.1.3 系统稳定性优化篇(LMKD Ⅲ)LMKD的设计原则