云原生微信小程序开发实战读书笔记-8

大多数互联网应用产品都会不断产生各种数据(可能是用户产生的,也可能是系统自动生成的)。要想高效保存这些数据,并维持应用产品的有效运转,就要用到数据库。

数据库是高效存储数据、读取数据的存储器。我们日常用到的绝大部分应用,都是把数据库放在云端,通过应用后端服务独立控制。

数据库是后端服务架构中最主要的部分,数据库的架构设计,以及灾备机制都属于应用开发中不可或缺一环。在项目开发初期,开发者就要做好架构设计,不过这会耗费大量的精力,从而对需要快速试错上线的产品造成不利影响。

云开发数据库是非关系型数据库,遵循 Mongo 协议,并提供丰富的操作 API,满足绝大部分业务场景下的数据操作需求。阅读数据库 API 文档就可以快速上手使用云开发数据库了。

数据库的设计

范式化

范式化是把数据像关系型数据库一样分散到不同的集合里,不同的集合之间可以通过唯一的 ID 相互引用数据,不过要引用这些数据往往要进行多次查询或使用 lookup 进行联表查询。Drawing 0.png

反范式化

反范式化是把文档所用的数据都嵌入文档内部,如果要更新数据,可能要查出整个文档,修改之后再存储到数据库里,如果没有可以进行字段级别的更新指令,大文档新增字段的性能较低。而范式化设计因为集合比较分散(也就比较小),更新数据时可以只更新一个相对较小的文档。Drawing 2.png

由此可见,数据既可以内嵌(反范式化),也可以采用引用(范式化),两种策略各有优缺点,关键是你要选择适合自己应用场景的方案。

完全反范式化可以大大减少文档查询的次数。比如你的应用数据查询比较频繁,但不用频繁更新,那就适合完全反范式化,没必要把数据分散到不同的集合,牺牲查询的效率。

完全范式化会降低文档更新的成本。如果应用数据需要频繁更新,业务数据特别复杂,你就要对数据库进行一定的范式化设计,不然用反范式化的设计会让集合过大,冗余数据更多,出现数据写入性能差的问题。

Drawing 3.png

数据库数据模式

云开发数据库的数据模式比较灵活,主要体现在以下两点。

  • 关系型数据库要求你在插入数据前必须定义好一个表的模版结构,而云开发的文档型数据库中数据的集合 collection 并不限制记录 document 结构。

  • 另外关系型数据库需要开发者对数据库的结构内容做声明描述,才可以正常运作,而云开发数据库不需要预先声明,在使用时也不会限制记录的结构,同一个集合记录的字段可以有很大的差异。如下图所示:Drawing 5.png

这种灵活性让对象和数据库文档之间的映射变得很容易,即使数据记录之间变化很大,每个文档也可以很好地映射到不同的记录。当然了,在实际使用中,同一个集合中的文档最好都有一个类似的结构(相同的字段、相同的内嵌文档结构)方便进行批量的增删改查以及聚合等操作。

预填充数据

虽然云开发数据库不像关系型数据库那样用声明结构限定记录的内容,但是开发者要对业务数据的类型、长度等有一些把控。

如果在开发时,你就知道这个数据库集合以后要用到哪些字段,那么在第一次插入数据时就该预填充这些字段,这样在用到时就能用更新指令进行字段级别的更新,不用再给集合新增字段,效率会高很多。

当你在用户注册时预先设置结构内容,后续更新时,能直接用更新操作符进行字段级别的更新。另外,当集合越大,修改的内容又比较少,使用更新操作符来更新文档,会大大提升性能。

如何保证数据库的安全

数据库安全规则

数据库是应用的数据存储核心,其稳定性和安全性会直接影响应用的稳定性和安全性。不管是什么应用,开发者最不想出现数据库被删、数据被篡改的情况。云开发数据库默认配备最基本的权限管控,有4 种基础策略:

  • 所有用户不可读写;

  • 所有用户可读;

  • 仅创建者可读写;

  • 所有用户可读,仅创建者可写。

云开发数据库的一条记录,往往因为反范式的设计,导致其承载的信息非常多,所以简单的权限管控并不能满足复杂数据更新条件的配置。在这个情况下,安全规则接替基础权限策略,提供更灵活的管控。

云开发数据库安全规则是一个可以灵活自定义数据库读写权限的权限控制方式,通过配置安全规则,开发者可以配置客户端、服务端发起的数据库操作权限规则,自动拒绝不符合安全规则的前端数据库与云存储请求,保障数据和文件安全。

安全规则身份认证

云开发数据库安全规则在配置中使用全局变量 auth 与 doc 进行组合(auth 表示的是登录用户,而 doc 与云开发环境的数据库相关),让登录用户的权限依赖于记录的某个字段。

使用安全规则之后,用户与数据库之间产生联系。 doc 不只包括标定用户身份的_openid,还有很多字段,这让数据库的权限有了很大的灵活性。举个例子,一个数据库集合的安全规则如下:

//登录用户为记录的创建者时,才有权限读
"read": "auth.openid == doc._openid",
//不允许记录的创建者删除记录(只允许其他人删除)
"delete": "auth.openid != doc._openid",

在这个例子中,auth.openid 是当前的登录用户身份 ID,而记录 doc 里的 openid 是之前业务中对该记录产生某种关系的用户身份ID,这里是记录的创建者。
所以上面例子的第一个语句,read(只读)配置,登录用户 ID 是记录创建者时才可以读,同理 delete 的配置是记录创建者不能删除,只能其他人可以删除。

提升数据库的性能

优化建议

  • 要合理使用索引: 使用索引可以提高文档查询、更新、删除等操作的效率,你要结合查询情况,适当创建索引,尽量避免全表扫描,考虑在 where 及 order by 涉及的列上建立索引。
  • 结合查询情况创建组合索引: 要想查询包含多个字段(键)条件,创建包含这些字段的组合索引是个不错的解决方案,组合索引遵循最左前缀原则,因此创建顺序很重要。
  • 查询时要尽可能通过条件和 limit 限制数据: 在查询里 where 可以限制处理文档的数量,而在聚合运算中 match 要放在 group 前面,减少 group 操作要处理的文档数量。无论是普通查询还是聚合查询都应该用 limit 限制返回的数据数量。
  • 尽可能限制返回的字段等数据量: 如果查询无须返回整个文档或只是用来判断键值是否存在,普通查询可以通过 filed、聚合查询可以通过 project 来限制返回的字段,减少网络流量和客户端的请求内存占用。
  • 查询量大时不要用正则查询: 正则表达式查询不能使用索引,执行的时间比大多数选择器更长,所以业务量比较大的查询请求,不建议用正则查询(尽量用其他方式代替),如果用一定要尽可能地缩写模糊匹配的范围(比如用开始匹配符 ^ 或结束匹配符 $ )。
  • 尽可能使用更新指令set: 通过更新指令修改文档可以获得更好的性能,因为更新指令不需要查询到记录就可以直接对文档进行字段级的更新,尤其是对不用更新整个文档只更新部分字段的场景。
  • 不要对太多数据进行排序: 不要一次性取出太多数据并对数据排序,排序要尽量限制结果集中的数据量,比如先用 where、match 等操作限制数据量,也就是通常要把 orderBy 放在普通查询或聚合查询的最后面。
  • 不要让数据库请求做多余的事情: 数据库尽可能只做必要的工作,对数据进行加工处理的操作尽可能转到外部去操作;另外,尽可能一次性取出业务所需的全部数据,如果不能则需要合理设计数据库结构。
  • 使用短字段名: 和关系型数据库不同,云开发数据库是文档型数据库,集合中的每一个文档都需要存储字段名,因此字段名的长度相比关系型数据库来说需要更多的存储空间。
  • 尽量不要把数据库请求放到循环体内: 我们经常会有查询数据库里的数据,并对数据进行处理之后再写回数据库的需求,如果查询到的数据很多,我们要进行循环处理,而这时你要注意,不要把数据库请求放到循环体内,而是先一次性查询多条数据,在循环体内对数据进行处理之后再一次性写回数据库。

提升数据库性能的设计

  • 增加冗余字段

在业务上有些关键的数据可以通过间接的方式查询获取到,但是由于查询时会存在计算、跨表等问题,这个时候建议新增一些冗余字段。

比如你要统计文章后面的评论数,你也许会把文章的评论独立建了一个集合如 comments,这时要获取每篇文章的评论数可以根据文章的 ID 条件来 count 该文章有多少条评论。

类似于评论数的还有点赞量、收藏量等,这些虽然都是可以通过 count 的方式来间接获取到的,但是在评论数很多的情况下,count非常耗性能,而且还需要独立占据一个请求。

我建议你在数据库设计时,用所谓的冗余字段来记录每篇文章的点赞量、评论数、收藏量,在小程序端直接用 inc 原子自增的方式更新该字段的值。

比如我们希望在博客的首页展示文章列表,而每篇文章要显示评论总数。使用 count 获取总数,不如直接查询新增的冗余字段commentNum来得直接。

  • 虚假删除

有时候我们的业务会需要用户经常删除数据库里面的记录或记录里的数组,但是删除数据非常耗费性能,碰上业务高峰期,数据库会出现性能问题。这时,我建议你新增冗余字段做虚假删除,比如给记录添加 delete 的字段,默认值为 false,当执行删除的时候,可以将字段的值设置 true,查询时只显示 delete 为 false 的记录,这样数据在前端就不显示了。在业务低谷时比如凌晨可以结合定时触发器每天这个时候清理一遍。

  • 尽量使用一个数据库请求,代替多个数据库请求

尤其是用户最常访问的首页,如果一个页面的数据库请求太多,会导致数据库的并发问题。有些数据能够缓存到小程序端就缓存到小程序端,不必过分强调数据的一致性。