写代码与洪水滔天
你好,我是yes。
之前不是稍微吐槽了下一个项目的代码嘛,有几个同学(1V1问答的同学)就顺着问了我几个平日里一些编码的问题。
不同人写代码其实分了好几种情况:
有些人完全没意识到问题,这个属于个人知识瓶颈,需要多加学习。 有些人就随意写,赶时间,没责任心,当下能跑就行,后面管它洪水滔天。 有些人就贼纠结,写个小功能想七想八,脑子总往着亿级流量上怼,可能项目黄了之前其实都只几个人用。
这几个情况我都经历过,写代码虽被戏称搬砖,但咱毕竟也是类似阿提斯特一样的创作型工作,跟心情有一定的关系。
工期紧心情差,将就将就,闲得很心情好,我缀一口咖啡精雕细琢。
咳咳,扯远了,回到今天的主题。其实平日里想要写好代码,没那么难,这篇我就总结下需要注意的几个要点,不是很全,但是都是比较常见的点。
批处理思想
遥想当年我还是实习生的时候,写个代码风风火火闯九州!就没有我不敢莽的代码。
一个 for 循环闯天下:
for (YesDTO dto : yesDTOList) {
//do sh
save(xx);//插入数据库
}
dev 和 qa 库里就几条数据,跑的好好的,一上线几千条数据,直接干蒙 mentor。
我当时都没意识到问题所在,但从他的眼神中我看到了杀气。
影响不大,这锅还得他背,谁让他不 check 我的代码?
现在知道了,一条一条插入太慢了,涉及到网络的开销,数据的解析等等。
所以特别在 for 循环的时候要想到批处理思想
for (YesDTO dto : yesDTOList) {
//do sh
xxList.add(xx);//添加列表
}
//批量插入
saveBatch(xxList);
不仅仅是我提到的数据库操作,还有 RPC、HTTP 调用也是一样,需要提供批量处理的接口替换 for 循环的一次次调用。
批处理能显著提高吞吐,如果你看过一些中间件的源码或底层一些实现你肯定能 get 里面很多批处理的思想。
事务
本地操作,如果涉及多个表的修改,不要忘了上事务,不然一旦中间处理出了差错,数据就不一致了,意味着需要补数据,而补数据是一件非常麻烦且敏感的事情。
如果本地操作,涉及多个表的修改,又涉及远程调用(或HTTP调用),需要注意事务的范围。
开始事务
修改A
RPC调用
修改B
结束事务
不推荐在事务中使用远程调用(或HTTP调用)。
因为远程调用(或HTTP调用)可能因为网络等其他原因导致响应很慢,而如果你的事务包裹了这些调用,可能会因为处理慢而长时间持有数据库连接,或阻塞后续其他请求修改对应的值,使得连接池的连接耗尽,然后就都堵着,就都挂了。
所以写代码的时候要想着上事务保证数据的一致性,又得想着事务内部的行为会不会阻塞连接的释放导致后续雪崩问题。
还有要注意一点,有些同学在事务里面包了 RPC 调用(或HTTP)是想着如果 RPC 调用失败本地事务就回滚,通过这样的手段来保证一致性。
这种想法是错的,因为调用可能是超时或其他网络情况,这不能代表对方的业务执行失败,所以如果对方执行成功,你还是回滚了本地事务,其实数据还是不一致的。
如果需要确保一致性,就只能上分布式事务,可以看下我这篇:分布式事务汇总
异步
异步化改造是提升服务性能的一个有力手段!
如果某个模块流量高,异步能减轻压力。
如果某个模块处理流程复杂且缓慢,异步能避免同步调用超时。
在编码中遇到以上这两个问题,就可以考虑异步。(当然还有其他场景,但这两个比较常见)
可以通过 MQ 或者线程池来实现异步。
在平日工作的场景中,我更多使用 MQ 来实现异步,因为线程池的任务毕竟是存储在内存中的,它没有自带的持久化操作,而且任务队列大小也有限,而 MQ 自带持久化且能存储的任务量更大。
你想,假设你线程池堆积了1千个任务,然后服务挂了,那不又得考虑补偿的机制了?而 MQ 就没有这个烦恼。
当然,一些定时批处理任务类的场景还是要利用线程池的,不过这种场景的数据源已经持久化在数据库中了,不会丢失。
重试
就像我前面说的,RPC 调用或 HTTP 调用可能因为网络问题没拿到正确的响应,这时候你必须要有重试的操作。
比如有个业务是异步的,别人调了你之后,你慢慢处理,等你处理完了需要通知别人,这就涉及到回调。
而回调别人接口的时候,脑子里一点要想着会遇到网络问题,比如超时等,你必需要设计一个重试机制来保证通知到对方。
这个重试机制最好是间隔延迟的,比如1s、5s、30s、1min 、5min这种间隔重试,也就是说需要给对方一点时间来恢复服务,避免对方服务出问题的前几分钟把重试都用了,导致后面需要人工介入补偿。
还有需要限制重试次数,因为我们的资源也是有限的,不可能给它无限重试,当达到一定的失败次数后进行记录,后续人工介入处理。
幂等
提到重试,那肯定伴随着幂等。
一切接口,如果可以,就按幂等实现,也是说一个接口同样的入参,调用多次都跟调用一次产生的结果是一样的。
因为现在基本上都是微服务,远程调用非常频繁,基本上 RPC 框架都会自带重试机制,你的接口很有可能在你没准备的情况下被被重复调用,所以幂等就很重要。
并且有时候需要补偿等动作时,幂等的接口可以让你补偿更加方便且没有后顾之忧。
缓存
缓存是提高服务性能的一个重要手段之一。
很多大流量高并发服务基本上业务层的数据源都来自于缓存,对于一些精细化拆分的业务组来说,可能几年都没写过 SQL。
我相信市面上公司基本都会接入 Redis(或类似组件),咱也不是说啥都要上缓存,只是说编码时候考虑下这块是否需要利用缓存来减少服务的压力,比如一些频繁调用且基本上不会更改的固定配置等等。
而缓存不仅仅是分布式缓存,还有本地缓存,也要善于利用本地缓存来实现优化。
总结
我稍微总结下以上内容:
编码时要有批处理思维,避免 for 循环单条保存数据和远程调用(效率极低)。 注意事务的范围,避免事务中进行远程调用或分布式锁竞争等可能长时间造成数据库连接不释放的场景,也就是说不要无脑用 @Transactional 包裹整个方法; 异步改造提高性能,但是要注意异步后如何保证异步的逻辑一定会被执行,且异步逻辑出错如何补偿等问题; 网络是不稳定的未知的,重试机制必不可少,要注意重试间隔,给对端多点时间恢复,减少需要人工介入的场景; 尽可能按幂等实现方法,防止被重复调用导致数据错乱; 缓存,缓存大法YYDS,但是要注意失效时间以及大 key 问题。
虽说咱是打工人,不是给人卖命,但是一些基本职业素养还是要有的,我们还是需要有责任地管一管,避免后面的“洪水滔天”。
好了,今天就暂时分享这么多,后期我看着再整理一下实现的细节点。
还有,我最新一期的个人VIP专属问答已经启动了,已经给很多小伙伴修改了简历和回答面试问题,有几个已经找到工作啦!
问答详情,可以看我的这篇介绍:最后再接 3 个名额凑个整。
我是yes,从一点点到亿点点我们下篇见~
微信扫码关注该文公众号作者