likes
comments
collection
share

怎样完成一次实现设计

作者站长头像
站长
· 阅读数 4

前言

前段时间评完需求,看了看觉得还是有点复杂,于是分完任务便让同事们先写一下设计文档,两天后一起评审一下。 结果两天后一看,基本没有达到想要的结果,要么是过于简单几句话搞定,要么是原文照着需求说明抄了一遍。 不知道大家对实现设计如何看待,简单的需求无所谓,但如果稍微复杂的需求这一步个人认为还是挺关键的。 和组内同事沟通了下,大体是认为浪费时间,一般都是直接开搞,也不管需求复不复杂。 费时是肯定的,但相较于优点这点时间我觉得还是有必要的,而且如果前期设计没做好,后期花费修复这些设计上的问题才是最致命的。 不幸的是,在我来这家公司之前,正好出了这种典型的案例。

问题案例

需求是一个仓库预警+自动补货的场景,开始的时候主导的同事简单的分了下任务,三个人就开始干,工期一个月。 大概月底的时候,负责核心功能的同事发现其他两人给的接口完全和他想的不一样,于是无奈之下自己几乎重下了一遍他们的逻辑。 到这里,开发时间从一个月变成了两个月,一路坎坷还是提测了。 提测后又是一堆bug,有些bug是涉及流程逻辑的,可能连产品当时也没想到,但这种bug往往也是致命的。 于是修修补补一个月终于还是上线了,原本计划的一个月时间变成了三个月。 然而,到这里事情还没完,上线之后业务反馈,功能有用是真的有用,但是不是那么完美。 于是需求2.0开始,大致增加的内容是: 1.需要按仓库维度统计,总仓的维度只能供管理层查,分仓的维度才是业务员想要的 2.预警规则需要灵活多样,每个仓库的触发预警的条件是不一样的 好家伙,这两条需求一上,当初负责核心功能的那位同事差点直接宕机,他最开始设计的模型完全满足不了。 于是,他又花了两个多月的时间强行将需求2.0给塞进了老模型上面。(顺带一提,我也是这个时候进的公司) 再然后,在行业还没有大规模【降本增笑】的时候,他率先完成了【防御性编程】。 再再然后,他【毕业】了,留下东西我也不想改了,看了几眼,能看得出真的很用心的想要将2.0的需求强行融进去。 上面这个例子不知道大家有没有似曾相识,但在我看来其实很多问题一开始就能避免。

一点看法

先来说说这个案例的问题点: 1.首先,开发时间预估不准确,看了需求后,我发现虽然前端交互的内容不多,但后端逻辑却很多。光一个计算参数就已经涉及了系统大部分核心业务。 2.两次返工过于费时 3.实现过于特殊化,导致后期扩展艰难 这个需求不算复杂,但更谈不上简单,有些公司的仓库预警+自动补货复杂到甚至能养活一个部门。 说回整体,先来说说我个人认为的功能实现设计的优点: 1.理清需求实现思路。 这一点很重要,产品更多的关注功能层面的交互,内在实现逻辑他肯定是不重视的。 但是一个功能如何实现?能否实现?有没有逻辑漏洞?等等问题,这是需要开发者根据需求去反复推敲的。 其实在我看来这一步才是体现我们开发者真正实力的地方,背再多的八股文,会再多的中间件也只是为这里做储备而已。 是经验+技术的双重加持,才能提现到代码上的【优雅】。 2.确定业务细节和逻辑细节。 认真去做设计,才能站在开发者角度思考,跟着需求思考实现逻辑,大概率会遇到走不通的情况或者某种场景不清楚处理方式的情况。 这个步骤是和产品撕逼的环节,也是你帮产品完善细节同时你确定边界问题实现的时候。 如果以来就开发,到发现问题时可能已经晚了。 3.拆分功能及需要实现的功能点。这没什么好说的,这一步当然越详细越好。 4.评估工作量。不知道大家是怎么评估一个复杂功能的工作量的,但既然第三点已经做好了,你能更准确的预测这些功能点开发的时间。 加起来,再乘个1.5或2才是实际的工作量。 5.多人检查。不要忘了团队合作,自己的实现设计也不一定就没有问题,组内的其他人也能根据你的设计去思考,也会去发现是否存在逻辑漏洞, 如果有相互调用的方法或接口,也会考虑你提供的方法是否满足他的功能需求,另外,还能一定程度上规避重复造轮子的问题。 甚至测试、产品也能参与进来,测试也能补全他的测试用例。而产品也能评估你的思路是否和他想的需求一致,同时也可能成为他给别人讲解产品功能的一种扩展。

开搞

既然优点已经清楚了,还是举个例子实践一下,也带入一下我平时写设计实现的一些思考。 比如一个简单的加减库存的功能,没有具体的前端交互,所有接口都是为系统内部提供,需要满足商城、WMS、ERP多个系统的需求。 开始头脑风暴: 看似很简单,就是两个接口,加库存一个,减库存一个。 那么表字段主要字段就是:

sku_idamountupdate_date
商品id数量更新时间

加库存的场景有:采购入库(ERP)、调拨入库(ERP+WMS)、退货入库(商城+WMS)、商品调整(WMS) 减库存的场景有:销售出库(商城)、调拨出库(ERP+WMS) 主要的场景就这些,其他的就不列了,从这里可以看出,两个接口最基本的需要支持批量多商品的加减库存,因为采购入库和销售出库都支持批量操作。 对应的接口传参应该是:

[{  
    "skuId": 商品id,  
    "amount": 加减库存的数量,传正数  
}]  

但是光这样不行,既然目前都有至少六个入口,那么就需要一个类型来标识它是怎么调用的,不然到时候连这个商品的amount值怎么来的都不知道。 所以参数需要加上type,标识是哪个业务来源,同时还需要新加一张表记录修改日志

{  
    "detail": [{  
        "skuId": 商品id,  
        "amount": 加减库存的数量,传正数  
    }],  
    "type": 业务类型  
}  

同时,既然有了业务类型,所以这里就多了一条检验,需要根据type校验场景,加库存的业务场景不能去掉减锁的接口减库存同理。 好了,接口字段就这样,表字段也那样了,现在开始具体讨论怎么实现了。 加减怎么做呢,肯定是需要加锁的,不然并发以来数据就乱了。 不加行不行,直接用update 表 set amount = amount + #{amount} where sku_id = #{skuId}。 好像也行,但其似乎不行,减库存需要校验amount + #{amount} > 0,所以加锁逃不过去了。 那就加个redis分布锁,用redisson做。 到这里好像就完了,目前的情况是: 1.新建两张表,库存表和日志表 2.接口请求参数为

{  
    "detail": [{  
        "skuId": 商品id,  
        "amount": 加减库存的数量,传正数  
    }],  
    "type": 业务类型  
}  

3.校验需要校验类型传参 4.接口需要根据sku维度加分布式锁 目前看好像没问题,大部分需求实现到这里就完了,也能满足需求,而且看上去实现也简单。 但是太过简陋!!!经不起线上业务推敲,一测试估计就崩了。 问题一:一个批量接口,会不会有性能问题 问题二:既然是一批数据,其中一条更新失败,需不要保证原子性,还是允许这种情况发生,亦或是两种模式都需要支持 想到这,接下来就是和产品沟通环节了,问题抛给他,你不做我自然高兴,你要做,我们再慢慢讨论。 产品回复: 第一个问题,肯定会啊,既然有商城,他们每季度一大促,每月一小促,有点并发是正常的 第二个问题,目前所有系统的要求是一批过来了,要么都成功要么都失败 好了,问题继续,既然要做那么第一个问题,我需要知道一批的量有多大,最多能传多少过来呢? 产品回复: 量吧,商城一次也就不超过100,但是WMS和ERP就不一定了,多的话一次可能上千不到一万的样子 从这里就可以看出,我们的产品开始不会考虑什么量的问题,他只知道需要提供这么一个加减库存的接口,而这种问题前期不考虑清楚,上线后大概率就是致命的。 先从减库存开始: 如果一次传100个,意思是我要循环100次加锁,如果其中一次失败,那么整体请求失败,同时删除已加的锁。 需要试下100个商品,接口响应是否满足要求,不满足还需要其他方案。 但是如果来了一个100个商品的减库存请求,那么就需要给锁加一个等待时间,否则容易提高报错频率。 然后是加库存: 一来就是一万不等的量,先不考虑请求大小的问题,单单加个锁就能锁死在那,直接上大概率超时,而且可能会影响商城下单。 所以,这里需要采取异步处理的方式,正好前段时间上了异步补偿方案,处理起来也不是很困难。但是光异步处理还不行,还需要拆分数据,将数据拆分成一条一条执行。 想到这,突然就有了两个问题: 1.批量请求过来的接口,sku会不会重复呢? 2.拆分执行的数据,其中一条失败了怎么处理? 所以第一步,接口在获取锁之前不论加库存还是减库存都需要对数据合并一次,相同sku的amount需要累加,这样做主要是减少锁碰撞概率。 第二条,失败一条怎么处理,失败一条走重试策略,由于加库存现在的需求上的限制较少,业务上一般都会成功,所以需要关心的问题是重试的手段是否完整。 这样处理下,加库存的流程将变成这样:

怎样完成一次实现设计 到这里,我们再来总结下加减库存两个接口需要做的事情 减库存: 1.校验数据类型是否合规 2.合并相同sku的amount 3.批量加锁,获取锁等待时间暂时定为300ms 4.如果批量加锁失败,需要删除已加的锁,并报错 5.如果加锁成功,校验库存是否充足 6.如果库存不足,报错并删除锁 7.如果库存充足,扣减库存并删除锁,业务执行成功 加库存: 1.校验数据类型是否合规 2.合并相同sku的amount 3.拆分数据,按每条数据发送到mq 4.异步消费加库存消息 5.单个sku获取锁 6.如果加锁失败,报错并等待mq重试 7.如果成功,加库存并删除锁 到这里,看上去一个加减库存的需求就差不多了。但是!这样就完了? 并没有,看上去已经思考了很多,也做了很大的优化,但实际还是有很多细节没处理。 问题一:既然是供其它系统调的,没有幂等怎么行,我们不清楚对方怎么调的,鬼知道他们有什么骚操作,万一一个订单调个两三次,bug他们修,数据我们修 问题二:加库存场景,一次请求循环发送mq,其中一条发mq失败怎么搞?前面的都已经发了,后面的没发,原子性被打破 问题三:消费重复? 第一个问题好解决,请求参数带上业务编号,相同业务编号的请求我们只处理一次,处理过的或处理中的都返回成功,至于业务编号怎么定,由每个调用方自己决定。 第二个问题既然循环发有问题,那么请求的时候就再套一层异步处理,接到请求后将请求参数记录到日志表,日志表记录成功就返回成功。 然后异步消息消费,消费的时候再循环发,但是需要带上这条信息的唯一值,在最后消费单个sku加库存时再做一次幂等处理,同时解决问题三。 就算循环发消息的任务其中一条失败,也可以通过重试等方式让数据保持最终原子性。 如此,加库存流程将会变成如下:

怎样完成一次实现设计 到这里,大体实现的思路有了眉目,需要和产品讨论的问题以及可能遇到的问题也有了一些解决手段,那么头脑风暴暂时结束。

完成需求设计

一、新建两张表 锁库表:

sku_idamountupdate_date
商品id数量更新时间

日志表:

business_idrequest_bodyresult
业务编号请求参数处理结果

二、接口请求参数 加减库存两个接口参数一致:

{  
    "detail": [{  
        "skuId": 商品id,  
        "amount": 加减库存的数量,传正数  
    }],  
    "type": 业务类型,  
    "businessId": 业务编号  
}  

三、接口处理步骤 减库存: 1.校验幂等性 0.5 2.校验数据类型是否合规 0.5 3.合并相同sku的amount 0.5 4.批量加锁,获取锁等待时间暂时定为300ms 0.5 5.如果批量加锁失败,需要删除已加的锁,并报错 0.5 6.如果加锁成功,校验库存是否充足 0.5 7.如果库存不足,报错并删除锁 0.5 8.如果库存充足,扣减库存并删除锁,业务执行成功 1 加库存: 1.校验幂等性 0.5 2.校验数据类型是否合规 0.5 3.合并相同sku的amount 0.5 4.保存接口请求日志并发送消息 1 5.消费消息,拆分数据,按每条数据发送到mq 1 6.异步消费加库存消息 0.5 7.校验幂等性 0.5 8.单个sku获取锁 0.5 9.如果加锁失败,报错并等待mq重试 0 10.如果成功,加库存并删除锁 1 关键性的步骤甚至可以贴出大致实现的代码

评估工作量

上面每步后面可以看到我都贴了一个数据,这个就是自己开发评估工作量的东西,2个点代表一天。 减库存4.5,加库存5,加起来一共9.5,多加0.5算送的摸鱼时间就是10,换算一下是5天的工作量。然后再套用上面说的乘以2,就是10天,两个接口到提测也就两周的时间(965模式)。 如果赶得急就乘1.5,不急就是2,这是比较安全的数字,如果小于7天半的开发时间,那么交付质量上就会开始打折扣。

总结

可以看到,如果按最开始产品的需要算,这两个接口最多也就两天的时间,但是实际简单思考一下, 真的想要高质量的交付,所要实现的东西远远不止他需求上那点,甚至中间还有些特殊情况产品根本罗列不出来,全靠我们摸索。 这里只是举一个简单例子,主要想表达的是一个如何将一个需求转换为一个实现设计,并评估出工作量。 有人可能会说,我就按需求走,没必要考虑那么多场景,照样能通过测试上线,有问题后续再优化。 我想说的是,既然设计实现能想到的东西,上线大概率就会出这些个问题,后续再改,改的不只是bug还将面临历史问题数据的修改。 同时,如果一个影响深的bug,要求紧急修复,这个班是加还是不加。 再一个,如果最开始设计上就没做到易于扩展性,今天加一点,明天加一点,到最后将变得举步维艰。我同事的那个就是最好的例子。 再来一个,即使按照实现设计上这套走下来,就真的没有纰漏了吗?然而实际不是,很多东西这里其实没有深入考虑,真实的业务需求也要复杂的多。