Git团队开发中PR工作模式的反思
Tag git, pr, 工作流, on by view 3282

在公司团队协作用了大半年的Git了,PR工作模式也用了半年。由一个用Git装逼的假老手成长为真正用Git进行团队协作开发的老油条。这其中也有一些值得反思的东西。我很崇拜PR的工作模式,因为它基本上是优秀的开源软件工作模式的代表,众多的开发者自发的给开源项目发送PR,以前总是盼望着自己的开源项目会有人发送PR,若是接收到PR就像是收到别人的礼物一般高兴。

我之前赞同的PR工作模式是这样的,开发者拿到开发需求之后,在自己的分支开发,然后向主仓库的相关分支发送PR,之后由测试人员在测试机上拉取主仓库相关分支的代码,然后fetch PR所在仓库分支的代码合并(merge)到本地主仓库相关分支,进行测试,测试完毕通过之后才合并PR到分支。我这一想法的灵感来源于 travis-ci 单元测试的工作模式:开发人员发送PR,travis-ci自动进行单元测试,PR管理者参考PR在 travis-ci 上单元测试的结果进行初步判定是否可以合并。但是我忽略了一点,测试人员并非是 travis-ci 上的单元测试。在工作的过程中往往是这样的:

开发人员初步开发出某个功能,然后经过自己的初步测试,发送PR到主仓库,测试人员看到有PR了,需要测试了,于是开始测试,但是测试有问题,于是开发人员就必须继续修改提交,然后再次测试,还有一些问题被发现依然存在,继续开发……可是,你突然发现,你的PR被合并了或者是被关闭了,因为要下班了;或者是要下班了,PR管理员会问这个PR能否合并,测试人员暂时没发现新的问题,于是测试人员告诉PR管理员“没有问题”,于是PR被合并了。或者没有上面的“PR管理员的询问”,但不管怎样,最后的结果往往是PR被草草的合并了。于是,没有充分测试的PR进入了仓库,甚至进入了发布分支,最终的结果可想而知。

在这个过程中,人们往往没有意识到其实是被PR给影响到了。因为开发者一个PR创立之后,在他会有一个潜意识希望PR被合并,测试人员同样也有这么一个潜意识,因为测试PR就是他的工作之一,他也希望PR被合并,对于PR管理员来说同样也有这么一个潜意识,希望PR被处理,不管是合并还是关闭。就是这些潜意识推动这个PR尽早被合并。

我个人感觉正确的做法应该是当测试人员测试充分之后再发起PR,避免这些潜意识促使PR过早的被合并。事实上,不管PR是否存在,测试人员都能够从开发者的分支上拉取到开发者的代码合并到主仓库,PR不过是一个形式而已,给合并者带来方便。但是那些错误的理解了PR的人们往往会搞出诸多莫名其妙的东西,比如PR用来人工做代码审核,比如搞个CheckList让人工去查代码中哪儿写得不规范,这些应该是让 Sonar 自动扫描PR中存在的不规范,谁又会有时间去为别人的PR人工做Sonar该做的事情。经过测试人员的充分测试之后,开发者创建PR,这时单元测试测试一遍,然后Sonar扫描一遍,后两者自动化测试通过之后代码才能入库。

总之,测试人员功能测试通过之后开发者才能创建PR,自动化工具进行单元测试和Sonar扫描通过后,PR管理员才能允许合并。这才是比较靠谱的PR工作流。


结构化数据缓存管理设计
Tag 数据库, 缓存, 管理, on by view 3368

现代web开发中,在数据库和应用程序服务器之间使用缓存是常见的手段,它有效的减轻了后端数据库服务器的压力。结构化数据库中的数据是按照表、字段这种结构存储的,而作为缓存系统的memcached或是redis-server是以key-value的方式存储数据的。如何将结构化的数据存储到非结构化的缓存系统是一个问题,对于MySQL等结构化数据库我们可以按照以下方式存储。

首先,对于表中单条数据我们可以按照Bean存储在缓存中,key为“表名+id”,value为Bean对象。

然后,对于列表查询,我们可以使用“select id from article where status=1 limit 10”这种语句查出article表的id列表 List<Long> 。然后通过id从缓存中逐条加载 Bean ,最终形成我们需要的 List<Bean>。而我们可以将通过sql查出来的List<Long>结构的id数组作为一个缓存对象,其key可以按照某种规则生成,例如“方法名+override_index+参数...”。

上面说的两种数据对象的存储方式都没有问题,而其中的难点在于如何管理缓存,我们知道,一个列表查询的结果List<Long>在这张表发生增删改的时候都会发生变化,因此,详细来说我们应该在列表的某个字段发生增删改的时候清理掉这个id列表缓存,等下次查询的时候再自动重建新的缓存,以保证缓存的正确性。不过,若是联表查询呢?联表查询“select user.id from article, user where article.userid=user.id and article.status=1”查出的是一个id列表,我们将其缓存假设key为key1,但是一旦article表和user表其中之一发生增删改,那么这个缓存数据应当失效,也就是说,Article在进行 Update()、Save()、Delete() 的时候我们应该清理掉这个key为key1的缓存,同样对于User在进行 Update()、Save()、Delete() 的时候我们也应当清理掉 key1 的缓存。在这儿只是列举了两个表联查,并且只是提到了一个联表查询,假如有N个表联表,而且有M个联表查询语句那么我们应该在多少个方法中调用清理这种缓存的方法呢M*N*3,好多好麻烦呀。

为了管理list的缓存,定义一个树形结构如下

cache_manage_struct.png

t开头的节点表示表名,tA、tB 表示 A 表和 B 表,f开头的表示字段,fAa 表示 A 表 a 字段,fAb 表示 A 表 b 字段,k开头的表示列表的缓存键。这个数据结构表达了我们应该如何清理缓存,比如A表中的a字段发生的改变(增删改),那么我应该清理掉键名为k1, k2, k3的缓存,A表字段b发生增删改时应该清理掉键名为k1, k2的缓存,B表a字段发生增删改时应该清理掉键名为 k1 的缓存。然后将该树形结构拆分为三级,分别用三类的key-value存储起来就好,也不必担心结构过于庞大,上图可以拆为6个key-value:

[0]    _init->(tA, tB)
[1]    tA->(fAa, fAb)
[1]    tB->(fBa, fBb)
[2]    fAa->(k1, k2, k3)
[2]    fAb->(k1, k2)
[2]    fBa->(k1)

上图的结构来自于如下三个SQL查询

[K1]   select id from A, B  where  A.fAa = ? and A.fAb = ? and B.fBa = ? ...
[K2]   select id from A     where  A.fAa = ? and A.fAb = ? ...
[K3]   select id from A     where  A.fAa = ? ...