怎样完成一次实现设计

前言

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

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

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

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

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

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

问题案例

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

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

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

提测后又是一堆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,要求紧急修复,这个班是加还是不加。

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

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

相关推荐
2402_8575893619 分钟前
Spring Boot新闻推荐系统设计与实现
java·spring boot·后端
J老熊27 分钟前
Spring Cloud Netflix Eureka 注册中心讲解和案例示范
java·后端·spring·spring cloud·面试·eureka·系统架构
Benaso30 分钟前
Rust 快速入门(一)
开发语言·后端·rust
sco528231 分钟前
SpringBoot 集成 Ehcache 实现本地缓存
java·spring boot·后端
我爱学Python!33 分钟前
面试问我LLM中的RAG,秒过!!!
人工智能·面试·llm·prompt·ai大模型·rag·大模型应用
OLDERHARD1 小时前
Java - LeetCode面试经典150题 - 矩阵 (四)
java·leetcode·面试
原机小子1 小时前
在线教育的未来:SpringBoot技术实现
java·spring boot·后端
吾日三省吾码1 小时前
详解JVM类加载机制
后端
努力的布布1 小时前
SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器
java·后端·spring
PacosonSWJTU1 小时前
spring揭秘25-springmvc03-其他组件(文件上传+拦截器+处理器适配器+异常统一处理)
java·后端·springmvc