设计原则与代码实践(记一次线上bug)

bug来龙去脉

前段时间接到一个需求,因为排期非常紧,没有认真规划就开干了。

需求描述起来大致是这样:门店的app首页会展示6张门店图片,以前只有A类门店,现在新增了B类门店,也需要首页图片。但是不同于A类门店不存在状态,B类门店的首页图片需要有 上传-->审核\驳回 这样一个操作流程,分别对应图片 待审核、已通过、已驳回 三种状态。

之前A类门店是直接将6张图片的url数据拼接成一个字符串,直接存放门店表中,如下图:

image-20191212105059917.png

因为B类图片存在状态,所以我创建了一个新表,将app_img_url字段拆分若干条记录,如下图:

image-20191212104540975.png

修改后,对于图片的增删改查操作,需要同时操作两张表,其中有个查询门店信息的rpc接口,是高频调用,实现后的调用流程如下:

image-20191212112104126.png

但是我新增的查询门店图片信息实现,没有加缓存,导致上线后的第二天早上,数据访问量一个小时内激增近千万。

头皮发麻。

赶紧加上缓存,紧急部署,然鹅并没有访问量并没有全部降下来。又开始排查,发现两点原因:

  1. 图片增删改逻辑复杂,多个地方均有调用,缓存添加的不全;
  2. 有相当一部分门店没有门店图片,导致缓存穿透。

冷静分析一波,突然感觉被自己蠢哭了!之前之所以将门店信息和图片信息分成两个sql查询,是因为在其他地方实现了查询图片信息的接口,直接复用,当时还沾沾自喜,感觉自己遵循了开闭原则。。。伪代码如下:

public Class ***ServiceImpl {
    ...
    // 查找门店信息
    Shop shop = getShopInfo();
    // 查找图片信息  (新增代码)
    List<Img> imgs = listImgInfo();
    //设置图片信息  (新增代码)
    shop.setImgs(imgs);
    ...
}

这样只是我写的代码最少,但是却不是遵循了开闭原则,而是对原有接口进行了修改,完全可以在SQL查询中关联两张表,这样就不会有上面的问题了。流程如下:

image-20191212141224470.png

但是这样做还是有一个问题,那就是当查询量大的时候,left join对性能的损耗还是很大的。

这样我就考虑到是否将图片的url在保存的时候,在两张表都保存(只针对已通过状态的图片,因为查询只查找已通过状态的图片),这样查询rpc接口完全不用做任何修改,和原来的逻辑一样,只改变的了增删改的逻辑,而增删改是低频操作。

总结

上述bug是多方面原因导致的:

  1. 排期紧,没有认真规划需求的实现逻辑;
  2. 修改对外提供的rpc接口不够谨慎,直接在原代码中添加了修改逻辑。
  3. 原来的设计本身存在问题。

原因1暂且不论,着重讨论一下原因2和原因3。

修改对外提供的rpc接口不够谨慎

开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。

从开闭原则的定义可以看出,我的第一次修改,很明显没有遵循这一原则,在工作实践中,大部分情况下不遵循设计原则并不会造成很大的麻烦,倒霉的是,我遇上了少部分情况。

所以对于一些普通的业务代码,遵循设计原则,可能会对代码后期维护有利,但是收益一般并不明显。

但是对于一些高频率高并发的接口,尤其是其他系统提供的接口,遵循设计原则就变得尤为重要。因为这些接口一般调用频率会很高,同时后期扩展变动的几率大。

原来的设计本身存在问题

这一点指的是,原来的数据库设计就没有为后期扩展留出余量,一个门店对应多个图片,这种一对多的问题,一般情况下,应该创建两张表,两张表之间通过主键id或者编号关联,这样如果后期,图片数量增加了(比如由6张图片变成100张),图片类型变化了(比如需要区分图片的审核状态、区分图片的类型等等)······对于这些变化,两张表的设计扩展修改起来会方便许多。


莫小点还有救
219 声望26 粉丝

优秀是一种习惯!