怎样完成一次实现设计

前言

前段时间评完需求,看了看觉得还是有点复杂,于是分完任务便让同事们先写一下设计文档,两天后一起评审一下。

结果两天后一看,基本没有达到想要的结果,要么是过于简单几句话搞定,要么是原文照着需求说明抄了一遍。

不知道大家对实现设计如何看待,简单的需求无所谓,但如果稍微复杂的需求这一步个人认为还是挺关键的。

和组内同事沟通了下,大体是认为浪费时间,一般都是直接开搞,也不管需求复不复杂。

费时是肯定的,但相较于优点这点时间我觉得还是有必要的,而且如果前期设计没做好,后期花费修复这些设计上的问题才是最致命的。

不幸的是,在我来这家公司之前,正好出了这种典型的案例。

问题案例

需求是一个仓库预警+自动补货的场景,开始的时候主导的同事简单的分了下任务,三个人就开始干,工期一个月。

大概月底的时候,负责核心功能的同事发现其他两人给的接口完全和他想的不一样,于是无奈之下自己几乎重下了一遍他们的逻辑。

到这里,开发时间从一个月变成了两个月,一路坎坷还是提测了。

提测后又是一堆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_id amount update_date
商品id 数量 更新时间

加库存的场景有:采购入库(ERP)、调拨入库(ERP+WMS)、退货入库(商城+WMS)、商品调整(WMS)

减库存的场景有:销售出库(商城)、调拨出库(ERP+WMS)

主要的场景就这些,其他的就不列了,从这里可以看出,两个接口最基本的需要支持批量多商品的加减库存,因为采购入库和销售出库都支持批量操作。

对应的接口传参应该是:

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

但是光这样不行,既然目前都有至少六个入口,那么就需要一个类型来标识它是怎么调用的,不然到时候连这个商品的amount值怎么来的都不知道。

所以参数需要加上type,标识是哪个业务来源,同时还需要新加一张表记录修改日志

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

同时,既然有了业务类型,所以这里就多了一条检验,需要根据type校验场景,加库存的业务场景不能去掉减锁的接口减库存同理。

好了,接口字段就这样,表字段也那样了,现在开始具体讨论怎么实现了。

加减怎么做呢,肯定是需要加锁的,不然并发以来数据就乱了。

不加行不行,直接用update 表 set amount = amount + #{amount} where sku_id = #{skuId}。

好像也行,但其似乎不行,减库存需要校验amount + #{amount} > 0,所以加锁逃不过去了。

那就加个redis分布锁,用redisson做。

到这里好像就完了,目前的情况是:

1.新建两张表,库存表和日志表

2.接口请求参数为

json 复制代码
{  
    "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_id amount update_date
商品id 数量 更新时间

日志表:

business_id request_body result
业务编号 请求参数 处理结果

二、接口请求参数

加减库存两个接口参数一致:

json 复制代码
{  
    "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,要求紧急修复,这个班是加还是不加。

再一个,如果最开始设计上就没做到易于扩展性,今天加一点,明天加一点,到最后将变得举步维艰。我同事的那个就是最好的例子。

再来一个,即使按照实现设计上这套走下来,就真的没有纰漏了吗?然而实际不是,很多东西这里其实没有深入考虑,真实的业务需求也要复杂的多。

相关推荐
得物技术19 分钟前
营销会场预览直通车实践|得物技术
后端·架构·测试
Ice__Cai1 小时前
Flask 入门详解:从零开始构建 Web 应用
后端·python·flask·数据类型
武子康1 小时前
大数据-74 Kafka 核心机制揭秘:副本同步、控制器选举与可靠性保障
大数据·后端·kafka
紫穹1 小时前
006.LangChain Prompt Template
后端
whitepure1 小时前
万字详解JavaObject类方法
java·后端
切克呦1 小时前
通过 Cursor CLI 使用 GPT-5 的教程
前端·后端·程序员
Ice__Cai1 小时前
Flask 之 Request 对象详解:全面掌握请求数据处理
后端·python·flask·request·python web框架
gitboyzcf1 小时前
Git 常用命令
前端·git·后端
灵犀海棠1 小时前
FLASK项目快速构建
后端·python·flask
CptW1 小时前
字节面试题:实现任务调度器(Scheduler)
面试·typescript