来吧,教你如何减少Bug!
往期热门文章:
1、卷不动了?300 秒快速了解 Java 9 - 16 新特性,助你脱离内卷 2、Java必会的工具库,让你的代码量减少90% 3、Java 程序员常犯的 10 个 SQL 错误! 4、摸鱼王! 5、抖音服务器带宽有多大,才能供上亿人同时刷?
前言
1. 数据库篇
1.1 慢查询
1.1.1 是否命中索引
查询条件包含or,可能导致索引失效 如何字段类型是字符串,where时一定用引号括起来,否则索引失效 like通配符可能导致索引失效。 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。 在索引列上使用mysql的内置函数,索引失效。 对索引列运算(如,+、-、*、/),索引失效。 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。 索引字段上使用is null, is not null,可能导致索引失效。 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。 mysql估计使用全表扫描要比使用索引快,则不使用索引。
1.1.2 数据量大,考虑分库分表
1.1.3 不合理的SQL
1.2 数据库字段注意点
1.2.1 字段是否会超长
`name` varchar(255) DEFAULT NOT NULL
1.2.2 字段为空,是否会导致空指针等
如果是整形,我们一般使用0或者-1作为默认值。 如果字符串,默认空字符串
NULL
值,容易导致程序空指针;如果数据库字段设置为NULL
值,需要注意count(具体列) 的使用,会有坑。1.2.3 字段缺失
1.2.4 字段类型是否支持表情
1.2.5 谨慎使用text、blob字段
1.3 事务失效的场景
1.3.1 @Transactional 在非public修饰的方法上失效
1.3.2 本地方法直接调用
public class TransactionTest{
public void A(){
//插入一条数据
//调用方法B (本地的类调用,事务失效了)
B();
}
@Transactional
public void B(){
//插入数据
}
}
1.3.3 异常被try...catch吃了,导致事务失效。
@Transactional
public void method(){
try{
//插入一条数据
insertA();
//更改一条数据
updateB();
}catch(Exception e){
logger.error("异常被捕获了,那你的事务就失效咯",e);
}
}
1.3.4 rollbackFor属性设置错误
unchecked
异常(继承自RuntimeException 的异常)或者Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,就需要指定rollbackFor
属性。1.3.5 底层数据库引擎不支持事务
1.3.6 spring事务和业务逻辑代码必须在一个线程中
@Transactional
public void mothed() {
new Thread() {
事务操作
}.start();
}
1.4 死锁
1.4.1 9种情况的SQL加锁分析
组合一:id列是主键,RC隔离级别 组合二:id列是二级唯一索引,RC隔离级别 组合三:id列是二级非唯一索引,RC隔离级别 组合四:id列上没有索引,RC隔离级别 组合五:id列是主键,RR隔离级别 组合六:id列是二级唯一索引,RR隔离级别 组合七:id列是二级非唯一索引,RR隔离级别 组合八:id列上没有索引,RR隔离级别 组合九:Serializable隔离级别
1.4.2 如何分析解决死锁?
模拟死锁场景 show engine innodb status;查看死锁日志 找出死锁SQL SQL加锁分析,这个可以去官网看哈 分析死锁日志(持有什么锁,等待什么锁) 熟悉锁模式兼容矩阵,InnoDB存储引擎中锁的兼容性矩阵。
1.5 主从延迟问题考虑
1.5.1 要求强一致性,考虑读主库
1.5.2 不要求强一致性,读从库
1.6 新老数据兼容
1.6.1 新加的字段,考虑存量数据的默认值
scene_type
,它的枚举值是 01、02、03
,那我们就要跟业务对齐,新添加的字段,老数据是什么默认值,是为空还是默认01,如果是为NULL
的话,程序代码就要做好空指针处理。1.6.2 如果新业务用老的字段,考虑老数据的值是否有坑
01:超级管理员 02:管理员 03:一般用户
。假设业务需求是一般用户拆分为03查询用户和04操作用户,那我们在开发中,就要考虑老数据值的问题啦。1.7 一些SQL的经典注意点
1.7.1 limit大分页问题
select id,name from employee where id>1000000 limit 10.
SELECT a.* FROM employee a, (select id from employee where 条件 LIMIT 1000000,10 ) b where a.id=b.id
1.7.2 修改、查询数据量多时,考虑分批进行。
remoteBatchQuery(param);
for(int i=0;i<100000;i++){
remoteSingleQuery(param)
}
2. 代码层面篇
2.1 编码细节
2.1.1 六大典型空指针问题
包装类型的空指针问题 级联调用的空指针问题 Equals方法左边的空指针问题 ConcurrentHashMap 类似容器不支持 k-v为 null。 集合,数组直接获取元素 对象直接获取属性
if(object!=null){
String name = object.getName();
}
2.1.2 线程池使用注意点
使用 Executors.newFixedThreadPool,可能会出现OOM问题,因为它使用的是无界阻塞队列 建议使用自定义的线程池,最好给线程池一个清晰的命名,方便排查问题 不同的业务,最好做线程池隔离,避免所有的业务公用一个线程池。 线程池异常处理要考虑好
2.1.3 线性安全的集合、类
HashMap
可能会出现死循环。因为它是非线性安全的,可以考虑使用ConcurrentHashMap
。所以我们使用这些集合的时候,需要注意是不是线性安全的。Hashmap、Arraylist、LinkedList、TreeMap等都是线性不安全的; Vector、Hashtable、ConcurrentHashMap等都是线性安全的
2.1.4 日期格式,金额处理精度等
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 31);
Date testDate = calendar.getTime();
SimpleDateFormat dtf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("2019-12-31 转 YYYY-MM-dd 格式后 " + dtf.format(testDate));
2019-12-31 转 YYYY-MM-dd 格式后 2020-12-31
public class DoubleTest {
public static void main(String[] args) {
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);
double amount1 = 3.15;
double amount2 = 2.10;
if (amount1 - amount2 == 1.05){
System.out.println("OK");
}
}
}
0.30000000000000004
0.19999999999999996
401.49999999999994
1.2329999999999999
2.1.5 大文件处理
Files.readAllBytes
直接读到内存,会OOM的,建议使用BufferedReader
一行一行来,或者使用NIO
2.1.6 使用完IO资源流,需要关闭
try (FileInputStream inputStream = new FileInputStream(new File("jay.txt")) {
// use resources
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
2.1.7 try...catch异常使用的一些坑
尽量不要使用e.printStackTrace()打印,可能导致字符串常量池内存空间占满 catch了异常,使用log把它打印出来 不要用一个Exception捕捉所有可能的异常 不要把捕获异常当做业务逻辑来处理
2.1.8 先查询,再更新/删除的并发一致性
if(selectIsAvailable(ticketId){
1、deleteTicketById(ticketId)
2、给现金增加操作
}else{
return “没有可用现金券”
}
if(deleteAvailableTicketById(ticketId) == 1){
1、给现金增加操作
}else{
return “没有可用现金券”
}
2.2 提供对外接口
2.2.1 校验参数合法性
2.2.2 新老接口兼容
//老接口
void oldService(A,B){
//兼容新接口,传个null代替C
newService(A,B,null);
}
//新接口,暂时不能删掉老接口,需要做兼容。
void newService(A,B,C);
2.2.3 限流,防止大流量压垮系统
2.2.4 接口安全性,加签验签,鉴权
2.2.5 考虑接口幂等性
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
查询操作 唯一索引 token机制,防止重复提交 数据库的delete删除操作 乐观锁 悲观锁 Redis、zookeeper 分布式锁(以前抢红包需求,用了Redis分布式锁) 状态机幂等
2.3 调用第三方接口
2.3.1 超时处理
2.3.2 考虑重试机制
2.3.3 考虑是否降级处理
2.3.4 考虑是否异步处理
2.3.5 调接口异常处理
3. 缓存篇
3.1 数据库与缓存一致性
3.1.1 几种缓存使用模式
Cache-Aside Pattern,旁路缓存模式 Read-Through/Write-Through(读写穿透) Write- behind (异步缓存写入)
读的时候,先读缓存,缓存命中的话,直接返回数据 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
3.1.2 删除缓存呢,还是更新缓存?
线程A先发起一个写操作,第一步先更新数据库 线程B再发起一个写操作,第二步更新了数据库 由于网络等原因,线程B先更新了缓存 线程A更新缓存。
3.1.3 先操作数据库还是先操作缓存
线程A发起一个写操作,第一步del cache 此时线程B发起一个读操作,cache miss 线程B继续读DB,读出来一个老数据 然后线程B把老数据设置入cache 线程A写入DB最新的数据
3.1.4 如何保证最终一致性
缓存延时双删 删除缓存重试机制 读取biglog异步删除缓存
3.2 缓存穿透
如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。 如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效) 使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。
3.3 缓存雪崩
缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5小时+0到1800秒酱紫。 Redis 故障宕机也可能引起缓存雪奔。这就需要构造Redis高可用集群啦。
3.4 缓存机击穿
使用互斥锁方案。缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。 “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
3.5 缓存热Key
Redis集群扩容:增加分片副本,均衡读流量; 对热key进行hash散列,比如将一个key备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量; 使用二级缓存,即JVM本地缓存,减少Redis的读请求。
3.6 缓存容量内存考虑
3.6.1 评估容量,合理利用
3.6.2 Redis的八种内存淘汰机制
volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰; allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。 volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。 allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰; volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。 allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。 volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰; noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
3.6.3 不同的业务场景,Redis选择适合的数据结构
排行榜适合用zset 缓存用户信息一般用hash 消息队列,文章列表适用用list 用户标签、社交需求一般用set 计数器、分布式锁等一般用String类型
3.7 Redis一些有坑的命令
不能使用 keys指令 慎用O(n)复杂度命令,如hgetall等 慎用Redis的monitor命令 禁止使用flushall、flushdb 注意使用del命令
最后
往期热门文章:
1、《历史文章分类导读列表!精选优秀博文都在这里了!》
2、Java必会的工具库,让你的代码量减少90% 3、腾讯最大股东收购了 Stack Overflow,以后“抄代码”都要付费了么? 4、灵隐寺招聘:没有KPI,佛系上班…… 5、如何优雅处理重复请求/并发请求? 6、不用到2038年,MySQL的TIMESTAMP就能把我们系统搞崩! 7、翻车!在项目中用了Arrays.asList、ArrayList的subList,被公开批评 8、想接私活时薪再翻一倍,建议根据这几个开源的Spring Boot项目改改~ 9、细数ThreadLocal三大坑,内存泄露仅是小儿科 10、Redis与MySQL双写一致性如何保证?
评论