• 后端存储实战课总结(上)


    《后端存储实战课》 的学习笔记,欢迎阅读斧正。

    创建和更新订单

    表设计

    最少应该有以下几张表:

    1. 订单主表:保存订单基本信息
    2. 订单商品表:保存订单中的商品信息
    3. 订单支付表:保存订单支付和退款信息
    4. 订单优惠表:保存订单的优惠信息

    订单主表和字表是一对多关系,关联的外键是订单主表的主键(订单号)。

    幂等性

    可能出现重复下单的场景:一种是就是用户多点了,另外就是网络错误导致(RPC、网关的重试机制)

    解决方案:通过一个专门生成全局唯一订单号的服务,插入数据的时候将其保存

    ABA 问题

    更新订单相关信息的时候可能出现。

    解决方案:在表中加个时间戳或者版本的字段,每次查询的时候将其返回,更新的时候要比较下当前订单数据的版本号,是否和预期的一致。不一致直接拒绝更新,一致的话在一个事务中更新数据的同时将版本号+1。

    商品详情页

    问题

    一是高并发,商品的详情页每天都会有很高的 DAU(日均访问次数)。

    二是商品数据规模的问题,比如下图中要存储的:后端存储实战课

    方案

    基本信息相对来说比较固定,可以在数据库中建表存储,也要提供缓存(比如 Redis、Memcached)帮助系统抗一些读请求。另外要注意的是,要保留商品的历史版本,因为订单中关联的商品数据必须是下单哪个时刻的商品数据,可以用一张历史表来保存。

    商品参数指商品特征,比如内存大小、屏幕尺寸、口红色号等,和基本属性一样,是结构化的数据,但是不同类型的商品参数也是不一样的。对于属性不固定的数据来说,可以使用 MongoDB 来存储,因为 MongoDB 没有数据表要有固定结构的要求。

    MongoDB 中的每一行数据,在存储层就是简单地被转化成 BSON 格式后存起来,这个 BSON 就是一种更紧凑的 JSON。所以,即使在同一张表里面,它每一行数据的结构都可以是不一样的。当然,这样的灵活性也是有代价的,MongoDB 不支持 SQL,多表联查和复杂事务比较孱弱,不太适合存储一般的数据。

    图片和视频由于占用空间比较大,一般的存储方式是在数据库中只保留图片视频的 id 或者 url,实际的图片视频以文件方式存储,可以保存在对象存储(Object Storage)中。

    对象存储可以简单理解为无限容量的大文件 KV 存储,key 是唯一的,对象是 value,可以通过 key 操作 value(写入、访问、删除)。对象存储提供了客户端 API,可以在 Web 页面或者 app 中直接访问,而不用通过后端服务。页面通过提供的 URL 直接访问,省事不占带宽,同时,对象存储云服务也提供了自带 CDN(Content Delivery Network)服务,响应时间比直接请求业务服务器更短。云服务厂商还会提供针对性优化,比如说缩放和转码服务。

    商品介绍在商详页中占得比重是最大的,包含了大量的带格式文字、图片和视频。其中图片和视频自然要存放在对象存储里面,商品介绍的文本,一般都是随着商详页一起静态化,保存在 HTML 文件中。

    静态化是相对于动态页面来说的。一般我们部署到 Tomcat 中的 Web 系统,返回的都是动态页面,也就是在 Web 请求时,动态生成的。比如说商详页,一个 Web 请求过来,带着 SKUID,Tomcat 中的商详页模块,再去访问各种数据库、调用后端服务,动态把这个商详页拼出来,返回给浏览器。
    商详页的绝大部分内容都是商品介绍,它是不怎么变的。那不如就把这个页面事先生成好,保存成一个静态的 HTML,访问商详页的时候,直接返回这个 HTML。这就是静态化。
    商详页静态化之后,不仅仅是可以节省服务器资源,还可以利用 CDN 加速,把商详页放到离用户最近的 CDN 服务器上,让商详页访问更快。
    至于商品价格、促销信息等这些需要频繁变动的信息,不能静态化到页面中,可以在前端页面使用 AJAX 请求商品系统动态获取。这样就兼顾了静态化带来的优势,也能解决商品价格等信息需要实时更新的问题。

    购物车

    场景

    1. 未登录,浏览器加购,关闭浏览器再打开,加购商品应该存在
    2. 未登录,浏览器加购,然后登录,加购的物品存在
    3. 登录后再注销,关闭浏览器,步骤 2 中加购的商品不登录看不到(未登录特定账号看到的购物车是空的)
    4. 手机登录,步骤 2 中的加购的存在

    原则

    用户未登录,需要临时暂存购物车中的商品;

    用户登录,将暂存购物车的商品加入到用户购物车,清空暂存购物车;

    用户登录后,各端同步用户购物车。

    设计

    暂存购物车

    如果存在服务端,需要唯一标识存储,浪费服务端资源,故需要存储在客户端。

    存在客户端的途径:SESSION 不合适(时间短,SESSION 数据其实还是在服务端);Cookie 存储的话,实现简单,通过服务端读写 Cookie 实现具体的加减购物车合并购物车逻辑;LocalStorage 存储的话,实现复杂(客户端和服务端都需要实现逻辑),但是容量大,不用每次请求都携带,节省带宽。

    用户购物车

    还是推荐 MySQL,如果用 Redis 存在丢数据的情况,查询方式和事务都不如 MySQL。

    引入 Redis 需考虑问题:

    1. 不同用户不同购物车,缓存命中率不高,为了维护缓存,还提高了系统复杂度,有没有必要
    2. Redis 的缓存更新策略

    账户系统

    目的

    对账系统存在的目的:来核对、矫正账户系统和其他系统之间的数据差异。

    每个账户系统都不是孤立存在的,至少要和财务、订单、交易这些系统有着密切的关联。理想情况下,账户系统内的数据应该是自洽的。所有用户的账户余额加起来,应该等于这个电商公司在银行专用账户的总余额。账户系统的数据也应该和其他系统的数据能对的上。比如说,每个用户的余额应该能和交易系统中充值记录,以及订单系统中的订单对的上。

    记录流水,可以修改由于系统 bug 或者认为篡改导致的账户余额的问题。流水的数据模型至少要包含:流水 ID、交易金额、交易时间戳、交易双方的系统、账户、交易单号等信息。

    原则

    1. 流水记录只能新增,一旦记录成功不允许修改和删除。即使是由于正当原因需要取消一笔已经完成的交易,也不应该去删除交易流水。正确的做法是再记录一笔“取消交易”的流水。
    2. 流水号必须是递增的,我们需要用流水号来确定交易的先后顺序。

    在对账的时候,一旦出现了流水和余额不一致,并且无法通过业务手段来确定到底是哪儿记错了的情况,一般的处理原则是以交易流水为准来修正余额数据,这样才能保证后续的交易能“对上账”。

    方案

    首先要保证只有记录流水的时候更新余额,不可以将更新余额单独提供;其次使用数据库事务,记录流水和修改余额两个操作要么都成功,要么都失败。

    MySQL 事务和 ACID 相关内容不在此赘述

    分布式事务

    背景

    分布式环境下跨系统数据一致性问题。

    方案

    2PC、3PC、TCC、Saga、本地消息表都是分布式事务的解决方案。

    2PC

    2PC(Two Phase Commit,两阶段提交),两个阶段指的是准备阶段和提交阶段。

    以订单和优惠券为例:订单系统中需要在「订单优惠券表」中写入订单关联的优惠券数据以及在「订单表」中写入订单数据,两个操作的一致性可以通过数据库事务解决;促销系统中需要将优惠券的状态改为「已使用」。

    在分布式系统下,我们需要上述两个系统的更新操作保持一致,要么都成功,要么都失败。

    准备阶段

    准备阶段要做的:除了提交数据库事务之外的所有工作,都需要在此阶段完成。如下述的两个系统步骤所示,二者都未提交事务。

    订单系统在此阶段完成:

    1. 订单库开启数据库事务
    2. 在「订单优惠券表」写入订单的优惠券记录
    3. 在「订单表」写入订单数据
    4. 向协调者发送准备成功

    促销系统在此阶段完成:

    1. 开始数据库事务
    2. 更新优惠券状态
    3. 向协调者发送准备成功
    提交阶段

    协调者在收到两个系统发来的「准备成功」响应后,开始第二阶段。

    等两个系统都准备好了之后,进入提交阶段。提交阶段就比较简单了,协调者再给这两个系统发送「提交」命令,每个系统提交自己的数据库事务,然后给协调者返回「提交成功」响应,协调者收到所有响应之后,给客户端返回成功响应,整个分布式事务就结束了。以下是这个过程的时序图:
    在这里插入图片描述

    异常情况处理

    先说准备阶段:如果任何一步出现错误或者超时,协调者就会给两个系统发送「回滚事务」的请求,两个系统收到请求后,回滚各自的数据库事务。
    在这里插入图片描述

    再说提交阶段:准备阶段成功,整个分布式事务只能成功,不能失败。

    网络传输失败:反复重试,直到提交成功;
    两个数据库宕机,服务节点宕机,订单提交,促销库宕机自动回滚,数据不一致:提交非常简单,非常快,这种情况概率非常小。

    实现

    没必要单独启动一个事务协调服务,协调服务的工作最好和订单服务和优惠券服务在同一进程,两个好处:

    1. 参与分布式事务的进程更少,故障点更少,稳定性更好
    2. 减少了一些远程调用,性能更好一些
    分析

    优点:强一致性设计,保证原子性和隔离性,要么都成功,要么到失败

    缺点:事务执行过程需要阻塞服务端的线程和数据库会话,并发场景下的性能不会很高,协调者是一个单点,过程中协调者宕机,导致订单库或者促销库的事务会话卡在等待提交阶段,直到事务超市自动回滚。

    本地消息表

    背景:下单做了两件事,第一件是创建一个新订单,关联的商品是购物车中的商品;第二件是清空购物车。
    这也是一个分布式事务问题,创建订单和清空购物车两个操作需要要么都成功要么都失败。但是清空购物车的一致性不如 2PC 中说的扣减优惠券那么高,创建订单后晚几秒删除购物车的商品也是可以接受的,只要经过小的延迟时间后,最终订单数据和购物车的数据一致就行。

    设计思路

    订单服务收到下单请求后,正常使用订单库的事务更新订单数据,并且执行更新数据库事务过程中,在本地记录一条日志,记录「清空购物车」操作。由于是在本地记录,就是一个普通的单机事务,所以完成这一步就可以给客户端返回成功响应。

    然后再开一个一步的服务,去读取刚才所说的「清空购物车」的日志,调用购物车的服务清空购物车。清空之后,将刚才的日志状态更新为已完成。如果清空失败,可以通过重试解决,最终保持订单系统和购物车系统数据一致。

    分析

    保证 ACID 中的持久性,对于 ACI 都比较差,优点是实现简单。

    使用前提:异步执行的操作,不能有依赖的资源。比如说下单的时候还要锁定库存。如果使用异步进行锁定库存的操作,可能会出现缺货锁定失败的情况。

    商品搜索系统

    用 SQL 的 like 也能实现搜索,但是对于全文匹配来说,数据库的索引几乎派不上用场,只能全表扫描,非常慢。一般用 ES(Elasticsearch) 来实现搜索。

    倒排索引机制

    通过 ES 搜索东西快的原因之一是采用了倒排索引机制。
    在这里插入图片描述
    在 MySQL 中存储的结构可能是上图左侧,DOCID 是 key。

    但是在 ES 中,将单词作为了索引的 key,每个 key 的值是一个列表,记录了包含单词的商品记录的 DOCID。当我们往 ES 里写入记录的时候,ES 会先对需要搜索的字段进行 分词。然后按照单词给商品记录做索引。

    概念对照

    在这里插入图片描述

    MySQL HA

    可以通过 mysqldump 命令来全量备份,使用 binlog 来增量备份,相关文档:MySQL:Backup and Recovery

    主从

    为了应对单机 MySQL 实例宕机后实例恢复可能会很耗时的问题,我们可以通过增加台备用的数据库,让其数据和主库一样。具体配置方法:MySQL Replication

    主库数据更新时,主从两个库的更新顺序:

    1. 在主库的磁盘上写入 binlog;
    2. 主库更新存储引擎中的数据;
    3. 给客户端返回成功响应;
    4. 主库 binlog 复制到从库;
    5. 从库回放 binlog,更新存储引擎中的数据。

    主从延迟:由于从库是通过读取主库的 binlog 来进行数据同步的,所以从库上的数据相对于主库来说是稍微旧一点的。

    解决方案:可以牺牲性能,开始 MySQL 的 同步复制,开启后,MySQL 主库会等待数据成功复制到从库后再给客户端返回响应。这种情况下,如果从库宕机,主库会一直等待才哭,主库也卡死了。

    在这里插入图片描述

    关联阅读

    后端存储实战课——高速增长篇

    后端存储实战课——海量数据篇

  • 相关阅读:
    pytorch安装(windows)
    ChatGLM3 本地部署的解决方案
    逆波兰算法、中缀表达式转后缀表达式
    P1068 [NOIP2009 普及组] 分数线划定
    微信支付 APP端 后端 第二弹 微信回调
    python的opencv操作记录(九)——图像清晰度计算
    CAS虚拟化平台Linux虚拟机安装vGPU显卡驱动并获取许可
    力扣二叉树调试工具类——根据力扣数组输入形式的二叉树构造真正的二叉树
    深入了解 Postman 中的变量
    【大学英语视听说上】Topic Presentation
  • 原文地址:https://blog.csdn.net/MrBaymax/article/details/127877796