6.软件架构设计:大型网站技术架构与业务架构融合之道 — 数据库


  1. 第6章 数据库
  2. 6.1 范式与反范式
  3. 数据库范式要求:
  4. 第一范式:
  5. 每个字段都是原子的,不能再分解。
  6. 第二范式:
  7. 1.表必有主键,主键可以是单个属性或者几个属性的组合。
  8. 2.非主属性必须完全依赖,而不能部分依赖。
  9. 第三范式:
  10. 没有传递依赖:非主属性必须直接依赖主键,而不能间接依赖主键。
  11. 6.2 分库分表
  12. 6.2.1 为什么要分
  13. 1.分库的目的是要做"业务拆分",通过业务拆分,把一个大的复杂的系统拆成多个业务子系统,之间通过RPC或者消息中间件通信。
  14. 2.应对高并发。但要针对读多写少,还是读少写多的场景分别讨论。如果是读多写少,可以通过增加从库,缓存解决,不一定要分库分表。如果是
  15. 读少写多,或者说写入的QPS已经达到了数据库的瓶颈,这时就要考虑分库分表了。
  16. 3.另外一个角度是"数据隔离"。如果把核心数据和非核心数据业务放在一起,一旦因为非核心数据库宕机,核心业务也会收到影响。
  17. 6.2.2 分布式ID生成服务
  18. 分库之前,数据库的自增ID可以唯一标识一条记录。分表分库之后,需要一个全局的ID生成服务。
  19. 6.2.3 拆分维度的选择
  20. 对于在分库分表之后其他维度的查询,一般有以下几个方法:
  21. 1.建立一个映射表
  22. 建立辅助维度和主维度之间的映射关系(商户ID和用户ID之间的映射关系)。查询的时候根据商户ID查询映射表,得到用户ID;再根据用户
  23. ID查询订单ID。但这里有个问题,映射表本身也需要分库分表,并且分库分表维度和订单的分库维度还不同。即使映射表不分库分表,写入一条
  24. 订单的时候也可能需要同时写两个库,属于分布式事务问题。对于这种问题,通常也只能做一个后台任务定时对比,保证订单表和映射表的数据
  25. 最终一致性。
  26. 2.业务双写
  27. 同一份数据,两套分库分表。一套按用户ID分,一套按商户ID切分。同样,存在写入多个库的分布式事务问题。
  28. 3.异步双写
  29. 还是两套表,只是业务单写。然后通过监听binlog,同步到另外一套表上。
  30. 4.两个维度统一到一个维度
  31. 把订单ID和用户ID统一成一个维度,比如把用户ID作为订单ID中的某几位,这样订单ID中就包含了用户ID的信息,然后按照用户ID分库,
  32. 当按订单ID查询的时候,截取出用户ID,再按用户ID查询;或者订单ID和用户ID中有某几位是相同的,用这几位作为分库维度。
  33. 6.2.4 Join查询问题
  34. 分库分表之后,join 查询就不能用了。解决方法:
  35. 1.把join拆成多个单表查询,不让数据库做join,而是在代码层面对结果进行拼装。
  36. 这种做法比较常见,因为数据库全是单表查询,大大降低了数据库发生慢查询的概率。
  37. 2.做宽表,重写轻读
  38. 很多时候会遇到这个情况:需要把join的结果分页,这需要利用mysql本身的分页功能。对于这种不得不用join的情况,可以另外做一个
  39. join表,提前把结果join好。这就是"重写轻读",其实也就是"空间换时间"的思路。
  40. 3.利用搜索引擎
  41. 利用类似ES的搜索引擎,把数据库中的数据导入搜索引擎中进行查询,从而解决join问题。
  42. 6.2.5 分布式事务
  43. 做了分库之后,纯数据库的事务就做不了了。一般的解决办法是优化业务,避免垮跨库的事务,保证所有事务都落到单库中。如果实在无法避免,就
  44. 需要做分布式事务的解决方案了。
  45. 6.3 B+ 树
  46. 关系型数据库在查询方面有一些重要特性,是kv型的数据库或者缓存所不具备的,比如:
  47. 1.范围查询
  48. 2.前缀匹配模糊查询
  49. 3.排序和分页
  50. 这些特性都得归功于B+树这种数据结构。
  51. 6.3.1 B+ 树逻辑结构
  52. 数据结构关键特征:
  53. 1.在叶子节点一层,所有记录的主键按照从小到大的顺序排列,并且形成了一个双向链表,叶子节点的每一个key指向一个记录。
  54. 2.非叶子节点取的是叶子节点里面key最小的值。这意味着所有非叶子节点的key都是冗余的节点。同一层的非叶子节点也互相串联,形成了一个
  55. 双向链表。
  56. 基于这样一个数据结构,要实现上面的几个特性就很容易了:
  57. 1.范围查询:比如要查主键在[1,17]之间的记录。二次查询,先查找1所在的叶子节点的记录位置,再查找17所在的叶子节点记录的位子,然后
  58. 顺序的遍历。
  59. 2.前缀匹配模糊查询:假设主键是字符串类型,要查询 where Key like abc%,其实可以转换成一个范围查询 Key in [abc,abcz]。
  60. 当然,如果是后缀匹配模糊查询,或者诸如 where Key like %abc%这样的中间匹配,则没办法转换成范围查询,只能挨个遍历。
  61. 3.排序与分页。叶子节点是天然排序好的,支持排序和分页。
  62. 另外,基于 B+ 树的特性,会发现对于offset这种特性,其实是用不到索引的。比如每页显示10条,要展示第101页,通常会写成 select xxx
  63. where xxx limit 1000, 10,从offset=1000的位置开始取10条。
  64. 虽然只取了10条数据,但实际上数据库要把前面的1000条都遍历了才知道offset=1000的位置在哪里。对于这种情况,合理的办法是不要用
  65. offset,而是把offset=1000的位置换算出某个 max_id,然后用where语句实现,如 select xxx where xxx and id > max_id limit 10,
  66. 这样就可以利用 B+ 树的特性,快速定位 max_id的位置,即使 offset=1000的位置。
  67. 6.3.2 B+ 树物理结构
  68. 对于磁盘来说,不可能一条条的读写,而都是以"块"为单位进行读写的。innodb默认定义的块大小是16kb,通过innodb_page_size参数指定。
  69. 这里说的"块",是一个逻辑单位,而不是指磁盘扇区的物理块。块是通过 innodb 读写磁盘的基本单位,innodb每一次磁盘IO,读取的都是16kb的
  70. 整数倍数据。无论是叶子节点,还是非叶子节点,都会装在page里。innodb为每个page赋予一个全局32位的编号,所以innodb的存储容量上限是
  71. 64TB(2^32 * 16kb)。
  72. 16kb是一个什么概念呢果用来装非叶子节点的话,一个page大概可以装1000个key(16kb,假设key是64位,8个字节,再加上其他字段),
  73. 意味着B+树有1000个分叉;如果用来装叶子节点,一个page大概可以装200条记录(记录和索引放在一起存储,假设一条记录大概100个字节)。基于
  74. 这种估算,一个三层的B+树可以存储多少数据量呢/span>
  75. 第一层:一个节点是一个page,里面存放了1000个key,对应1000个分叉;
  76. 第二层:1000个节点,1000个page,每个page里面装了1000个key;
  77. 第三层:1000*1000 个节点(page),每个page里面装200条记录,即是 1000*1000*200=2亿条,总容量是16kb*1000*1000=16GB。
  78. 把第一层和第二层的索引全部放入内存,即(1+1000)*16KB,也即约16KM的内存。三层B+树 就可以支撑2亿条记录,并且一次性基于主键的等值
  79. 查询,只需要一次IO(读取叶子节点)。由此可见B+树的强大。
  80. page与page之间组成双向链表,每一个page头部都记录2个关键字段:前一个page的编号,和后一个page的编号。page里面存储一条条记录,
  81. 记录之间用单向链表串联。最终所有的记录形成双向链表的逻辑结构。对于记录来说,定位到了page,也就定位到了page里面的记录,因为page会
  82. 一次性读入内存,同一个page里面的记录可以在内存中顺序查找。
  83. 在innodb的实践里面,其中一个建议是按主键的自增顺序插入记录,就是为了避免 Page Split问题。比如一个page里一次装入了(1,3,5,9),
  84. 4条记录,并且假设这个page满了。接下来如果插入key=4的记录,就不得不新建新的page,同时把(1,3,5,9)分成两半,前一半(1,3,4)还依旧在
  85. 旧page中,后一半(5,9)拷贝到新的page里,并且要调整page前后的双向链表的指针关系,这显然会影响插入速度。但如果插入的是key=10的记录,
  86. 就不需要做page Split,只需要创建一个新的page,把key=10的记录放进去,然后让整个链表的最后一个page指向这个新的page即可。
  87. 另外一个点,如果只是插入而不删除记录(软删除),也会避免某个page的记录数减少而发生相邻的page合并的问题。
  88. 6.3.3 非主键索引
  89. 对于非主键索引,同上面的结构类似,每一个非主键索引对应一颗B+树。在innodb里面,非主键索引的叶子节点存储的不是记录的指针,而是主键
  90. 的值。所以,对于非主键索引的查询,会查询2棵B+树,先在非主键索引的B+树上定位主键,再用主键去主键索引的B+树上查找最终记录。
  91. 有一点需要说明:对于主键索引,一个key只会对应一个记录;但对于非主键索引,值可以重复。所以一个key可能对应多条记录。
  92. 6.4 事务与锁
  93. 6.4.1 事务的四个隔离级别
  94. 事务并发导致的问题:
  95. 1.脏读
  96. 事务A读取了一条记录的值,然后基于这个值做业务逻辑,在事务A提交之前,事务B回滚了该记录,导致事务A读到的这条记录一个脏数据。
  97. 2.不可重复度
  98. 在同一个事务里,两次读取同一行记录,但结果不一样。因为另外一个事务对此记录进行了update操作。
  99. 3.幻读
  100. 在同一个事务里,同样的select操作,执行两次,返回的记录条数不一样。因为另外一个事务在进行insert/delete操作。
  101. 4.丢失更新
  102. 两个事务同时修改同一条记录,事务A的修改被事务B覆盖了。举个例子,x=5,A和B同时把x读取出来,减1,再写回去,得到x=4,实际
  103. x的正确值应该是x=3。来源:enlyhua

    声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2021年10月11日
下一篇 2021年10月11日

相关推荐