1. 线程与性能相关
类别 |
总结 |
性能问题排查步骤 |
1. CPU占比过高: 1.1 top命令查看CPU占比高的进程,然后top -Hp pid查看异常线程; 1.2 jstack pid查看进程的各线程栈信息,找到异常线程的堆栈信息,分析堆栈调用的类、方法; 2. GC相关信息: jstat 命令,查看新生代、老年代GC的次数、时间、以及内存使用等信息; 3. 内存数据分析: jmap + jhat 命令,dump内存文件到本地,然后MAT工具分析内存中对象占比,及产生的源头。 jmap -dump : 生成Java堆转储快照 jmap -heap:显示Java堆详细信息 jmap -histo:like |more:显示堆中对象统计信息 |
定时任务与线程池 |
定时任务不适用线程池模型,定时任务触发时刻瞬时压力大,其他时间基本不执行;使用线程池模型则在触发时线程池瞬间被塞满任务,且容易出现拒绝的情况;但是处理完之后几乎全部空闲,浪费线程资源; 一般根据任务触发时预计处理数据量来估计使用方式,比如一万条数据以内,一次性取出全部数据,然后循环串行执行完;如果数据量较大,则考虑分批次每次取1000条数据处理; |
druid与dubbo线程池 |
获取driud conn导致dubbo线程池被占满:查看jstack发现线程状态waiting,且卡在获取连接处。查看源码定位到breakAfterAcquireFailure设置为true,当获取连接失败时当前线程会退出,就会出现栈中没有其他线程的情况; 解决办法:1.设置获取连接的超时时间,2. breakAfterAcquireFailure不设置为true; |
线程池中线程的执行 |
线程池在调度的时候执行的Runnable的run()方法,而不是start()方法。如果是start()方法,那么当往线程池仍过多的任务时,线程池会额外创建线程,同是每个任务都是异步执行的,所有的任务一下子就执行完了。如果是要通过线程池调度,那么只能使用同步阻塞的run()方法调用才满足这个要求。 |
start()方法与run()方法 |
1. Thread.start() 方法用来异步启动一个线程,然后主线程立刻返回。该启动的线程不会马上运行,会放到等待队列中等待 CPU 调度,只有线程真正被 CPU 调度时才会调用 run() 方法执行。 2. 如果直接调用 run() 方法,那就等于调用了一个普通的同步方法,达不到多线程运行的异步执行。 |
线程池的hystrix隔离 |
|
信号量的hystrix隔离 |
|
2. 接口对接
类别 |
总结 |
对外SDK设计 |
1. SDK接口参数尽量设计成通用参数,不用频繁升级,如参数放在context中,业务方动态扩展; |
2. SDK参数灵活设置,则后台接入层需要校验参数,包括key是否存在,value是否为null,类型是否匹配,能否有默认值等 |
|
3. SDK中所有入参、出参一定要设置序列号,否则容易出现新老包因序列化导致的不兼容问题,或使用jsom格式、context格式等; |
|
4. SDK升级,老版本一定要回归测试,业务方比一定也升级SDK,确保SDK能向下兼容; |
|
5. SDK中最好包含调用者信息,如token;便于后台服务统计和监控调用者请求量、QPS,甚至设计降级和熔断策略; |
|
前后端接口对接 |
对外提供接口升级一定要确保兼容使用老版本的调用方,入参和出参均需要序列化,建议使用json格式传递参数; |
1. 所有需要处理的数据(包括转换、赋值、判断等)一定要判断是否为空,避免出现空指针异常; |
|
2. 数据转换时一定校验为空的情况,避免传入数据为空导致转换时报异常; |
|
3. 枚举值空值处理,基本类型与包装类型的空处理,实体Bean的定义采用包装类型,避免基本类型的自动初始化赋值; |
|
4. 时间、日期等在后端封装成String后返回至前端,避免传输时丢失UTC时间; |
|
5. long型数据传至前端时最好转换成string(16位以上数据可能会丢失精度); |
|
6. post请求以json格式传参,get请求以form格式传参,与前端规范保持一致; |
|
7. 未查询到数据返回code值200,返回信息说明未查询到数据,注意与异常值-1区分开; |
|
8. 返回前端的状态值最好带上desc,例如(0571,杭州),(1,已支付)等; |
|
9. 对外提供服务要打印接口请求参数和返回结果,便于问题排查,参数map不建议用json格式打印,会丢失value为null的key; |
|
定时任务相关 |
9. 定时任务预留手动触发接口,万一线上数据跑错后可以重新跑;另外统计数据一定要做到幂等,多次统计结果保持正确; |
10. 文件操作(excel、txt等)一定要注意数据量的大小,必须有限定条件限制数据量,但数据量太大时可能会导致处理时间过长、内存被占满等问题,或者是当后台还未处理完,但http请求认为已经超时后重新请求,导致重复处理,使问题更严重。最好还能限制并发操作,避免同时请求导致内存被耗光。 |
|
props.load(new FileInputStream("db.properties")); 是读取当前目录的db.properties文件
getClass.getResourceAsStream("db.properties"); 是读取当前类所在位置一起的db.properties文件
getClass.getResourceAsStream("/db.properties"); 是读取ClassPath的根的db.properties文件,注意ClassPath如果是多个路径或者jar文件的,只要在任意一个路径目录下或者jar文件里的根下都可以,如果存在于多个路径下的话,按照ClassPath中的先后顺序,使用先找到的,其余忽略。 |
|
其他 |
1. SpringBoot支持动态的读取文件,扩展接口为:EnvironmentPostProcessor; |
2. MongoSocketOpenException异常,原因是springboot自动配置了支持mongodb。在启动springboot时会自动实例化一个mongo实例,需要禁用自动配置。 增加 @SpringBootApplication ( exclude = MongoAutoConfiguration.class)配置即可; |
|
3. 手动指定外部logback加载路径,添加配置项:logging.config=D:/logs/mq-demo/logback.xml |
|
4. String操作类的replace和replaceAll的区别(replace替换字符串命中,replaceAll替换正则表达式命中) |
|
5. 如果在try语句里有return语句,finally语句还是会执行。它会在把控制权转移到该方法的调用者或者构造器前执行finally语句。也就是说,使用return语句把控制权转移给其他的方法前会执行finally语句。 |
|
es查询深度分页问题 |
1. es搜索原理:当执行查询10000~10100条记录时,es节点将请求广播到各分片,各分片查前10100条记录。然后查询结果返回给es节点,es节点都结果合并整合后,取相应数据返回给client; 2. 问题:如果此方式查1000000~1000100条记录时,cpu、内存、网络io可能都会出现问题;所以会有修改index.max_result_window 值的方式来限制from/size的执行范围,即不能超过设定值,否则报错; 3. 更高效的深度分页方式:Scroll 或者SearchAfter。scroll常用于离线的读取深度分页数据,SearchAfter用于实时和高并发场景,执行原理类似与传统数据库里的cursors(游标),如果是不关心顺序的场景,可以考虑使用scroll-scan。 4. Scroll方式在一次查询请求后维护一个索引快照的search context,然后每次再去批量的读取数据,效率比较高,缺点是维护search context需要占用很多资源;searchAfter的方式通过维护一个实时游标来避免scroll的缺点,可以用于实时请求和高并发场景,缺点是不能够随机跳转分页,只能是一页一页的向后翻,并且需要至少指定一个唯一不重复字段来排序。 |
线上空指针问题排查 |
1. 线上频繁报空指针异常,error日志打印过多,不再打印堆栈信息,不知道去查看最开始的日志信息,结果需要手动审查代码,难以定位问题; 2. 请求参数打印时,json打印map参数不打印值为null的key,导致本地测试map参数和线上不一致,无法重现问题; 3. map取值校验时,判断key是否存在后,取值要先做空判断再转类型,彻底避免空指针异常的出现; |
3. myBatis使用细节
描述 |
解释 |
错误:with a primitive return type (int). |
返回值类型为int,但是查询出来的结果有空值(null),无法转换成基本类型。包括char,long,short都有可能。 解决方式:1. select ifnull(value, 0) from xxx;结果为空时默认返回返回值0。2. 如果where 条件命中不上,select语句则不会执行(where语句比select先执行),返回值仍为null。此情况一是使用 select case when then语句。二是修改Mapper接口中的返回值类型,改用包装类(Integer,Short,Long等),并在业务逻辑中处理null值。
select CASE WHEN (select provinceID from kdmc_t_province where name =#{province})is null THEN 0 ELSE (select provinceID from kdmc_t_province where name =#{province}) END; |
resultType和resultMap区别 |
查询出来的结果集只有一行且一列,可以使用简单类型进行输出映射。通过resultType映射。 如果查询结果复杂,需要封装成bean,则适合使用resultMap,定义映射规则。 |
动态传入表名,例如数据记录日表 |
先区分#{}和${}的却别,简略来讲${}单纯填充字段,不做预编译;#{}有预编译过程,可以防止SQL注入。(#{} 是编译好SQL语句再取值,${} 这是取值以后再去编译SQL语句) 那么,动态传入表名则需要使用${}方式,例如select * from ${prefix}ACT_HI_PROCINST where PROC_INST_ID_ = #{processInstanceId},另外SQL语句配置为statementType="STATEMENT",即不进行预编译。 |
mySQL时间选择 |
TIMESTAMP 和时区相关,更能反映当前时间。当插入日期时,会先转换为本地时区后再存放;当查询日期时,会将日期转换为本地时区后再显示。所以不同时区的人看到的同一时间是不一样的。注:mySQL中timestamp范围从1970年~2038年。 |
多参数入参 |
单个参数配置文件映射时能自动识别,多参数则需要指定参数,一是通过Map传递,二是封装成bean,三是在接口通过注解标明。例如: Integer getAccountPermissionValue( @Param("publicId") long publicId, @Param("prvCode") String prvCode ) throws Exception; |
随机数生成 |
private String getFixLenthString(int strLength) { Random rm = new Random(); // 获得随机数 double pross = (1 + rm.nextDouble()) * Math.pow(10, strLength); // 将获得的获得随机数转化为字符串 String fixLenthString = String.valueOf(pross); // 返回固定的长度的随机数 return fixLenthString.substring(2, strLength + 2); } |
insert返回主键id |
1. xml文件中insert语句配置useGeneratedKeys="true" keyProperty="id" keyColumn="id"即可,对于insert ignore into,insert into on duplicate key update不生效。 2. param需以bean的方式传入,且在接口通过bean.getId()获取主键id,sql返回结果只表示执行的状态(插入成功的行数,即1)。 |
resultMap示例 |
|
批量插入示例 |
SELECT LAST_INSERT_ID() insert into redeem_code ( bach_id, code, type, facevalue, create_user, create_time ) values ( #{item.batchId}, #{item.code}, #{item.type}, #{item.facevalue}, #{item.createUser}, #{item.createTime} ) |
建表脚本示例 |
CREATE TABLE `risk_platform`.`rp_rule_center_rule` ( `id` bigint(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `scene_no` bigint(10) NOT NULL COMMENT '场景No', `active_type` tinyint(4) NOT NULL COMMENT '触发类别,1:实时触发,2:定时触发', `event_no` bigint(10) NOT NULL COMMENT '事件No', `rule_code` varchar(50) NOT NULL COMMENT '规则代号', `rule_name` varchar(50) NOT NULL COMMENT '规则名称', `rule_desc` varchar(2048) DEFAULT NULL COMMENT '规则描述', `biz_lines` varchar(50) DEFAULT NULL COMMENT '适用业务线', `run_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '运行状态, 0:未启用, 1:运行, 2:观察', `start_active_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '有效时间', `end_active_time` timestamp NOT NULL DEFAULT '2038-01-01 00:00:00' COMMENT '有效时间', `condition_exps` varchar(2048) NOT NULL COMMENT '条件表达式', `punish_exps` varchar(2048) NOT NULL COMMENT '处罚表达式', `contents` varchar(2048) NOT NULL COMMENT '完整的规则表达式', `related_features` varchar(2048) NOT NULL COMMENT '规则关联特征列表', `level` tinyint(4) NOT NULL DEFAULT '0' COMMENT '优先级', `deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除标识, 0:未删除, 1:已删除', `remark` varchar(2048) DEFAULT NULL COMMENT '备注', `md5` varchar(50) NOT NULL COMMENT 'MD5签名', `create_user` varchar(50) DEFAULT NULL COMMENT '创建者', `update_user` varchar(50) DEFAULT NULL COMMENT '修改者', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_rce_rule_code` (`rule_code`) USING BTREE COMMENT '特征编码唯一约束', KEY `idx_create_time` (`create_time`) USING BTREE, KEY `idx_update_time` (`update_time`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=COMPACT COMMENT='规则中心_规则表'; |
表变更sql语句 |
ALTER TABLE risk_platform.rp_rule_center_rule ADD rule_type tinyint(4) NOT NULL DEFAULT '0' COMMENT '策略类型, 0:表达式策略, 1:积分策略'; ALTER TABLE risk_platform.rp_rule_center_rule_condition MODIFY true_value varchar(30) DEFAULT NULL COMMENT '正确结果集:true用1表示,其他数值表示符合条件时积分分数'; ALTER TABLE risk_platform.rp_rule_center_rule_condition MODIFY false_value varchar(30) DEFAULT NULL COMMENT '正确结果集:true用1表示,其他数值表示符合条件时积分分数';
ALTER TABLE risk_platform.rp_rule_center_rule_punish ADD left_threshold varchar(50) DEFAULT NULL COMMENT '左阈值'; ALTER TABLE risk_platform.rp_rule_center_rule_punish ADD right_threshold varchar(50) DEFAULT NULL COMMENT '左阈值';
ALTER TABLE risk_center.rc_risk_config MODIFY COLUMN value text COMMENT '参数值' AFTER cn_name; |
插入数据脚本 |
INSERT INTO `user_system`.`ac_physics_system` (`physics_system_name`, `host`, `gray_host`, `del_flag`) VALUES ( 'risk-platform', 'https://center.caocaokeji.cn', '', 0);
INSERT INTO `risk_platform`.`rp_punish_action` (`code`, `name`, `type`, `description`, `create_time`, `update_time`, `deleted`, `remark`, `priority`) VALUES ( '701000', 'REGISTER_CLUSTER', NULL, '注册账号聚集', '2020-02-20 15:20:28', '2020-03-02 14:47:21', '0', NULL, '30'); INSERT INTO `risk_platform`.`rp_punish_action` (`code`, `name`, `type`, `description`, `create_time`, `update_time`, `deleted`, `remark`, `priority`) VALUES ( '702000', 'REGISTER_FREQUENTLY', NULL, '高频注册', '2020-02-20 15:20:59', '2020-03-02 14:47:40', '0', NULL, '20'); INSERT INTO `risk_platform`.`rp_punish_action` (`code`, `name`, `type`, `description`, `create_time`, `update_time`, `deleted`, `remark`, `priority`) VALUES ( '704000', 'REGISTER_CLIENT_ABNORMAL', NULL, '注册终端异常', '2020-02-20 15:21:43', '2020-03-02 14:47:42', '0', NULL, '10'); |