商城项目是公司的初创业务,2017 年 4 月到 6 月间,经历了从 0 到 1 的过程。在两个月多时间,完成了商品浏览、下单、支付、仓储、物流的完整闭环,搭建了买家系统、卖家后台、运营中心三个系统。这个过程中经历了很多思考与选择,本文将从以下几个方面介绍相关的思考。另外在文末分享团队建设方面的心得。
业务:产品为王、技术服务于业务
效率:合理复用、高效自建、便于扩展
安全:面向外网用户,金钱交易
优化:抓住重点,阶段性优化
业务篇
新项目需要快速迭代,反复试错,在初期没有详细的产品设计文档,研发同学必须深入理解业务,从头到尾进行思考:要做成什么样?为什么做?用户体验怎么样?以此为基础,再去想技术如何实现。
库存管理作为供应链管理的重要组成部分,不论对电商还是实体企业都至关重要,库存体系的构建异常复杂,我们以库存系统和仓库系统为例,介绍我们如何从思考业务需求到设计数据结构,希望能为开发同学在实现业务需求时提供思路。
第一步、梳理需求
通过调研电商网站和实地考察仓库,总结出库存的需求分为线上和线下:
线上:电商网站的商品详情页上需要告诉用户是否可以购买当前商品?还可以购买几件?下单时需要扣减库存,预售活动需要设置预售数量等等。
线下:对于家居这类实物商品要有大型仓库存储,需要管理每日进入仓库的商品种类,每种商品的总量,每日发出商品数量,每种商品的剩余数量,剩余商品置于仓库的存储位置等等。
线上商品的库存变化和线下仓库密不可分,而仓库又和销售、采购相关、以下是简单的示意库存变动的影响因素。
库存变动的影响因素
由此我们可以梳理出与库存相关的业务需求都有哪些。
第二步、明确概念
梳理出业务需求后,我们对库存的概念进行拆分,定位更明确。
库存系统:管理各个商品的实时库存数量,面向前台,商品粒度;
仓库系统:管理实体仓里面的库存数量,面向后台,批次粒度;
有了仓库系统就可以管理商品的数量,为什么还要有库存系统呢?
某个商品 A 在实体仓里面有 10 个,仓库系统负责管理商品 A 的数量,以及它的位置信息。但是商品 A 在网站上不一定允许卖 10 个。因为可能在网站上已经有 3 个被用户买掉,只不过这 3 个商品还没有出库,所以在仓库系统里商品A还有 10 个,但此时网站上只能卖7个,这种可售卖的数量仓库系统是区分不出来的,它只是负责管理在当前时刻仓库里一共有多少库存,并不区分商品的状态信息。所以库存系统主要是用来解决这个问题。仓库系统管理的是仓库里面商品的实际数量,库存系统管理的是商品的可销售数量。
第三步、分层设计
有了清晰的概念,并且梳理出库存和仓库之间的关系后,从用户的角度出发,采用分层设计,分为销售层(库存系统),仓库层(仓库系统)。
库存分层
销售层:面向前端用户,库存决定是否可售卖,下单是否能成功。在活动秒杀时,活动库存决定是否可以秒杀成功;预售时,预售库存决定是否可以预定。
仓库层:对应实物库存,采购入库、下单出库、退货入库、盈亏盘点、仓库调拨都会引起仓库库存的变动。
第四步、解决难点
库存如何同步?
库存的变动有两种流向,自上而下(销售层 → 仓库层)、自下而上(仓库层 → 销售层)两种。
自上而下:用户下单后,首先会生成订单,扣减销售层的库存;然后生成发货通知单,推送至仓库,仓库系统同步响应,扣减库存进行出库。
自下而上:主要有三种入库:采购入库单、退货入库单、调拨入库单的实物入库,会连锁引起仓库层、销售层的可用库存逐步增加。
下图为售后订单、订单发货、采购入库等引起的库存变动、仓库库存变动:
库存系统、仓库系统、商城其他系统如何交互?
我们针对采购入库、下单出库、退货入库三个需求,梳理了各系统之间的时序图,考虑了各种异常情况。下图截取了退货时未付款和已付款两种场景举例:
退货的部分场景时序图
除此之外还有诸多细节不在这里说明,如盘点库存、库存/仓库系统如何对账等。
第五步、设计数据结构
数据结构是 web 开发的基石,在项目之初应当尽可能的设计出较为完善的数据结构,并且具有一定的前瞻性。上线后再对表结构重构或频繁变更,将会带来代码变更,数据补全等问题,这些问题的解决成本是巨大的。
正是由于我们梳理了需求、设计出不同层级的库存系统、清晰各系统的交互、并且针对核心问题设计出解决方案,这才使得我们能够设计出较为完善数据结构。
数据表 E-R 图
效率篇
初创项目,会不断的试错,需求变更频繁。这就要求我们快速迭代、持续交付、尽早反馈。所以提高效率、快速落地是我们重点关注的问题。下面将从三方面介绍我们是如何高效开发的:
合理复用现有服务、框架、技术组件
代码具有可扩展性
高效的工作效率
一、 合理复用
在系统建设初期要求快速上线,所以首先要思考哪些服务可以直接复用,避免重复造轮子,节省开发成本。其次哪些服务需要自建,避免协作团队不能及时响应我们的需求。
1. 通用服务
通用服务指的是公司或部门级别的基础服务或业务平台,能否复用主要从几方面考量:
功能是否符合当前业务需求;
平台是否稳定,有完善的监控;
是否有稳定的团队或人员维护;
由于前期用户量较少,且通用服务平台为支撑多个业务线设计,所以可以暂时不用考虑性能因素。基于上面的考虑,我们复用的通用服务有:图片系统、搜索平台、监控平台、数据平台。
2. 自建业务系统
之前提到系统建设初期有种种问题,所以在自建业务系统时尽量简单、灵活、可扩展。
简单:工程目录、代码结构都从简单入手,避免复杂设计带来的维护成本;
灵活:前期多采用单体式应用,不使用 SOA,但考虑到中短期规划,将系统根据业务划分多个模块,各个模块高内聚、低耦合,便于以后服务化改造。后面架构优化章节会详细介绍。
可扩展:下面一节会详细介绍可扩展。
3. 技术框架与组件
在技术框架和组件选择时,偏向于成熟框架,成熟技术组件。选择时需要考虑:
· 技术文档是否完善;
· 技术问题解决方案是否全面;
· 是否能够完全满足当前需求;
· 接入是否简单、是否易用,是否便于维护;
以数据访问中间件的选择为例,前期需求:读写分离,卖家侧、运营侧读写从库,不影响买家侧读主库的性能。
最初我们选择当当的开源框架 sharding-jdbc,该框架功能完备,性能影响较小,符合需求。但使用过程中发现该框架存在一些 bug,bug 解决方法难以查询,提交开源组织解决又需要跟版本升级,迭代较慢。最终放弃该框架,选择自己编写基于 mybatis 的读写分离方案,该方案虽然功能单一,但正因为功能单一,代码量少,便于维护,能够满足前期需求。
基于上面对通用服务复用、自建业务系统的思考,商城项目初期整体架构落地形式如下图所示:
二、 便于扩展
项目初期一些运营决策是不确定的,有时候会频繁变化或尝试接入多家供应商,例如:与第三方仓储物流系统的对接,开始选定的是德邦,但经过深入调研、在与德邦沟通联调的过程中,发现成本过高、系统对接响应慢、用户体验差等问题后,会随时更换供应商或选择多个供应商。为了应对这些变化,要求我们在编写代码时尽量做到可扩展、可替换,避免运营的决策变更增加大量开发成本,推迟上线时间点。
选择恰当的设计模式很好的帮助我们解决这些问题,针对上面的场景,我们采用桥接模式支持多个第三方仓储物流系统的可插拔式切换。
基本原理:将抽象部分(仓库系统与物流系统对接的接口)与它的实现部分(具体第三方接口调用实现)分离,使它们都可以独立的变化。由于行业内有较规范的物流系统解决方案,所以仓库系统与物流系统对接的接口一般不会随着接入的第三方不同而变化,但具体的第三方物流公司的接口协议、参数会有不同,所以如果需要替换第三方物流公司,采用桥接模式只需要编写接口的具体实现类即可。具体的代码类图如下:
桥模式仓储设计类图
除此之外,还使用了多种设计模式解决扩展性问题:
状态模式-用于订单状态的流转,随着业务功能的逐步完善,状态流转会越来越复杂,状态模式抽象了订单状态的转变,新增业务功能后只需要新增订单状态流转实现类;
模板方法模式-用于性能统计,随着系统统计、监控功能的逐步完善,代码中需要埋点的地方越来越多,模板方法将统计、监控代码抽离出来统一维护,避免对代码的侵入。
三、 提高工作效率
除了系统架构和代码编写提效外,要尽可能用有限的时间完成更多的开发任务,在这里与大家分享一些可以提升工作效率的方法。
· 如果可能就别去开会:会议会导致多人效率的同时下降。如果不是那种非参加不可的会议,那就别参加了。你可以说手头还有很多事情要做(也许事实就是如此),然后在会议后问一下参会的同事,了解一下重要的内容就行。如果真的有必要参加某个会议,那么请记住下面这些原则:
o 一定要设定好要讨论的主题,别随意发散
o 设定严格的会议结束时间,会议时间控制在 1 小时内
o 会议结束时一定要确定好清晰的下一步行动计划
· 先做必要的需求:敢于和 PM 说不,为了达成本次迭代上线目标,整理出必要的需求点和非必要的需求点。制定排期时根据产品、运营要求的时间点倒推(在业务前期多数情况是多部门同步配合,产品尽快投入市场,所以上线时间点非常关键),如果时间不够开发所有需求点,要和产品沟通先确保必要的需求按时完成,不必要的需求排在下次迭代。
· 自动化:在面对无聊的事情时,尽可能自动化,比如自动比较线上库和测试库的表结构变更,使用 mysqldbcompare 工具自动比较并生成表结构变更 SQL。请不要将你的精力浪费在机器能够更快、更可靠完成的事情上。
· 多做事半功倍的事情:有时候花精力做一些与编码相关的事情,反而能起到事半功倍的效果。例如:编码过程花费一些时间编写单元测试,看似增加了开发成本,但会节省团队成员之间联调、QA 测试、修复 bug 的时间。
安全篇
由于商城项目直接涉及金钱交易,其本身安全性至关重要,如果用户在做金钱交易时出现安全问题,这会给公司的品牌形象、网站的信誉带来毁灭性打击,也会给用户带来经济损失。所以研发团队需认真对待安全问题,不仅是对用户负责,也是对公司负责,对自己的职业生涯负责。
想要在两个月时间内开发出一个安全的 Web 应用其实是非常困难的。这是个比较大的话题,篇幅有限,在本文中只做简单介绍,后续将用一篇文章的篇幅重点介绍相关的细节。
如何在有限的时间里构建一个相对安全的网站?
构建防御体系,安全是个全方位的事情,某一个环节存在漏洞就会导致满盘皆输
建立安全开发流程,从技术上降低安全风险
一、 防御体系
结合系统的整体架构,从下面 4 个层面采取安全措施,在这里列出我们在前期所做的一些工作,具体细节将在后续文章重点介绍。
客户端:
Xss 攻击防御,XssFilter 过滤请求参数,重要 cookie 设置 httponly
CRSF 防御,参数加密生成 token 并放在表单提交时传递
URL 跳转,建立白名单对重定向 url 校验
网络层:
使用 HTTPS 协议
内往网分离,服务器间调用采用内网域名,确保后台数据库和后台服务无法通过外网访问
端口控制,所有服务只开启必要端口,关闭不用端口
应用层:
业务逻辑校验,如:只允许用户操作自己的订单、支付金额与下单金额比较等
针对刷单、恶意攻击行为增加防御,如:增加购买次数限制、支付超时时间等
安全的 API
确保用户使用 API 之前对其认证,如:登录、签名
确保 API 没有可枚举资源,如:订单号、用户 ID 等
API 参数校验
后台应用用户访问权限控制
数据层
使用 mybatis 预编译机制避免 SQL 注入
最小访问权限分配数据库账号
二、 安全开发流程
软件开发有一套完整的开发周期,同样安全也有 SDL(软件安全开发周期),SDK 是从安全角度指导软件开发过程的一套管理模式,将软件安全的考虑集成在软件开发的每一个阶段 需求分析、设计、编码、测试和维护。
我们按照 SDL 进行开发,使得提前发现安全风险、确保安全策略正确落实、快速修复安全漏洞,确保在一次次迭代上线后,交付一个相对安全的版本。各阶段所做工作如下图所示:
优化篇
在业务前期,访问量的绝对值虽然还不是太高,但我们仍然需要持续关注接口的性能与响应时间。特别是业务推广初期,用户的第一印象将直接影响其对业务的心理评判。由于业务压力大,用户访问量不太高,在性能优化上又不能投入过多精力,这就需要我们权衡成本与收益,要做适度的优化。
综上,最终把精力放在针对移动网络下页面加载速度的提升,主要从链路优化和架构优化两方面进行。
一、 链路优化
PC 时代我们访问网站的接入条件是相对恒定的,所以在开发时很少考虑网络对用户体验的影响。但是移动 APP 则不然,尤其是在中国,基础的移动网络环境并不好,而且我们有很多用户的访问是发生在地铁、公交车这样的移动环境下,移动基站的频繁切换进一步增加了网络的不稳定。从下图手机淘宝统计出的数据可以看出,电商网站活跃用户中有不少来自类似 2G 这样的弱网环境,如果端到云连接不稳定、高延时,那么所有的用户体验都无从谈起。所以链路优化是我们提升用户体验,提高性能的重要环节。
手淘移动网络使用占比图
链路优化的目标:提升用户访问体验(低延时)。服务端响应时间只占整个请求路径上的很小一部分,无线端更多的是优化中间通道,我们主要从三方面进行了优化:
减少数据传输:
图片采用 webp 格式(有损 100%压缩,大小减少 30%),减小图片大小,前端根据浏览器版本自适应图片格式
Tomcat 开启 gzip 压缩,减小返回结果大小
连接优化:
请求合并,使得一个页面尽量少的请求
CDN 厂商优化
Tomcat 开启 HTTP2(减少协议头信息、减少响应信息、降低页面下载的连接数)
TCP 协议参数调优
提高可用连接数量,重启 TIME_WAIT sockets
关闭 TCP 快速回收
设置合理的 SYN 队列长度、缓冲区大小(Socket buffer > 64k)
初始拥塞控制窗口不小于 10。因为大部分页面在 10kB 以下,很多请求在慢启动阶段已经结束,改为 10 可以降低小页面资源传输时延。内容越大,这个选项的效果就比较不明显
开启 keepalive,减少 CLOSE_WAIT sockets
利用并发:
前端 js 包拆分
针对图片异步加载,合理的预加载机制
前后端分离、ajax 并发调用后端接口
二、 架构优化
除了链路的优化,面对当前的需求,首先面临的问题是选择什么样的应用架构,既能支持现有的业务需求、性能要求,又能面向未来,保证架构平滑过渡。应用作为独立可部署的单元,为系统划分了明确的边界,深刻影响系统功能组织、代码开发、部署和运维等各方面。我们思考的过程中主要参考两种应用架构:单体架构、面向服务架构。
1.单体架构:不切分系统,系统内部耦合
优点:
一个应用包含所有功能,容易测试和部署;
运行在一个物理节点,环境单一,运行稳定,故障恢复简单;
接口之间调用无网络通信,性能最佳;
缺点:
业务边界模糊,模块职责不清晰,当系统逐渐变大,代码依赖复杂,难以维护;
所有人同时在一个工程上开发,容易发生代码修改冲突,依赖复杂导致项目协调困难,并且局部修改影响不可知,需要全覆盖测试,需要重新部署,难以支持大团队并行开发;
当系统很大时,编译和部署耗时。
应用水平扩展难,不同模块对资源需求差异大,当业务量增大时,一视同仁地为所有模块增加机器导致硬件浪费。
2.面向服务架构:先竖切系统,再横切,系统间纵向依赖
优点:
每个 service 聚焦某方面核心业务,同时以复用的方式供整个系统共享;
服务作为独立的应用,独立部署,接口清晰,很容易做自动化测试和部署;
服务是无状态的,很容易做水平扩展;通过容器虚拟化技术,实现故障隔离和资源高效利用,业务量大的时候,加机器即可;
基于 SOA 的系统可以根据服务运行情况,灵活调控服务资源,包括服务上下架、服务升降级等,使系统真正具备可运营的能力;
缺点:
系统依赖复杂,给开发/测试/部署带来一系列挑战。
端到端的调用链路长,可靠性降低,依赖网络状况、服务框架及具体 service 的质量。
分布式数据一致性和分布式事务支持困难,一般通过最终一致性简化解决。
端到端的测试和排障复杂,对运维提出更高要求。
通过上面的介绍可以看出两种应用架构的优缺点很明显,分别应对系统发展的不同阶段,在初期阶段业务简单采用单体架构比较合适,但也要考虑到未来业务复杂度提高,大团队并行开发效率等因素,我们的架构应能够平滑过渡。基于这些思考,我们采用单体架构,但有一定的变形,初步落地架构如下:
买家系统、卖家系统、运营系统分别为三个独立单体应用,分别测试、部署;
service 层以 jar 包方式提供给卖家系统和运营系统使用;
后续演进为面向服务框架时,将数据库按业务拆分,将 service 以服务化方式提供出去即可;
确定好应用架构后,针对当前架构的性能的优化,主要关注了数据的动静分离,基于以下几个原则分别做了优化:
让用户的请求尽量不要经过 java 系统
前后端进行分离,静态资源不访问 webserver
让静态数据放在离用户最近的地方
静态资源使用 cdn 缓存
动态数据中变化不频繁的数据访问 cache
让动态数据尽可能的小
nginx 开启 gzip 压缩
部署的架构如下图所示:
三、 最终效果
经过上述各点的优化后,采用基调网络监控,页面加载时间由 5s 降至 3s 以内,请求耗时主要集中在内容下载时间,花费在建立链路的时间较少,基本达到移动用户使用的标准。后续仍需在链路优化和后端优化上做持续投入。
团队建设篇
文章最后简单分享一下在团队建设方面的心得:
一、 各尽其职
团队开发人员分工时高内聚、低耦合,一个人分配有关联的 N 个独立业务模块,一方面便于深入理解业务,另一方面能够聚焦业务领域。
二、 业务了解
让开发同学参与产品、运营的部分调研,一方面深入了解业务,另一方面增加和产品运营同学的沟通机会,减少团队协作中因缺乏沟通、理解偏差产生的不稳定因素。
三、 建立技术规范
初创项目前期迭代频繁,频繁的迭代会使发生故障的概率增大,所以控制每次迭代的风险很重要。为此我们结合之前项目规范经验持续推动技术规范的落地:
新人须知
开发规范
API 定义规范
java 编码规范
SQL 书写规范
数据库设计规范
项目管理规范
技术方案设计模板
性能测试规范
提测规范
上线 checklist、步骤模板
这些技术规范的建设大大降低了初创团队的工程风险。同时新加入到同学,能够快速的了解开发习惯,统一开发风格,提高代码维护性。
总结篇
本文从初创项目前期发展需要出发,通过业务、效率、安全、性能四个方面进行了分享。结合业务特点和不同发展阶段的需要,做出合理的技术设计,设计过程中权衡收益与成本,用有限的时间做最重要的事情。
本文转载自公众号贝壳产品技术(ID:gh_9afeb423f390)。
原文链接:
https://mp.weixin.qq.com/s/3TsU0B_K9ghqTtMNRwNN2g
评论