简介:类似于“防御性驾驶”对驾驶安全的重要性,防御性编码目的概括起来就一条:将代码质量问题消灭于萌芽。要做到“防御性编码”,就要求我们充分认识到代码质量的严肃性,也就是“一旦你觉得这个地方可能出问题,那基本它就会(在某个时刻)出问题”。当然,实际情况比这个更严峻。由于大家的编码经验和风格差异,导致大家的意识边界是大小不一的,那些潜伏在意识边界之外的“危险”更加隐蔽和不可琢磨。在意识层面上,我们当然要摒弃“想当然”和“差不多”的思想,严肃评估这些问题发生的可能性,认真对待这些风险。但如若话题止步于此,那其实还是缺乏执行层面的指导意义的,激不起半点“涟漪”的。这个文章目的也更多是关注到“实操层面”的引导
作者 | 字白 来源 | 阿里开发者公众号
一 防御性编码的意义类似于“防御性驾驶”对驾驶安全的重要性,防御性编码目的概括起来就一条:将代码质量问题消灭于萌芽。要做到“防御性编码”,就要求我们充分认识到代码质量的严肃性,也就是“一旦你觉得这个地方可能出问题,那基本它就会(在某个时刻)出问题”。当然,实际情况比这个更严峻。由于大家的编码经验和风格差异,导致大家的意识边界是大小不一的,那些潜伏在意识边界之外的“危险”更加隐蔽和不可琢磨。
在意识层面上,我们当然要摒弃“想当然”和“差不多”的思想,严肃评估这些问题发生的可能性,认真对待这些风险。但如若话题止步于此,那其实还是缺乏执行层面的指导意义的,激不起半点“涟漪”的。
这个文章目的也更多是关注到“实操层面”的引导。
二 如何防御性编码?以下需关注的具体方面更多来自于我的习惯和观察,并且统一用伪代码作问题示例。
欢迎大家把自己的“防御性编码心得”在评论区分享出来。
1 并发冲突问题
这个问题在实际项目中,被错误地忽视的比例相当高。它的外在表现形式五花八门,但关键点是:“当你的代码被并发调用时,它会怎么表现?”
我们心里要有个运行时的世界观,代码运行的Context是这样的:多线程 -> 多进程 -> 多机器 -> 多集群。我们编码时,要充分考虑代码在上述世界观多点并发的可能性,及相应的潜在后果。
举几个具体的问题例子):
- 存在共享变量 或者 数据。(不限于堆内存,也可能是缓存、DB、文件等)
例子2:
// 某个 Spring singleton Bean 'aService' 存在一个调用来源标记,记录调用来源是HSF还是HTTP。
// 先 记录来源标记。
aService.setSource(source);
// 再结合source执行其他逻辑。例如将上面记录的source 和 其他参数 插入数据库.
aService.doSomethings(params);
如果这个代码被 HSF和 HTTP 同时调用就会发生问题。
例子3 :
在一个系统中,有两个价格类型 small 和 large,业务逻辑要求 small = 2(系统small) 通过。准备写入新的large (3)。- 非原子操作问题。
// 先查询是否存在目标记录
resultList = dbRepo.list(query);
// 有结果就更新,没有就插入
if( resultList.size() > 0 ){
dbRepo.update(xxxx);
} else {
dbRepo.insert(xxxx);
}
如果这个代码被多个request 同时执行也会发生问题。
- 错误的发生并发
2 事务问题
对于先A再B后C的这类组合操作,要仔细考虑保障一致性的必要性,做好是否做事务保障的评估。
事务即要求:对一组的operation combo,要保障好执行顺序,保障好context的一致性,保障好结果的一致性。
- 数据库事务。 发生概率不高,大多会主动预防。
- 上下文一致性问题。
这个例子在集群环境中就会出现概率性成功或失败的情况,集群节点数量越多,失败概率越高。这是因为 前端的前后两次请求调用到了不同节点上,执行上下文出现了不一致。
- 顺序一致性问题。
3 分布式锁问题
分布式锁日常也经常用到,在使用细节上存在一些容易忽略的盲点。
- 获取锁
RLock lock = redisson.getLock(lock);
lock.lock(-1L, TimeUnit.MINUTES);
// 获取到锁就持久占有,避免反复切换
while( !isStopped ){
if( lock.isHeldByCurrentThread() ){
// do some work
}else{
// try to acquire lock again.
}
SleepUtil.sleep(loopInterval, TimeUnit.MINUTES);
}
4、能用本地锁 不用全局锁。
- 锁超时
- 释放锁
4 缓存问题
- 缓存穿透问题
- 缓存击穿问题
- 缓存雪崩问题
- 缓存的一致性
- 缓存命中率
- 热点key问题
5 失败处理问题
这类问题虽属于低级问题,但往往比较隐蔽。在异常发生时,选择相应处理action时,我们要头脑非常清醒。
- 失败处理
这里不在于选择那种处理方式,而是要“头脑清醒”的结合自己场景需求做出选择。
- 注意默认值
6 switch配置问题
- 分批推送的时间间隔
- 内存值与持久值
- 代码重构注意事项
做代码结构重构时,如果没有指定switch的namespace,会导致你推送过的持久化开关失效,进而引发严重的线上故障。
关于应用级服务发现与接口级服务发现的区别和 dubbo 生态的解决方案,本文中不多赘述,可以参考刘军前辈写的文章文章《Dubbo 迈出云原生重要一步 应用级服务发现解析》 简单来说,应用级服务发现需要开发者关心接口之外还要关心应用名,注册中心的冗余信息较少;接口级服务发现开发者只需要引入接口名,但注册中心的冗余信息较多。- 合理使用,避免滥用
7 重大风险评估和处置
针对一个需求开发,我们需要评估风险及我们的承受能力。主要目的是 预防重大故障的发生,而不是要预防所有Bug。
关于风险处置,也没有一个固定的标准。我建议是结合业务场景,评估风险概率和潜在问题的严重程度,最后来制定相应的解决方案。例如,如果发现有资损风险,那要采取一切手段把漏洞堵上;但如果只是小概率的漏掉钉钉通知,那增加相应的告警即可。
我们如何评估 重大风险呢?我建议分这么几个环节做评估:
1、梳理 关键的业务流。 2、梳理 每个业务流的关键环节。 3、梳理 每个关键环节的关键逻辑 和 关键上下游。 4、结合自己场景,假定 关键逻辑 和 关键上下游 出现极端问题。例如 网络挂掉、机器重启、高并发来临、缓存挂掉等。这里需要强调一点,并非所有模块都需要假定非常极端的情况,要结合自己实际业务要求、历史风险等 来综合判断。
再举个例子:
假设,有一个用户资金转账系统,用户可以通过App进行跨行转账操作。 那这个系统就要考虑到 转账超时、转账失败等场景。同时还要考虑 转账超时 或 失败时,是fail-fast 好,还是 fail-over好? 此外,还需要考虑到 App端的用户交互设计,假如遭遇网络中断或超时,且用户看不到任何问题提示,那用户很可能再次发起转账尝试,最后转了两笔的钱。这个评估过程看上去有点冗长,但其实对于了解自己系统和需求细节的人来讲,应该是很容易做到的。如果做不到那就只能加强细节的理解和学习了。
三 最后以研发同学为中心,向内看:需持续提升防御性编码的意识和实操能力;向外看:外部环境需要尽可能提供与之匹配的环境。
例如,在面临有紧急DeadLine的需求时,防御性编码的执行完整度就会受到一定影响。
再次欢迎大家把自己的心得留言。
原文链接
本文为阿里云原创内容,未经允许不得转载。