前言
抽象,是一个宝贵的工具,能叫人们在更高的角度看待问题。
将要解决的问题抽象成一个模型,并根据对问题的理解不断的修正这个模型,最终利用这个模型去指导实践。这就是为什么人们要将繁杂的军事谋略抽象成孙子兵法,最终浓缩成一句话:致敌而不致于敌。
本文谈一谈对web系统设计的理解,从中抽象出一点理念性的东西,以为日后所复用。
存储,缓存,查询模型
现在网站数据一般会存储在数据库中,关于数据库表的设计,教科书上有三大范式,然而在实际实施时,有很多违反三大范式的设计(比如一定的冗余),或者为了数据安全,或者为了快速查询,这就说明数据的应用场景和数据的最优存储模型之间存在冲突。
一次写入,伴随一些变化,进而改变用户的查询结果。有时候不得不做一些取舍:是为了插入方便,还是为了查询方便。
本文将数据可能出现的形式归纳为三种模型:存储、缓存和查询模型。
存储模型,总的来说,尽量按三大范式进行
- 存储模型的底线是:要存储所有必要的的数据。这句话并不是废话,这告诉我们在设计表结构时,要包含哪些数据。进而再去考虑将这些字段分散在哪些表中。
- 尽量面向添加
- 一次操作要尽量少,比如,服务端改动一个东西要改四五条关联记录。
现在的业务系统中,越来越多的用到了缓存,多数为<key,model>
形式,那么这个model是否一定为数据库的model呢?
- 缓存应该尽量面向查询,最好客户端传入一个key,直接从缓存中返回model。从这个角度看,model不一定是数据库model。
- 客户端的情况可能千变万化,很难hash到一定范围的key空间,因此有时候缓存没必要完全就面向查询。
查询模型,主要面向用户,其一些概念、字段的设置要考虑到用户的理解
- 查询模型的底线是:不可少的描述客户端现在的状态
- 查询要尽量简化用户的操作,比如减少用户的传参数。
在设计的时候,还要兼顾系统的复杂性,至少理念上不能太复杂。
存储、缓存和查询模型的演化,就好比是os的发展,系统调用提供基本的功能,然后库函数不停地向上封装。
举一个配置中心的例子,假设客户端要与服务端同步配置项item,配置项由group 组织,每个group 有group versoin来标识数据的版本。
那么对于查询模型就是
[
"groupId":verson
]
服务端呢,也要显式或隐式的提供该用户对应的所有<groupId,version>
,以判定某个group是否要更新数据。
对于存储模型呢?以item 标识配置项,group 标识配置项组,db表结构模型如下:
item : id,groupId,name,value,desc
group : id,name,version,desc
对于缓存模型呢?通常是KV结构,可以将客户端状态序列化为一个字符串,但在group比较多的情况下不现实,因为配置项更改后,清空特定的缓存就会很复杂。因此,能够根据groupId快速找到所有配置项即可。
groupId : [
version : xx
item1 : {},
item2 : {}
}
同步请求
有的url请求很简单,就是拉取某个数据。而有一些请求则比较复杂,比如客户端和服务端同步一些数据。此时,如何妥当的抽象,去描述客户端的状态,如何描述服务端的状态,这个状态差值意味着什么,这是我们在实现这个项目时,要妥善思考的。
监控
知道你目前的系统的所处的状态,每个调用花了多上时间,哪些点是瓶颈;哪种异常比较多;用户经常会发起哪些误操作,如何避免。
架构设计
以笔者短暂工作的经历,所做所见项目的结构主要经历了以下变革:
- 所有代码在一个项目
- 不同业务代码拆开
- 不同业务代码的公共模块拆出,形成微服务。数据层面,加入缓存,读写分离。
- 业务层面要有监控报警、流控、降级、扩容缩容等措施。数据层面,要有异地多活等措施。
维护才是一个系统最大的生命周期
知道这句话没啥用,这句话意味着什么呢?以笔者目前的体会看:
- 设计模式的重要性,这可以让你看到Abstract、Template等关键字时,就知道大概的结构。过一段时间后能看懂、轻易的看懂自己写的代码,这很重要。
- 相关的逻辑写在一个类中(将数据和相关操作放在一个类中,本来就是面向对象的应有之意),比如有一个将A类转换为B类的工具类,那最好将B类转换为A类的代码也放在这个类中。因为,维护过程中为A类加了一个字段,也会牵涉到这段逻辑。放在一个类中,改的时候自然就注意到一起改。