后端存储实战-极客时间

电商系统是如何设计的

  • 不要一上来就设计功能

  • 这个系统是给谁用的?

  • 这些人用来该系统解决什么问题?

  • 电商:用户、运营、报表

  • 购物流程:浏览商品-----加购------下单-------支付-------发货-------收货

  • 复杂的促销模块要封装起来

  • 一般来说,解决超卖问题的方法都是在下单的时候锁定库存,如果订单取消再释放库存。

01 | 创建和更新订单时,如何保证数据准确无误?

  • 订单系统很重要,从下单开始、支付、发货、收货都需要更新订单
  • 订单系统的数据表:订单主表、订单商品表、订单支付表、订单优惠表
  • 订单重复请求问题:订单服务的幂等性
  • 可以利用数据库id不能重复的特性,在更新数据的时候带上id
  • 有个生成订单号的服务,每次创建订单都会带上订单号,订单号是唯一约束
  • 数据库的update本身就有幂等性
  • ABA问题:因为操作失败导致重试,A发了两次

在这里插入图片描述

  • 解决方法:带上版本号
  • 幂等性可以防止很多异常问题

02 | 流量大、数据多的商品详情页系统该如何设计?

  • 商祥页的流量并发很大
  • 商祥页数量多重量大
  • 国内的一线电商的SKU(库存单元)数量在亿级别,内容还多

在这里插入图片描述

  • 商品系统需要存储的内容
  • 缓存数据:
  • 读:先查询缓存,有就返回没有就查询数据库,如果有写到缓存中返回前端。
  • 写:更新数据库,删缓存,最后返回。称为cache aside
  • 读:先查询缓存,有就返回,没有查询数据库,再写到缓存
  • 写:先写数据库,在更新缓存,先尝试读缓存,如果没命中返回更新成功,如果命中,则更新缓存再返回成功。称为read/write throuth
  • 还有read、write throuth和write behind
  • 设计商品表要保留数据的每个历史版本,历史版本的数据保存一个 快照保存在mysql中
  • 因为不同的商品信息不一样,不能每种产品都建一个表,所以需要mongodb
  • mongodb表不用事先定义可以保存不同结构的数据
  • 不需要事务和多表连查,可以用mongodb

在这里插入图片描述

  • mysql也支持json数据的格式

03 | 复杂而又重要的购物车系统,应该如何设计?

  • 购物车可以在localStore存储
  • clone 项目git clone https://github.com/link1st/go-stress-testing.git# 进入项目目录cd go-stress-testing# 运行go run main.go -c 1 -n 100 -u https://www.baidu.com/

  • 读多写少用缓存,写多读少用MQ
  • 两份数据经常写就会有不一致的风险

04 | 事务:账户余额总是对不上账,怎么办?

  • 账户系统的设计

在这里插入图片描述

  • 账户系统不是独立的,和订单,交易等系统需要吻合
  • 但是业务的复杂性现实情况是很少有系统账户能做到一点不差
  • 可能是网络请求错误,服务器宕机,系统bug等
  • 对不上帐的通用问题是:冗余数据的一致性
  • 账户系统的数可以通过别的系统来计算,之所以单独存储是空间换时间的思想
  • 用本地数据库事务来做
  • 事务的原子性和持久性是必须保证的,隔离性和一致性可以牺牲
  • RC和RR的区别:其他事务对数据的更新是否可见
  • RR级别下加的是next-key锁,可以解决幻读的吧?

05 | 分布式事务:如何保证多个系统间的数据是一致的?

  • 分布式事务:2PC 3PC TCC 本地消息等
  • 2PC和本地消息常用:订单和优惠券系统

2PC引入事务协调者角色,协调者对客户端提供完整的“使用优惠券下单的服务”,在服务内部再分别调用订单和促销相应的服务

  • 两阶段提交指准备阶段和提交阶段,在准备阶段,协调者分别给订单系统和促销系统发送prepare命令,收到命令之后开始执行准备操作(除了提交数据库事务之外的所有事)

  • 在订单库开启一个数据库事务,在优惠券表写入优惠券记录,在订单表写入订单数据

  • 暂时不要提交事务,给协调者返回准备成功,协调者收到准备成功开始提交,协调者发送提交命令,然后接受成功的响应

在这里插入图片描述

  • 异常情况:在准备阶段出现错误会回滚
    在这里插入图片描述

  • 准备阶段成功进入提交阶段:当提交阶段发生故障需要反复重试,如果发生宕机还是会出现数据不一致的情况,但是因为简化的过程,提交的过程特别快,所以出现的概率还是很小的

  • 实现2pc没有必要单独起一个事务协调服务,最好是放在和其中一个事务同一个进程中:好处故障点减少、减少了远程调用。

  • 2pc是强一致性的设计,隔离性和一致性

  • 2pc适合要求数据一致性比较高的场景

  • 2pc的缺陷:阻塞操作,性能不好,协调者是单点,会卡服务

  • 只有在强一致和并发不大的场景下使用

  • 通常使用最终一致性还是比较好的:本地消息表

场景2:清空购物车和创建订单,因为业务不像优惠券的需求那么高,可以延迟几秒,最终一致性就可以
本地消息表就是在本地事务后再记录一条消息,另一个事务去获取消息然后执行事务,如果成功更新消息状态,失败就重试,消息可以存在数据库中或者存在磁盘上都是可以的,放在订单库中更加简单

  • RocketMQ提供的事务消息就是本地消息表的思想,可以考虑使用RocketMQ
  • 本地消息表这种方式只能满足持久性、其他三个特性都比较差,优点:实现简单、最终一致性
  • 但是有前提条件:异步执行的操作不依赖资源,比如库存还要判断是否有库存,如果因为缺货导致事务失败就不好处理了
  • 分布式事务的思想:本地事务+分布式一致性

06 | 如何用Elasticsearch构建商品搜索系统?

  • 搜索的核心是全文匹配,对于全文匹配数据库索引是派不上用场的,只能全表扫描
  • DOCID类似与数据库的主键
  • 倒排索引:以单词为索引的key,值是一个列表,列表的元素是DOCID

在这里插入图片描述

  • ES先会做分词,然后按照按照单词给商品做索引
  • 会根据匹配度进行排序,查找苹果手机是做了两次查找,没有做任何模糊匹配
  • 物理上的存储和MySQL的Innodb索引差不多都是一颗查找树

在这里插入图片描述

  • 对比mongodb
  • index:document表
  • document:collection行
  • field:field列
  • mapping:表结构
  • ES支持中文分词:IK Analysis for Elasticsearch
  • 创建ES先定义mapping

在这里插入图片描述

  • ES提供了标准的restful接口不需要客户端,通过curl就可以操作es

在这里插入图片描述

  • 使用put方法可以创建一个index
  • 指定title的插件ik作为分词器
  • ES就是支持全文索引的分布式内存数据库
  • 倒排索引就是先对索引字段进行分词,分词组成了一个查找树,将全文匹配的查找转化成对树的查找
  • 但是更新和插入的性能比较差,只能做全文搜索
  • 搜索提示的功能是怎么实现的:

因为用户每输入一个字都可能会发请求查询搜索框中的搜索推荐。所以搜索推荐的请求量远高于搜索框中的搜索。es针对这种情况提供了suggestion api,并提供的专门的数据结构应对搜索推荐,性能高于match,但它应用起来也有局限性,就是只能做前缀匹配。再结合pinyin分词器可以做到输入拼音字母就提示中文。如果想做非前缀匹配,可以考虑Ngram。不过Ngram有些复杂,需要开发者自定义分析器。

07|MySQL HA:如何将“删库跑路”的损失降到最低?

  • mysqldump可以用来做全量备份
mysqldump -uroot  -p test >test.sql
mysql -uroot  test < test.sql
  • 全量备份会导致锁表,开销也比较大,不要经常做全量备份
  • 全量+增量备份:binlog
  • 开启binlog后MySQL的每一次更新操作都会被记录到binlog里
  • 还可通过binlog回滚执行相反的操作
  • MySQL通过配置文件开启binlog并指定文件
  • 查询数据库binlog是否开启:
show variables like '%log_bin%';
  • 定期的全量备份+binlog

  • 回放binlog可以时间稍微提前一点,因为回放binlog的操作是具有幂等性的

  • mysqlbinlog --start-datetime “2020-02-20 00:00:00” --stop-datetime “2020-02-20 12:50:14”

  • 实时的主从binlog就是HA的方式

  • 主从两个数据库更新数据实际的时序是这样的:

  1. 在主库的磁盘上写入 Binlog;
  2. 主库更新存储引擎中的数据;
  3. 给客户端返回成功响应;
  4. 主库把 Binlog 复制到从库;
  5. 从库回放 Binlog,更新存储引擎中的数据。
  • 也就是说,从库的数据是有可能比主库上的数据旧一些的,这个主从之间复制数据的延迟,
    称为“主从延迟”。正常情况下,主从延迟基本都是毫秒级别,你可以认为主从就是实时保
    持同步的。麻烦的是不正常的情况,一旦主库或者从库繁忙的时候,有可能会出现明显的主
    从延迟。
  • 而很多情况下,数据库都不是突然宕机的,而是先繁忙,性能下降,最终宕机。这种情况
    下,很有可能主从延迟很大,如果我们把业务直接切到从库上继续读写,主从延迟这部分数
    据就丢了,并且这个数据丢失是不可逆的。即使事后你找回了当时主库的 Binlog 也是没法
    做到自动恢复的,因为它和从库的数据是冲突的。
  • 简单地说,如果主库宕机并且主从存在延迟的情况下,切换到从库继续读写,可以保证业务
    的可用性,但是主从延迟这部分数据就丢失了。
  • 这个时候你就需要做一个选择题了,第一个选项是,保证不丢数据,牺牲可用性,暂时停止
    服务,想办法把主库的 Binlog 恢复到从库上之后再提供服务。第二个选项就是,冒着丢一
    些数据的风险,保证可用性,第一时间切换到从库继续提供服务。
  • 那能不能既保证数据不丢,还能做到高可用呢?也是可以的,那你就要牺牲一些性能。
    MySQL 也支持同步复制,开启同步复制时,MySQL 主库会等待数据成功复制到从库之
    后,再给客户端返回响应。
  • 如果说,牺牲的这点儿性能我不在乎,这个方案是不是就完美了呢?也不是,新的问题又来
    了!你想一下,这种情况下从库宕机了怎么办?本来从库宕机对主库是完全没影响的,因为
    现在主库要等待从库写入成功再返回,从库宕机,主库就会一直等待从库,主库也卡死了。
  • 这个问题也有解决办法,那就是再加一个从库,把主库配置成:成功复制到任意一个从库就
    返回,只要有一个从库还活着,就不会影响主库写入数据,这样就解决了从库宕机阻塞主库
    的问题。如果主库发生宕机,在两个从库中,至少有一个从库中的数据是和主库完全一样
    的,可以把这个库作为新的主库,继续提供服务。为此你需要付出的代价是,你要至少用三
    台数据库服务器,并且这三台服务器提供的服务性能,还不如一台服务器高。

在这里插入图片描述

08 | 一个几乎每个系统必踩的坑儿:访问数据库超时

  • 可以做一个降级页面
  • Nginx策略,超时使用静态页
  • 根据CPU的曲线变化判断定时任务

09 | 怎么能避免写出慢SQL?

  • MySQL数据量不要大于1000w
  • type:all全表扫描 range:使用索引并在范围中查找,index是直接命中索引
  • 遍历100w是安全的,1000w以上就会有瓶颈
  • where用到了函数,优化器不会使用索引

10 | 走进黑盒:SQL是如何在数据库中执行的?

在这里插入图片描述

  • 存储引擎负责将逻辑的表行列转化成物理的数据结构
  • 不同的数据库物理存储结构是完全不一样的
  • innodb是以主键为关键字的B+tree,每行数据存在主键那个b+tree的叶子节点上
  • 索引也是通过b+tree存储的,区别是叶子节点保存的不是数据而是主键ID
  • 优化后的逻辑执行计划会转化成物理执行计划

11 | MySQL如何应对高并发(一):使用缓存保护MySQL

  • redis的list双向链表进行了优化

  • read/write through:读请求先读缓存,写请求先写数据库
    在这里插入图片描述

  • 读:先查询缓存,有就返回没有就查询数据库,如果有写到缓存中返回前端。

  • 写:更新数据库,删缓存,最后返回。称为cache aside

  • 读:先查询缓存,有就返回,没有查询数据库,再写到缓存

  • 写:先写数据库,在更新缓存,先尝试读缓存,如果没命中返回更新成功,如果命中,则更新缓存再返回成功。称为read/write throuth

  • 但是并发场景会有脏数据的可能,cache aside会解决并发读写脏数据的问题

  • 缓存命中率低会导致缓存穿透,少量的缓存穿透是可以的,需要预防的是短时间大量请求都穿透

  • 当系统初始化或系统刚刚上线,缓存是空的,大量的请求打上来会导致缓存穿透而雪崩,可以灰度发布少量的请求打上来,启动时进行缓存预热,将经常访问的数据放在缓存中

  • 当发生缓存穿透的时候如果从数据库读取的数据时间比较长也会引起数据库的雪崩

  • 复杂的SQL执行时间比较长,因为长时间不能返回写到缓存中,这段时间内大量的请求进来会都穿透而恶性循环从而雪崩

  • 所以构建数据查询时间太长或者并发量太大都会造成缓存穿透,只读缓存不读数据库,用后台线程定时更新缓存数据,和redolog一样的思想

  • 这些基础的策略其实早就写在几十年前的书中了

  • 数据加版本号,写库时自动增一。更新缓存时,只允许高版本数据覆盖低版本数据。

  • Cache Aside 模式在下面的场景下:

  • 读写线程之间在执行 Cache Aside Pattern 操作的时候,写线程删除了缓存,读线程从 DB 读到老的数据,把老的数据放到了缓存中,这样就会在缓存中产生脏数据。

12 | MySQL如何应对高并发(二):读写分离

  • 当个性化的不共享数据不要用缓存,因为每个人看到的都是不一样的嘛
  • 订单用户购物车使用缓存的效果就没那么好了
  • 因为互联网的系统读请求远大于写请求,所以只要加数据库就可以实现,只需要保证实时同步就行
  • 读写分离的主从数据不一致的情况,没有好的方法解决
  • 使用读写分离组件来分离读写请求,在设计更新数据之后不要去立刻查询更新的数据
  • 写后读,走主库

13 | MySQL主从数据库同步是如何实现的?

  • 客户端提交一个事务到MySQL集群中,经过的过程:主库提交事务,更新存储引擎中的数据,把Binlog写到磁盘,给客户端返回响应,把binlog复制到所有从库,从库将binlog写入暂存区(relaylog),回放binlog、更新存储引擎中的数据、给主库返回成功响应

在这里插入图片描述

  • 异步复制和同步复制区别是什么时间给客户端返回响应,同步复制是不能用的
  • 一主两从集群:是半同步复制,只要有一个数据库复制成功就返回

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 无论技术发展的多快,这些理论基础都是不变的

在这里插入图片描述

  • 异步复制、同步复制、半同步复制

在这里插入图片描述

14 | 订单数据越来越多,数据库越来越慢该怎么办?

  • 解决海量数据的问题核心的思路就是分片
  • 当订单表数据太多时首选方案不是分库分表而是将历史订单分开
  • 从库的历史订单写到主库的历史订单表中去
  • 订单表和历史订单表中都有历史订单中的数据,不应该着急删除订单表中的历史数据,应该上线测试,如果没有bug就可以
  • 然后还要有个小脚本定时任务去定期将订单表的数据写到历史订单表,与订单相关的子表通过订单的ID外键关联订单表可以随着订单一起归档数据就可以了
  • 要写定时任务,闲时迁移
  • 如何批量删除订单表的数据?删除的数据量太大需要分批删除

在这里插入图片描述

在这里插入图片描述

  • 但是批量删除数据库的空间还不会减小,只是标记为这块空间可以复用,如果想释放空间可以OPTIMIZE TABLE但是执行的过程会锁表,导致业务的暂时不可用,还可以通过临时文件再改名的方式复制过去

在这里插入图片描述

15 | MySQL存储海量数据的最后一招:分库分表

  • MySQL本来是不适合TB以上级别的,但是又不能舍弃MySQL,因为事务能提供金融级别的保证
  • 到底是分库还是分表?数据越散维护就越困难
  • 分库和分表解决的是数据量太大查询慢的问题,查询是指事务中的查询和更新操作,因为只读查询是可以通过缓存和分库分表解决的
  • 数据量大就分表,并发高就分库
  • 分表是解决大数量查询慢的问题
  • 如何选择Sharding key?
  • 分片算法:解决热点问题,要尽量均匀
  • 范围分片和hash分片,取模的算法、一致性hash算法,就是划分边界要尽量均匀
  • 分多少库是用并发量评估,分多少表是按数据量评估
  • 问题:关联的表怎么做分片处理?

16 | 用Redis构建缓存集群的最佳实践有哪些?

  • redis cluster可以支持更大的并发,高可用
  • 通过槽来进行分片,每个集群的槽数是固定的16384,每个key落在槽中的位置也是固定的
  • 通过取余的方式,这些槽如何存放到具体的redis的节点中?查表法
  • 客户端可以连接任意一个节点访问数据,当客户端请求一个Key时通过公式计算key在哪个槽中,然后查找槽和节点的映射关系,最后找到真正的节点,如果在就返回,不在就会重定向
  • 当集群中增加了节点可以通过官方提供的脚本自动分配槽
  • 分片不能解决高可用的问题
  • 要增加从节点做主从复制,replication
  • 因为redis不支持事务索引会比较简单
  • redis不需要做读写分离默认都是主节点,从节点只是做高可用

在这里插入图片描述

  • redis cluster优点是易于使用适合中小规模redis集群10-几十个节点
  • 但是不适合构建超大规模的集群,因为采用了去中心化的设计,因为每个节点都保存了所有槽和节点的映射关系,可以访问任意一个节点再重定向
  • 但是更新的操作需要广播配置变化:Gossip协议
  • 数据量过大还是中央集权的效率更高
  • codis架构

在这里插入图片描述

  • 加入代理增加了调用链路。引入单点,或者在客户端中d代理
  • 为什么大数据的生态都是master slave结构
  • 就是路由的配置引入配置中心
  • 但是不支持多keys的插入

17 | 大厂都是怎么做MySQL to Redis同步的?

  • redis缓存全量的数据如何更新

在这里插入图片描述

  • 丢消息了怎么办?可以用消息队列

在这里插入图片描述

  • 异步订阅更新信息
  • 解析binlog的功能canal,实时接受binlog并更新redis

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值