《后端存储实战》笔记

https://time.geekbang.org/column/intro/100046801

以下是关于这门课程的笔记,有一部分图来源于这门课,侵删哈

一、创业篇

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

创建&更新订单:
创建——前端请求id存redis,后端生成业务id,用作请求和数据的幂等

更新——乐观锁

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

img

图中实线表示每访问一次商详页,需要真正传输的数据,虚线表示当商详页数据发生变化的时候才需要进行一次数据传输。用户打开一个 SKU 的商详页时,首先去 CDN 获取商详页的 HTML,然后访问商品系统获取价格等频繁变化的信息,这些信息从 Redis 缓存中获取。图片和视频信息,也是从对象存储的 CDN 中获取。分析一下效果,数据量最大的图片、视频和商品介绍都是从离用户最近的 CDN 服务商获取的,速度快,节约带宽。真正打到商品系统的请求,就是价格这些需要动态获取的商品信息,一般做一次 Redis 查询就可以了,基本不会有流量打到 MySQL 中。

ps:CDN的加速原理是什么?

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

暂存购物车:Cookie|LocalStorage

用户购物车:Redis+MySql

自己的想法:读写都在Redis,异步回写到MySql

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

https://time.geekbang.org/column/article/206544

img

可重复读:在一个事务执行过程中,它能不能读到其他已提交事务对数据的更新,如果能读到数据变化,就是“不可重复读”,否则就是“可重复读”。大厂采用的好像都是RC隔离级别。

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

2PC、3PC、TCC、本地事务表、消息事务

面试必问:分布式事务六种解决方案

2PC:

img

3PC:

img

TCC:

img

本地事务表:

本地消息表的实现思路是这样的,订单服务在收到下单请求后,正常使用订单库的事务去更新订单的数据,并且,在执行这个数据库事务过程中,在本地记录一条消息。这个消息就是一个日志,内容就是“清空购物车”这个操作。因为这个日志是记录在本地的,这里面没有分布式的问题,那这就是一个普通的单机事务,那我们就可以让订单库的事务,来保证记录本地消息和订单库的一致性。完成这一步之后,就可以给客户端返回成功响应了。然后,我们再用一个异步的服务,去读取刚刚记录的清空购物车的本地消息,调用购物车系统的服务清空购物车。购物车清空之后,把本地消息的状态更新成已完成就可以了。异步清空购物车这个过程中,如果操作失败了,可以通过重试来解决。最终,可以保证订单系统和购物车系统它们的数据是一致的。

消息事务:

img

6. 如何用Elasticsearch构建商品搜索系统?

这里简单理解一下ES,后续还需深入学习一下.....

ES为什么能快速搜索?

ES 本质上是一个支持全文搜索的分布式内存数据库,特别适合用于构建搜索系统。ES 之所以能有非常好的全文搜索性能,最重要的原因就是采用了倒排索引。倒排索引是一种特别为搜索而设计的索引结构,倒排索引先对需要索引的字段进行分词,然后以分词为索引组成一个查找树,这样就把一个全文匹配的查找转换成了对树的查找,这是倒排索引能够快速进行搜索的根本原因。但是,倒排索引相比于一般数据库采用的 B 树索引,它的写入和更新性能都比较差,因此倒排索引也只是适合全文搜索,不适合更新频繁的交易类数据。

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

-- 全量备份数据
mysqldump -uroot -p test > test.sql

-- 全量导入备份数据
mysql -uroot test < test.sql

-- 使用binlog进行备份和恢复增量数据
mysql> show variables like '%log_bin%';
+---------------------------------+-----------------------------------+
| Variable_name                   | Value                             |
+---------------------------------+-----------------------------------+
| log_bin                         | ON                                |
| log_bin_basename                | /usr/local/var/mysql/binlog       |
+---------------------------------+-----------------------------------+
mysql> show master status;
+---------------+----------+--------------+------------------+-------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+-------------------+
| binlog.000001 |    18745 |              |                  |                   |
+---------------+----------+--------------+------------------+-------------------+

MySql同步复制:

开启同步复制时,MySQL 主库会等待数据成功复制到从库之后,再给客户端返回响应。

但是如果一主一从,从库宕机了,就无法返回,主库会被阻塞,所以可以采用一主二从的架构

img

cache aside 为什么出现数据不一致的可能性低?

缓存更新的套路

二、高速增长篇

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

MySql中的LIKE '12345%'和LEFT(XXX,5)='12345'为什么前者走索引,后者不走索引?

首先LEFT是函数计算,至少得遍历所有数据一遍,否则不知道到底有哪些数据符合要求。这个问题在于使用了函数,但由此我也想搞明白一条sql如何执行的,为什么同一个语意,MySql却做了不同的操作?

一条sql语句究竟是如何执行的

上面这篇文章把这个流程大致讲了一遍,我复述一下:

首先MySql分为3层,客户端,服务层,存储层

服务层里有连接器、分析器、优化器、执行器

  1. 连接器负责接受客户端的TCP/IP连接
  2. 分析器将sql文本转化为AST(语法树)
  3. 优化器负责优化sql,同时规定执行计划
  4. 执行器调用对应的存储引擎执行优化器优化后的计划

存储层则负责MySql中数据的存储和提取。

一条sql语句究竟是如何执行的

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

读写分离的3种办法:

  1. 纯手工方式:修改应用程序的 DAO 层代码,定义读写两个数据源,指定每一个数据库请求的数据源
  2. 组件方式:也可以使用像 Sharding-JDBC 这种集成在应用中的第三方组件来实现,这些组件集成在你的应用程序内,代理应用程序的所有数据库请求,自动把请求路由到对应数据库实例上
  3. 代理方式:在应用程序和数据库实例之间部署一组数据库代理实例,比如说 Atlas 或者 MaxScale。对应用程序来说,数据库代理把自己伪装成一个单节点的 MySQL 实例,应用程序的所有数据库请求被发送给代理,代理分离读写请求,然后转发给对应的数据库实例

TODO 有时间了解一下HAProxy+Keepalived???可以做无感知扩展MySql从库

读写分离会带来主从延迟的问题,但这个问题没有很好的技术解决方案,只能从业务流程上规避,一个典型的例子:
大的电商,它支付完成后是不会自动跳回到订单页的,它增加了一个无关紧要的“支付完成”页面,其实这个页面没有任何有效的信息,就是告诉你支付成功,然后再放一些广告什么的。你如果想再看刚刚支付完成的订单,需要手动点一下,这样就很好地规避了主从同步延迟的问题。
一个好的建议是:重新设计业务逻辑,尽量规避更新数据后立即去从库查询刚刚更新的数据

3. MySQL主从数据库同步是如何实现的?

下图是异步复制的流程图

img

同步复制和异步复制的区别就在于同步复制会等待所有从库都返回成功再继续下一步操作

MySql从5.7版本开始,增加了半同步复制的方式,半同步复制介于同步和异步之间,事务线程不用等着所有的复制成功响应,只要一部分复制响应回来之后,就可以给客户端返回了

配置半同步的时候,需要注意这些参数:

  1. rpl_semi_sync_master_wait_slave_count「至少等待数据复制到几个从节点再返回」
  2. rpl_semi_sync_master_wait_point「控制主库执行事务的线程,是在提交事务之前(AFTER_SYNC)等待复制,还是在提交事务之后(AFTER_COMMIT)等待复制,默认是 AFTER_SYNC」

半同步的自动降级策略:主库提交事务的线程等待复制的时间超时了,这种情况下事务也会正常提交,并且MySql被降级到异步复制,直到有足够的从库追上主库,才能恢复成半同步复制。如果这个期间主库宕机了,仍然有丢失数据的风险

4. 订单数据越来越多,数据库越来越慢该怎么办?

解决海量数据导致存储查询满的方案:分片

  1. 存档历史订单数据提升查询性能
  2. 支持停服的话可以重建一张新订单表,然后把数据都导过去

存档历史数据时会碰到一个问题,如何大批量删除MySql数据?

这里先给出结论,尝试将查询条件转化为id范围查询,再按照id排序一遍

  1. MySql的存储引擎一般选用InnoDB,InnoDB的底层是B+树,B+树本身有序,所以按id查找起来非常快,但这一定要id与时间正相关
  2. 至于为什么要order的原因则是InnoDB底层对数据有分页,连续的id大概率被存放在同一页,所以order一下,就可以减少对其他页的访问

我们delete之后,MySql数据所占用的空间并不会减少,因为MySql执行的是逻辑删除,而非物理删除,MySql的数据存储也是二进制存储,如果物理删除一段,就会像数组的删除一样,把后面的数据前移,所以MySql选择了逻辑删除,数据还存在那里,等待新数据的覆盖,如果非要把这部分数据删除,可以使用OPTIMIZE TABLE,把这个表重建一遍,执行过程中会一直锁表。另外这个优化也有条件,MySQL 的配置必须是每个表独立一个表空间(innodb_file_per_table = ON),如果所有表都是放在一起的,执行 OPTIMIZE TABLE 也不会释放磁盘空间。

三、海量数据篇

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

  1. 分库分表分片的区别?

    • 我的理解是这样的,分片是分库分表之后数据的最小单位,他们可能分布于不同的表或库中,而分片按范围聚合之后就是分区。分库、分表则是字面意思。
  2. 分库分表的依据?

    • 数据量大,分表;并发量大,分库
  3. 分片算法?

    • 范围分片,对查询友好,适合并发量不大的场景,缺点是会产生热点数据
    • 哈希分片,分配的会比较均匀
    • 查表法,灵活但多了一次查表,查询性能一般
  4. 例子

    • 如何对订单表分库分表?分片算法选择哈希分片!!!按用户id分,那用订单id就无法查了;按订单id分,但订单按用户id聚合;所以一个折中的方案是按用户id分,约定订单id的后几位是用户id,这样就可以通过订单id查出用户id,再去找对应的shared key了

2. 大厂都是怎么做MySQL to Redis同步的?

在处理超大规模并发的场景时,由于并发请求的数量非常大,即使少量的缓存穿透,也有可能打死数据库引发雪崩效应。对于这种情况,我们可以缓存全量数据来彻底避免缓存穿透问题

对于缓存数据更新的方法,可以订阅数据更新的 MQ 消息来异步更新缓存,更通用的方法是,把缓存更新服务伪装成一个 MySQL 的从节点,订阅 MySQL 的 Binlog,通过 Binlog 来更新 Redis 缓存

有很多开源的项目就提供了订阅和解析 MySQL Binlog 的功能,例如Canal。Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用,如下图所示:

img

需要特别注意的是,无论是用 MQ 还是 Canal 来异步更新缓存,对整个更新服务的数据可靠性和实时性要求都比较高,数据丢失或者更新慢了,都会造成 Redis 中的数据与 MySQL 中数据不同步。在把这套方案应用到生产环境中去的时候,需要考虑一旦出现不同步问题时的降级或补偿方案,例如通过定时任务去对账

3. 分布式存储:你知道对象存储是如何保存图片文件的吗?

对象存储是最简单的分布式存储系统,主要由数据节点集群、元数据集群和网关集群(或者客户端)三部分构成。数据节点集群负责保存对象数据,元数据集群负责保存集群的元数据,网关集群和客户端对外提供简单的访问 API,对内访问元数据和数据节点读写数据。

img

img

对象的存储一般不记录MySQL这样的Binlog日志。主从复制的时候复制的是整块的数据,而不是Binlog,这么做的原因如下:

  1. 第一个原因是基于性能的考虑。我们知道操作日志里面,实际上就包含着数据。在更新数据的时候,先记录操作日志,再更新存储引擎中的数据,相当于在磁盘上串行写了 2 次数据。对于像数据库这种,每次更新的数据都很少的存储系统,这个开销是可以接受的。但是对于对象存储来说,它每次写入的块儿很大,两次磁盘 IO 的开销就有些不太值得了。
  2. 第二个原因是它的存储结构简单,即使没有日志,只要按照顺序,整块儿的复制数据,仍然可以保证主从副本的数据一致性。

4. 跨系统实时同步数据,分布式事务是唯一的解决方案吗?

可以采用canal同步binlog,再通过MQ发出,解耦上下游

因果一致性:有因果关系的数据之间必须要严格地保证顺序,没有因果关系的数据之间的顺序是无所谓的

Canal 自带的分区策略就支持按照指定的 Key,把 Binlog 哈希到下游的 MQ 中去,从而保证因果一致性

5. 如何在不停机的情况下,安全地更换数据库?

双写新旧DB

  1. 支持双写新旧两个库,并且预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步双写。
  2. 支持读新旧两个库,同样预留热切换开关,控制读旧库还是新库。

6. 类似“点击流”这样的海量数据应该如何存储?

“点击流”数据:页面访问量、用户停留在页面的时间等等

方法一:Kafka存储数据,但受限于单节点的存储能力,Kafka实际能存储的量并不是无限的

方法二:针对这个场景,可以选用HDFS,HDFS提供更加灵活的查询能力,而且可以真正做到无限存储容量(不够的时候水平扩展就可以了),但HDFS的吞吐量不如Kafka

展望未来:分布式流数据存储、时序数据库

四、总结

「后端存储实战」这一个系列的文章对我这种刚入门的小白还是很友好的,他主要讲解了后端开发过程中碰到的一些常见问题的方法论,虽然都是浅尝则止,但也为我打开了一扇继续深入学习的大门,也深深的感觉到了自己的无知

以前总觉得要把ES、NewSQL、Hadoop、ClickHouse.....这些框架都给学习一边,但在看完这个课程后发现其实并不用这样,框架是学不完的,我需要做的是透过框架看底层逻辑,把框架分类,哪些是属于一套的,这套框架的核心逻辑是什么?他们有没有一套通用的方法论?这些都是我在学习的过程中需要去理解和掌握的。

虽然明白了这个道理,但实践起来还是有那么点困难,emmmmmm,任重而道远吧,加油!!!

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×