前言
本文档内容仅为个人经验和总结,不能保证完全是正确的或合理的,请根据个人习惯适当调整。
编码规范
黄金准则
不要整个文件格式化,选中自己提交的指定代码进行格式化(代码格式化算 change,影响 review)
此处为语雀视频卡片,点击链接查看:iShot_2022-10-18_13.46.57.mp4
快速失败,不要让异常逃离在所属服务/职责之外,流转到下游
举个栗子
用户通过前端页面提交参数,后端 web 服务接口没有对参数做足够的校验,让不合法的参数存储到了数据库。营销服务通过内部 rpc 服务获取该参数时,触发了异常,导致一些列问题等。
这个异常完全可以在后端 web 服务接口做参数校验,对于不合法的参数不予处理,直接将错误告诉用户。
方法实现尽可能的精简,独立,清晰,职责单一
方法实现尽可能简短,能够做到和方法名一样。如果代码和方法名无关,可以抽离出单独的方法。
比如下面的代码,方法定义为doSendReply意为 “发送所有pending的reply”。但是在发送完回复后,还做了 session 相关的操作,那么 session 更新和发送回复无关,即可抽离成单独的 updateSession类似的方法,意为“更新 session”
善用可变和不可变对象
| 可变对象 | 不可变对象 | |
|---|---|---|
| 特点 | 可读可写,灵活,多线程读写不安全,如需线程安全要加锁 | 只读,可复用,线程安全,语义明确 |
java 集合
合理的接口定义,会让我们的代码具有层次感和辨识度。比如 Java 中最经典的 Collection 接口及其子实现类。
如图所示:
这些集合类都是由其数据结构特性来定义的
- Set 不可存重复元素的集合
-
- HashSet 元素是分散的、无序的
- TreeSet 元素是有序的,由其内部
comparator实现元素的顺序,为 null 则为自然插入的顺序
- Queue 队列,以插入为顺序,先插入的元素先出队
- Deque 双端队列,可从双侧插入和出队
- List 可存重复元素的列表
以上接口和实现类默认都是可变的,因为都提供了写方法(set/add 等等)但是在实际编码中,我们可能需要一个不可变的集合(常量)。
比如查询中国所有的行政区编码,并不需要每次都去数据库查询数据返给前端。可以将其缓存在服务器本机内存,大大提高查询效率,同时为了避免该集合被其它开发人员修改,我们可以将其定义成不可变的集合。
guava 的集合
比如下图,guava 实现的不可变集合(理论上不应该定义写方法)但是,由于父类 Collection 接口提供了写方法,实现类只能在写方法抛运行时异常。
kotlin 的接口设计
直接从接口层面区分了可变和不可变,可变的提供了add``remove等写方法,不可变的则只提供了 get等读方法
git 分享
cherry-pick
git cherry-pick可以理解为精选、挑选,功能是合并其它分支或 commit 到当前分支,相对于 merge 会更加灵活,因为可以指定单个或多个 commit 进行合并。
举个栗子
🌰 bugfix(dialogue): 解决 session 序列化 MatchedScenario 问题
比如这个 mr, 是为了修复 dialogue 中 gson 对于 proto 对象序列化不兼容的问题。因为乐于助人团队已经修复过该 bug 了,我们可以用 cherry-pick 将其修复的 commit 合并到我们所需的分支,减少重复工作。
🌰 feat(store-home): 拼多多时间组同步优化
比如这个 MR, 时间组优化这个功能,在开发时提交了多个 commit,然后良军在其它分钟也有个小的 commit, 我用 cherry-pick 将他的提交合并到我的分支,再统一合到主分支,因为是两个功能(时间组优化和订单质保期限制)所以我将之前提交的时间组优化的 commit 合并成一个 commit,看起来更加清晰
类似的还有 feat(marketing-home): 营销配置同步
举个反栗
提交多个 commit 的备注信息一样,区分度太低,不清晰,对于 review 的人不友好,提高了后续代码复用或审查的难度。
worktree
当某个迭代,多名开发者在同一个仓库进行开发时,我们可能会遇到以下几个问题
- 如果在同一个分支开发,增加了代码冲突的概率,每次提交和拉代码都可能需要解决冲突
- 如果按照功能划分,每个功能一个分支,那么在发布到测试环境时,可能会出现代码覆盖
所以针对代码冲突和覆盖,我们通常又会有以下约定
- 每个功能一个分支,比如开发者 A 在 featA 分支上开发, 开发者 B 在 featB 分支上开发,以此类推, 独自开发和维护
- 再约定一个测试主分支,每个开发者都将自己的代码合并到测试分支,然后用该分支进行发布,避免了代码覆盖
- 如果 A 功能 delay ,B 功能可以正常发布,那么只要将 B 分支合并到生产主分支即可,这样就可以通过分支解耦来实现灵活发布和回滚
但是针对以上的约定,对于开发人员本地来说,可能会出现以下两种操作
- 本地只有一份仓库,提交代码
- 本地克隆两份仓库A, 一份是开发分支,一份是测试分支,来回切换进行代码合并
本地只有一份仓库,提交代码
- 本地克隆一份仓库,多分支合并代码,来回切换分支
此处为语雀视频卡片,点击链接查看:多分支合代码.mp4
- 本地克隆一份仓库,多分支合并部分代码,当前开发分支暂存不需要提交的代码,来回切换分支
此处为语雀视频卡片,点击链接查看:多分支合代码,本地暂存.mp4
本地克隆两份仓库
本地克隆了两份仓库,存储在不同的路径。
此处为语雀视频卡片,点击链接查看:本地克隆两份项目.mp4
使用 worktree
使用 worktree 只需要本地克隆一份仓库
如下面的视频所示,先 git worktree add <path> <branch>新建一个工作区,然后 ide 打开该工作区,即可合并其它分支代码到所需的分支
<path>分支存储的本地路径,可以是绝对或相对路径
<branch>分支名
此处为语雀视频卡片,点击链接查看:使用 worktree 管理多分支.mp4
此处为语雀视频卡片,点击链接查看:iShot_2022-10-18_13.38.11.mp4
关于 kafka
主要说下 kafka 消费者的几个配置,因为消费者是我们开发者最常用也是最容易出问题的一端。
自动提交偏移量
我们在使用 kafka 消费者时,通常将 enable.auto.commit设为 true即消费者端自动提交偏移量,间隔时间配置auto.commit.interval.ms默认 5 秒。
举个栗子,如果消费者 A 从 broker 拉取了 10 条消息进行消费,但是由于耗时较长,超过 5 秒没有自动提交偏移量到 broker,broker 则认为该消费者没有正常消费,随后这 10 条消息将会被重新消费(有可能是其他消费者)
如果配置和使用不当,可能导致消费者重复拉取和消费,从 garafana 看起来是消费很慢或没有消费的假象
单次拉取配置
max.poll.records控制单次拉取的最大消息条数(默认 500),如果消费者自身有不错的消息处理速度,那么这个值越大吞吐则越大
fetch.max.wait.ms消费者线程在向 broker 拉取消息时,自身是阻塞的,这个即最大等待时长
fetch.min.bytes单次拉取最小字节,配合fetch.max.wait.ms使用,满足任意条件即返回数据给消费者
针对上述配置,建议配合监控、消息大小和实际场景等因素(比如 galaxy 服务允许消息堆积在 kafka 慢点消费)
进行调优
如下图所示,拼多多兜底 question 消费 dialogue-grpc-session-all-in-one-pdd的kafka 消息,之前的消费者收到消息延迟大约在 100 毫秒左右(消费者收到消息时间 - 生产者发出消息时间),经过调整消费者参数,延迟降低到了 25 毫秒左右
不要再额外阻塞消费者
正如 单次拉取配置 提到的,kafka 消费者在向 broker 请求消息拉取时,已经做了阻塞,所以不要在代码侧做不必要的优化(避免频繁拉取空的消息,休眠一段时间)
如下图所示,兜底回复下发量比较小(最大 qps=6)所以在会经常出现拉取不到消息的情况,然后本地休眠。
在 去除不必要的休眠 后,消费者收到消息延迟降低到 50 毫秒左右