Leon Ding Blog - ddia
type: channel
link: https://blog.leonding.me/tag/ddia/
items: 分布式系统设计的挑战
items: 数据库中的触发器
items: 分布式数据分区设计
items: 数据库事务隔离性
items: 数据库为什么要有事务支持
items: 分布式数据同步复制
items: 存储数据格式编码
items: 数据大端小端编码
items: 基于 LSM-Tree 存储引擎的问题
items: 数据存储和索引

分布式系统设计的挑战
type: item
date: 2023-04-10T17:19:00+08:00
description: 分布式系统对于刚刚入门作研发的程序员还是接触不多，刚刚入行的大部分程序员做的最多工作还是开发单体应用和简单业务操作，遇到最多问题估计也是本地线程并发数据安全问题，和数据状态一致性问题。而往后发展最多把单体应用部署为 HA 主从节点的方式备份架构，采用 nginx 做网络应用层的负载均衡。单体应用 bug 修复往往是万能法重启应用，让应用的状态消失，再恢复到正常状态来解决问题，但是实际上问题还是没有得到解决，除非是一些硬件故障可以跟换硬件，软件本身的问题需要开发者自己修复发布新的版本才能行。分布式系统[scode type="blue"]而分布式系统是由多个独立计算机组成的集合，而大型的软件系统被拆分成了多个部分，部署在网络中计算机中，这些计算机通过网络互相通信协同工作，在设计分布式系统时，需要考虑以下几个问题：[/scode]通信协议：如何让分布式系统中的各个节点进行通信？需要确定适合该系统的通信协议，并确保节点之间的通信稳定可靠。数据一致性：如何确保分布式系统中的所有节点都拥有相同的数据副本？需要选择适合该系统的数据一致性协议，如Paxos或Raft等，并在设计时确保数据的一致性。负载均衡：如何在分布式系统中均衡地分配负载？需要设计适合该系统的负载均衡策略，并确保在节点故障或网络问题时系统依然可用。安全性：如何保护分布式系统中的数据和节点不受攻击？需要在设计时考虑安全问题，并实现适当的安全措施，如加密和身份验证等。可扩展性：如何在需要时扩展分布式系统的规模？需要设计可扩展的系统架构，以便可以添加更多的节点和处理更多的请求。故障恢复：如何在节点故障或网络问题时恢复分布式系统的功能？需要设计故障恢复机制，并确保系统在出现问题时可以快速自动恢复。性能优化：如何优化分布式系统的性能？需要考虑各个节点的性能特点，并尝试优化通信协议、数据一致性协议、负载均衡策略等，以获得最佳的性能表现。[scode type="yellow"]网络和时间是设计分布式系统时必须考虑的关键因素，网络延迟、带宽限制和数据包丢失等问题可能导致节点之间的通信出现问题，从而影响系统的性能和可靠性。时间同步也是分布式系统设计中非常重要的问题，因为节点之间的时钟可能会出现偏差，导致数据一致性和协议的正确性受到影响，因此，在设计分布式系统时，必须考虑网络和时间问题，并采取适当的措施来解决这些问题。目前时间存在于物理世界中，一个系统可能被部署在全球的网络服务器节点中，这些地区的用户时区也不一样，而且是还是多应用进行异步操作的，此时分布式系统设计就不单单是单体系统中加个锁就能解决的问题了。[/scode]扩展性对于单体系统数据的一致性也是一个问题，例如数据库和缓存系统的数据一致性问题，当然缓存系统的数据一致性存在时间窗口的偏差是可以容忍的，但是对于分布式系统的数据一致性要考虑到问题加很多，关系型的数据库的 ACID 已经帮业务层解决了很多问题，而分布式系统特别是针对的分布式数据库，数据被分散在不同网络节点上，如何处理 ACID ？目前很多微服务架构是使用一些数据库中间件代理解决，或者说分库分表，而颠覆性的是 NewSQL 数据库例如 PingCAP 和 Amazon Aurora 分布式关系数据库来解决分布式数据一致性系统。[scode type="yellow"]分布式关系数据库用起来来和传统的 Oracle 数据库是没有区别的，对于业务层是无感知的，但是分布式数据库本身的设计问题是要对 CAP 定律进行权衡的，大部分采用的分区同步复制写入的方式，而这些数据如何保证一致性？总的来说，NewSQL 是一种结合了传统关系型数据库 ACID 的优点，并加入了一些新技术，以提高数据库的性能和可伸缩性。[/scode]为提高 CAP 定律中的 P 容错机制，通常采用的方法是将数据分片复制到多个节点上，以提高数据的可靠性和容错性，同时还需要考虑 CAP 中的 A 可用机制，对节点故障进行监测和自动故障转移处理，保证系统的高可用性，NoSQL 系列例如 Redis 采用的是一致性哈希算法，Facebook Cassandra 也是一致性哈希算法，这种采用 Gossip 协议进行数据同步的，但是数据还是分区存储在当个节点上，遇到 ACID 需求无法将整个集群视为一个整体系统做强一致性数据处理，只能在单个数据节点上做 ACID 处理，并且这种分区有热点数据问题。一致性为了保证分区所有节点数据一致性目前大多数数据库采用的是同步复制，换句话来说往集群中一个节点写入数据其他节点马上会同步到最新的数据，但是由于分布式系统是建立于在不可靠网络之上的，如果出现分布式分区脑裂情况，导致多个分区不能通常通信，此时数据版本该从何处写入，从何处复制到集群中剩下的可以节点中？目前大多数 NewSQL 采用两种分…

数据库中的触发器
type: item
date: 2022-10-10T15:05:00+08:00
description: 在数据库事务中如何保证数据一致性这也是给开发者带来许多的挑战，我们将现实生活中的东西抽象成数据库的关系模型，存放在数据表中，但是现实生活中某些东西有一些约束的，例如付款不能出现负数，人民币最大的面值是 100 元不可能出现比 100 面值还要大的面值，那么这些如何在数据库中保证这些约束？在做某件事情之前查看是否满足某个条件，这就是本篇文章笔者要写的数据库触发器，如何使用 Trigger 来保证数据正确性。应用场景例如下面我用 DDL 写一段 SQL 语句创建表结构，在这段 DDL 语句中最后有一句 CHECK (balance >= 0)，如下：CREATE TBALE account_info { uid INT NOT NULL AUTO_INCREMENT, name VARCHAR(10), balance INT, PRIMARY KEY(id), CHECK (balance >= 0), }这是用SQL语句里面的 CHECK 关键字来规定 balance 列的值不能小于 0 的约束，这就是保证现实世界银行账户不可能出现负数情况，但是这个 CHECK 关键字在 MySQL 中是不起作用的插入数据值时不会检测条件是否成立，而在 Oracle 和 SQLServer 中是可以起到作用的，如果某个用 CHECK 语句来判断条件的列不满足条件就会出现错误。触发器像是上面的这种场景在日常业务需求经常遇到这些问题，例如一个用户在注册系统的时候会查看当前用户名是否占用？如果被使用了就不允许使用，这时大部分做法是通过业务层的代码进行检查的，通过业务层通过 SELECT 语句条件判断，但是这种情况会出现一个问题如果系统并发量很多如何保证多个用户输入一个相同的用户名情况下还能被正常满足数据正确性，也就是多个用户的条件查询如何保证？一般操作将几个语句包装成一个事务去操作。上面这种只是一个场景在某个操作之前先要查看是否能满足某个条件才能操作，这种场景很多，在数据库里面可以通过触发器去做，触发器是数据库中提供一个非常实用的功能，它可以在操作者对表进行「增删改」 之前（或之后）被触发，自动执行一段事先写好的 SQL 代码，目前触发器支持的模式有：名称作用域BEFORE INSERT在插入数据前检查某个条件，如不满足返回错误信息。AFTER INSERT在插入数据之后执行某个预定义好的SQL语句。BEFORE UPDATE在更新某个条记录之前检查某个条件，如果不满足返回错误信息。AFTER UDPATE在更新某条记录之后执行某个预定义SQL语句，例如记录日志信息。BEFORE DELETE在删除数据前，检查是否有关联数据，如有，停止删除操作。AFTER DLELTE删除表 A 信息后，自动删除表 B 中与表 A 相关联的信息。有了触发器在某些场景下就不需要再业务层做额外的处理了，有时候也不需要让业务方知道这些事情，例如每条更新操作都会被记录到日志表中，触发器也有自己的特定的语法的，类似于 Linux 下的 Shell 脚本，触发器的基本语法如下：DELIMITER // CREATE TRIGGER [触发器的名字] [触发器执行时机] [触发器监测的对象] ON [表名] FOR EACH ROW [触发器主体代码]// DELIMITER ;在头部和结尾的 // 符号表示触发器开始与结束，下面的中括号里面都已经其作用，触发器执行时机可以是 BEFORE 和 AFTER ，而触发器监测的对象则为具体对应操作动作类型可以是 INSERT 、 UPDATE 、 DELETE 这些，表名则为具体监测对象表，触发器主体代码则为具体触发器代码，下面是一个实例：DELIMITER // CREATE TRIGGER validate_sales_amount BEFORE INSERT ON account_info FOR EACH ROW IF NEW.balance<=0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = "你输入Balance金额不合法！"; END IF// DELIMITER ;上面这段代码中，我们使用 IF...THEN...END IF 来创建一个监测 INSERT 语句写入的值是否在限定的范围内的触发器，这个触发器的作用就是为了检测在向 account_info 表添加新数据时会检测 balance 列的值是否合法，从而达到前面类似于 CHECK 的效果。触发器缺点触发器不是没有缺点的，虽然能解决一部分业务场景下的问题，但是需要注意合理使用。例如一些变态的需求，假设一个系统有张用户表里面存储用户账户和余额信息，需求要让每个用户在完成每笔交易之后都会对其让整个所有用户余额减去用…

分布式数据分区设计
type: item
date: 2022-09-29T15:23:00+08:00
description: 本篇为分布式系统数据分区设计章节总结，当一个数据系统存储的数据单位达到TB级别往往都会采用多个服务器机架存储；面对海量的数据集合和非常高的查询请求的压力，往往都会把这些压力分摊到多个数据分片中处理。很多数据库也支持数据分区，可以把分区看成电脑上分区不同的盘管理着不同的资料文件。一个大型的数据库可以拆分成多个小数据库，和上面篇讲解的数据按地理位置分布效果类似，分而治之。数据分区主要的目的就是为了数据做冗余存储和异地多活，防止因为一个数据节点故障导致不能正常访问数据，另外可以提高整体系统查询数据吞吐量，将查询请求路由到不同数据分片；设计数据分区主要的问题是如何进行数据分区？查询请求如何定位到数据所在的分区？数据如何均匀的分布？热点数据如何处理？数据分区数据复制和分区通常一起使用，把整体数据系统分成多个数据分区，每个分区节点都有分区的副本，每条记录属于特定的分区，而同样的数据内有在不同分区存在副本，这样就可以提高整体系统可容错性。分区主要设计目的就是为了将数据分散起来存储，并且能通过分区来负载查询请求压力，理论上如果有 10 个节点，那么相比单节点就可以提供 10 倍的读写吞吐量，也提高 10 倍的系统容错性，如下图：像上图这样将数据分区到不同节点上做冗余存储，这样可以提高整体系统容错性，当某一个分区节点故障时，其他节点也能正常通过服务，但是这种模式会出现热点数据倾斜问题。数据倾斜，在极端情况下会导致分区效率严重下降，例如如果有 10 节点，一个热点数据节点，那么剩下的9个节点就为空闲状态，系统瓶颈就在最忙碌节点之上也会成为整个系统的热点。避免热点数据方法就是将其数据记录分区将其分布到多个分区中，这就会有另外一个问题如何的访问这些数据？如何查找到需要的那条数据？这时就要设计一个数据分片规则来存储数据记录，将其分布到不同的节点中保存？目前数据库相关的存储设计是简单的键和值数据模型，可以通过键计算哈希值，通过特定的算法将其分布到节点，这可以查看我之前写的一篇文章一致性哈希数据分片算法。但是哈希算法缺点不支持范围扫描，只能通过特定的哈希值来取决于具体存放位置，如果算法存在某些问题或者对应键计算处理的值不能满足均匀分布一样会带来另外一个后果数据分片不均匀。例如图书馆书籍按照26个字母进行排列分区，某些书架的区间比较大那么存储的书籍就越多，产生了分片不均匀的情况，这时就要设计一个合理的数据分界条件。例如物联网传感器设备采集的数据，如果按照键存储分区可能会出现分布不均匀情况出现热点数据情况，但是可以按照时间段分区处理，而基于时间戳则可以对应一个时间范围来做，例如将每天的数据存储在为一个分区，我们可以通过键和时间戳一起组合使用来达到区间访问的效果，这叫做复合键。二级索引分区上面介绍分区的方式是通过记录的键进行的分区，还有一种就是通过记录类型进行分区和特定的属性进行分区。例如有一个二手车交易网站存储不同品牌的汽车的记录数据，这时平台查询要通过根据特征的查询操作，这时就可以通过属性进行分区和建立二级索引的方式。可以基于唯一的文档 ID 进行分区，ID 的范围分区，0 至 599 存放在 0 号区域其他以此类推，这时针对这些区域存储数据记录进行构建二级索引，可以是汽车的颜色和车龄等...这种方式的分区要求每个分区自己能针对所存储数记录的属性建立维护一个二级索引，并且每个分区都是独立维护自己的二级索引，索引数据来自本分区；其缺点也很明显，每次查询特定颜色的汽车时，要查询多个分区，并且最后汇总数据，如果是串行的方式很有可能出现尾部响应问题。二级索引的另一种实现将其设置为全局二级索引，每个分区不需要维护本地分区索引，全局的索引方式也可以分区例如通过汽车的颜色进行分区，将颜色为红色的汽车收录到在一个索引color:red词条中，索引本身也分区将其按照 26 个小写字母排列顺序分区，这种分区称之为词条分区，这种方式不需要再查询某个特定颜色的汽车时不需要搜索全局的分区，查找符合条件的数据；但是缺点明显是当写入一个新的数据记录要跨区操作涉及到分布式事务问题，索引的数据要更新速度会受影响。动态分区分区原则是在数据库创建时就要设计好其数量，这样就可以应对未来的容量需求，但是如果设计的太多会导致一部分节点空着没有充分利用。分区的实际大小应该与集群中的数据总量成正比，小了可以缩容，大了可以扩容，数据的数量肯定是不确定的这就给设计带来一个问题如何设计扩容的策略？ 上面都根据键进行的分区策略，边界条件没有设置好可能会出现所有数据出现在少数几个分区里，而大部分分区却没有数据，导致数据倾斜到某几个分区节点上。业界一些有名的存储项目例如 HBASE 采用的动态分区策略，当数据超过某个阀值时进行拆分，当数据被删除时就自动调整合并一个分区，这和 LSM-Tree 的按大小策略扩容很相似，…

数据库事务隔离性
type: item
date: 2022-09-29T11:49:00+08:00
description: 事务作为每个数据库提供的一层高度抽象机制，保障着数据的安全性和一致性和持久化，这时每个数据库必备的功能；只是不同类型的数据库实现可能存在某种差异化，有了事务的保障应用层程序才能简单处理一些复杂的业务逻辑，如果没有数据库的事务保障那么对于一个数据库来说这时一个很难解决的问题，程序要面对进程崩溃和网络中断，并发数据竞争问题。最重要的是数据库要面对多个客户端连接的操作，此时客户端完全在异步环境下操作的，时间是固定的，但是每个客户端操作事务可能是异步进行，当在某个时间段内处理多个事务就会面对其并行事务问题，让异步的操作变成同步操作。本篇文章对几个场景的事务处理方式举例，表明事务对于这些场景的重要性。读提交事务例如一个计数器场景用户1和用户2同时操作计数器，这时就可能会发生读取到脏数据的场景，防止脏读的场景如下图：如果其中某一个用户在未提交情况下，被其他用户读取到了未提交的数据就为脏读，在前一个事务未提交的时候读取的数据只能是已经被提交的数据。另外一个场景 Alice 和 Bob 同时去买一辆汽车，但是交易网站有一个场景就是要分为两个事务步骤才能处理完成一笔交易，分别是更新被购买汽车的主人信息，再更新汽车购置发票信息，但是这时如果是多个用户同时并行操作就会发生脏写的情况如下图：因为整体操作依赖于时间异步操作的，可能二者都并行操作，此时当Bob结完账，系统缺将发票信息设置为了Alice，这就是出现了脏写的情况。Read Committed 读已提交：防止脏读和脏写通常都是通过锁🔐的方式来解决相关事务隔离问题，如果事务想修改某个对象字段必须先要申请获取到一把锁，然后事务在执行直到中断或者提交才能归还锁，与此同时其他事务必须在此等待；这种方式确定很明显如果一个写事务运行时间过长，会导致很多只读的事务等待太长时间。大多数数据库读锁场景采用的双版本维护机制，在写事务未完成之前新的读操作返回其旧值，以此来缓解。不可重复读读提交隔离机制的实现相对于大多数的事务确实管用，但是还有一种场景存在问题，例如Alice有两个银行账户余额为 1000 块钱，这时要从其中一个银行账户想另外一个银行账户进行转账 100 元，最后数据库系统要完成两个账户数据最终更新操作，如下图：此时因为在转账的过程中来查看其账户余额就会发生两个银行账户余额不一样的情况，发现少了 100 元，这个就是因为事务处理时间窗口异常情况，这种异常为 不可重复读 取或 读偏斜情况。出现这些问题在当前场景是可以容忍的，在短暂几秒钟之后刷新查询数据则为最新版本。但是另外几种方式就是不能允许的，例如备份场景因为备份可能需要几小时才能完成，这样就会导致两个数据库的数据出现旧值和新值情况，如果最终数据没有弥补检测机制会出现永久不一致情况；另外一种方式就是实时数据分析场景下，定期检测数据完整性，如果不在用一时间点内这些分析结构毫无意义。快照级别隔离出现读倾斜和不可重复读的情况原因就是在多个事务执行过程中因为时间窗口导致的，快照级别的隔离的实现是针对某个事务在某一个时间点创建一致性快照中读取，及时在事务执行过程中被另外一个事务修改了，但是每个事务只能看到该特定时刻的旧数据，而不会出现读偏斜的情况。实现快照级别最常见的方式为 MVCC 多版本并发控制，MVCC 实现原理为对每个执行事务分配一个事务 ID 并且是自增长的，当某个事务向数据库写入新的内容时，被写入的数据都会标记为写入者的事务 ID 。表中的每条记录都两个 created_by 字段 和 deleted_by 字段，如果事务要删除某条数据自己将其 deleted_by 字段设置为本次事务的 ID 号，仅仅是标记删除的，而谁创建此条记录则将其设置为对应事务的 ID 号，如下图：被删除的数据记录当没有被其他事务引用的时候，就会被数据库的垃圾回收进程真正的删除掉，来释放存储空间占用。事务 ID 其实是一个为 32 位的整数，大约可以在处理 40 亿次事务之后会溢出，这个设计的看具体不同数据库实现。在不同关系型数据库产品中都已经实现多版本并行控制的事务隔离级别，在不同版本中只是叫法不一样，Oracle 为可串行化，PostgreSQL 和 MySQL 则称之为可重复读。在 SQL 标准中并没有定义如何去实现快照隔离级别，较老的数据库也没有快照级别这种概念。事务可视化规则： 多版本并发控制在一致性情况下要考虑到每笔事务之前开始时要检测当前系统中正在执行中的事务，即尚未提交或终止的事务，这些事务部分写入数据不应该被展示出来，即不可见，即使这些事务之后会被提交。所有的终止的事务操作的修改全部不可见，较晚的事务 ID 所操作的修改也不可见，不管是否成功提交事务。除此之外的其他所有操作的写入的数据查询可见。快照隔离级别还有另外一种实现为通过索引快照隔离，将索引指向多个…

数据库为什么要有事务支持
type: item
date: 2022-09-26T15:21:00+08:00
description: 分布式数据系统设计会面对很多的问题，整体系统出错的地方有很多例如：硬件问题或者断电问题，部署在不同节点上软件程序会出现 Bug 故障，由于节点之间依赖于网络会出现随时中断连接的情况，多个客户端操作某一条数据记录如果没有控制好可能会产生脏数据情况，客户端之间的操作可能会发生边界条件竞争的问题。如何提高系统的高可用性和数据可靠性的目标，就必须要设计一个完整的方案解决相应的问题，本篇将注重分布式系统中事务处理技术一些实现细节剖析。何为事务在前面的文章讲过数据被分成多个分区如果需要操作可能就需要执行多次操作才能完成一次完整的查询请求，随之而来的问题就是如何保证操作的多个分区节点都完成对应操作，如果其中一个分区节点执行操作失败了怎么办？事务机制就是一种简化这些问题的首选机制方案，事务机制可以把客户端发起多个读写请求操作捆绑成一次逻辑操作单元，这样我们就将多个写操作或者读操作视为一个执行的整体，这就是事务，事务执行要么成功要么就失败，没有如何的中间状态。如果执行失败程序还能回滚状态，也可以重试操作，有这样的设计客户端操作起来就变的很简单。要理解事务机制就要了解它做些什么？传统的关系数据库都是支持 ACID，这里的ACID分别为：Atomiciy、Consistency、Lsolation、Durability；这4个单词的首写字母组成的，这早期的数据库相关论文中提出，描述一个数据库的容错机制的定义。 传统关系型数据库大部分都已经支持这些功能，只是具体的实现细节每个不同厂商可能略有不同但是提供功能是符合ACID定义的。但是在NoSQL相关的实现就会有所差异，大部分为了更好支持系统的扩展性和高可用性放弃严格的ACID功能实现，弱化的了ACID的一些功能，而相应却使用另外一套BASE理论去实现数据库相关的功能，BASE的定义提倡分布式系统基本可用性和软状态、数据最终一致性，这就使得某些分布式系统在事务上支持并不是很完善，例如Redis在集群模式下的事务操作支持。原子性： 很多第三方资料描述的就是一些列操作集能一次性执行完成，结果只有两种状态为成功或失败。原子性在 ACID 中最为重要，一个客户端发送了包含多个写操作的请求，当服务器收到请求执行如果发生了错误，硬件故障等情况，这时就视为本次事务操作不成功，则会中止本次事务执行，并且回滚和撤销局部的修改，使得失败后的数据和未执行事务之前的状态保证一致。一致性： 数据库中的数据在操作修改的时候必须满足状态约束，对数据的修改必须是预期状态变更，例如我们在操作微信转账的时候就会产生2次数据记录状态的变更，A 向 B 转账 100 块钱，对应的事务操作就是要保证其A账户会减去 100 元，而对应的B账户能添加 100 元，事务执行开始是从一个有效状态转换成另外一个有效状态，并且其更新操作没有违背约束条件，结果状态符合预期并且是有效的状态。一致性是有应用层保证的，因为如何定义事务的内容，这时有客户端决定的，如果客户端故意向服务器提交了一下些错误的数据，这里一个例子就拿上面转账来说，如果客户端把 100 写成 1000 这就是无法保证的，但是数据库可以保证操作数据类型是否合规。隔离性： 传统关系数据库都会支持多个客户端会话连接，每几个客户端可能在某一段相同的时间内发起多个写操作事务，操作变更相同的记录，很有可能就会发生数据竞争问题，例如下图：这时就需要将其两个操作串行化来操作避免造成数据混乱情况，但是如果是串行化的操作会使得事务会排队很多商业数据库已经放弃这样的实现方案，Oracle 数据库采用的快照隔离。持久性： 对于一个单机的关系数据库来说操作的事务数据记录能安全可靠得被持久化存储起来，它能保证一断事务操作成功提交，数据就会被持久化起来不丢失，即使发生了硬件故障。对于分布式数据库来说还要考虑到数据副本的复制和持久化，要保证其他远程的节点已经成功持久化数据记录，事务才能算成功执行持久化。假设一个邮件系统事务应用场景，例如现在有一个需求用户1和用户2同时操作邮件数据库，用户1向数据库中写入邮件信息和更新邮件未读邮件数，因为这2个操作没有组成事务，可能会出现这种情况，当操作写操作的时候用户2正在读取未读邮件，此时用户2读取到邮件内容，但是未读邮件数却还是为0，这就因为没有事务原子性导致数据混乱的情况发生。单对象和多对象事务有些第三方资料容易把放到多个客户端并发同时修改一个对象的时候问题解决方案称之为事务，例如 CAS 的方式轻量级事务，这种严格意义上说不能说 ACID，通常意义上的事务是对多个对象的，将多个操作聚合为一个逻辑执行单元，而 CSA 严格意义上来说只是多线程并发下的术语。 在关系型数据库中一次事务处理所需要的数据可能由多个表组合而成，表与表之间的关系可能存在某种约束，例如外键，这时如果操作一条数据记录…

分布式数据同步复制
type: item
date: 2022-09-18T15:27:00+08:00
description: 一个公司的产品随着时间的发展单机的架构肯定无法满足其负载状态，产品迭代也是随着用户量进行的，当把单体架构拆分成多个微服务时就考虑一些数据同步问题，例如两个服务同时操作缓存中的一条数据或者是数据库中的数据，都会带来一些数据上的问题。这篇文章主要是针对的数据库系统，主从架构，主从切换、主从数据同步一些问题展开，也是DDIA第5章开始的数据复制和分布式系统设计一些挑战问题的总结。当单机服务不能满足需求时系统往往需要扩容，扩容之前讲过有两种，一种垂直扩容的方式进行的，就是无条件的堆机器硬件，这种方式即使把机器性能加到很强的状态也不一定能满足系统高可用的设计；另外一种就是水平扩展，通过将程序代码部署到不同地理位置的数据中心里运作，这些节点通过网络进行数据交换，这种方式有良好系统扩展性，但是会依赖网络，网络是很不可靠的，这时系统设计时就要考虑单节点故障时能保证整体可靠性。分布式系统节点进行数据复制可不像是在本机共享内存，本机复制数据只需要操作系统提供的API即可；而分布式系统重数据量太多的时候，往往会将其数据分区到不同的子系统上，也可以不同的网络节点上，这些数据如何同步？如果有主从关系如何保证数据的完备性？主从架构当一个数据库表记录达到某个上限可能就会考虑分库分表来缓解外部事物读写请求压力，有时候没有读写请求压力时也会考虑做主从架构考虑数据库的高可用性。例如主节点用来作为事务处理，而从库做一些数据分析相关的工作，另外就是当线上业务读事务大于写事务的时候，可以部署多台从节点来帮助主节点分担读压力。关系数据库做主从架构有一个很大问题就是如何保证事务的 ACID ，因为主从架构都是通过网络连接的，虽然大部分业界主流的数据库都提供了主从架构配置设置选项。但是不能保证多台节点做联机事务处理，目前支持这种也就是 NewSQL 系列的数据库。且大部分写请求是操作主数据库的，如何把主数据库的数据记录同步到从节点上？如何设计同步规则？怎么实现数据多节点同步完整性?目前主从架构的另外一个问题就是如何在主节点失效情况下，其他子节点如何晋升为主节点？如何处理节点失效问题？如何处理添加节点问题?数据同步即架构为分布式主从结构就要考虑解决数据多节点同步问题，目前数据同步分为3种：同步复制、异步复制、半同步复制；同步复制当用户向主节点写入一条数据记录时，此记录就会同步到其他节点，同步过程必须是当所有子节点写入成功之后才能返回请求，一段向用户确认说明主从节点数据都是最新版本的，缺点也很明显如果出现网络故障一直迟迟没有成功写入子节点请求就一直不会返回，其他写请求也会被阻塞。另外一种方式就是异步复制，将集群内部少数节点的数据复制设置同步的方式，其他多节点为异步的方式，异步复制可以在后台进行，当一条数据被同步到部分从节点时就可以返回写入成功请求，这样可以节约写入响应时间，这种同步和异步一起使用我更多认为是半同步复制。当集群中某个节点失效时，如需要增加新的子节点提高系统整体容错能力时，如何同步之前的数据？因为主节点是在并行运行着的，而新加入节点想快速同步全量数据该如何做？如何保证数据一致性？数据同步光靠着即使同步数据文件是无效的，因为此时主节点也在写入其他事务请求，而新加入的节点只能同步数据文件中数据而不是及时的；这里 Redis 主从架构设计的方式采用的是数据快照和增量同步方式，当需要同步时主节点快速生成数据快照通过网络传递给从节点，从节点则恢复数据，至于主节点新写入数据采用记录日志的方式同步，从节点为追赶模式，直到数据达到一致性；增量同步方式也适用于少量的网络抖动情况，当出现暂时性节点失联情况数据也能完成增量同步。不管是同步还是异步都会产生一些矛盾的地方，例如增加从节点是为增高读的吞吐量和系统整体可用性，如果将集群中的所有节点都设置为同步模式也会带来另外一个问题，每添加一个从节点，那么每次往主节点写入一条记录需要同步的数据记录就要写入到其他子节点中，子节点越多那么系统整体可用性就会下降，因为是全同步模式导致一次写请求要多次复制写入情况，每多一次复制写入就会多一次请求写失败概率，同步式要等着所有节点写入成功才能返回响应。这里可能有人会想全部换成异步模式不好吗？当把数据记录请求写入到主库中，其他节点就后台异步复制数据，但是然而因为是异步所以数据同步肯定会出现时间偏差的情况；当因为业务方可能是并发操作的不会立即等待异步数据写入才去读取，这时不现实的，当主节点写入的数据的成功，但下一个请求读取落到子节点而此时子节还没有完全同步数记录，导致读取不到数据记录情况发生，时序情况为下图：异步数据同步数据一不是像同步一样做强一致性的，而类似于 DNS 域名解析生效过程，采用的最终一致性，这也就是复制滞后问题，如果采用异步复制的模式可以为数据记录设置版本号。我个人想法是在写入更新请求之后一段时…

存储数据格式编码
type: item
date: 2022-09-17T15:29:00+08:00
description: 计算机程序在通过网络进行数据交换过程中会使用到数据格式编码，一些本地持久化文件也是需要进行编码存储，前面讲到了数据密集型应用设计中一个需要考虑的有可扩展性和可演变性，本文将对数据编码的可演进性进行探讨，改如何设计数据的编码方式？用什么样的数据格式？应用程序随着时间的变化数据格式和编码也会发生变化，怎么做到向前兼容？在关系模型中数据表结构在项目需求分析阶段就已经确定了，通过DDL创建表结构，但是在项目运行一段时间后不能保证需求没有变化，如果发生了变化这时就需要对其表结构进行修改，例如使用ALTER来修改其表字段结构，这个就属于数据格式的变化了。数据库中的表结构一改，那么业务ORM层的映射对象代码也要跟着变化。这种变化在服务端，如果是线上项目就需要进行滚动升级发布，可以尝试着部署几个节点跟踪其运行状态；而如果客户端应用，只能看用户自己是否愿意升级程序；但是也可以在第一个版本发布的时候再客户端加入强制升级的代码，如果有一些紧急Bug修复可以强制升级。软件兼容性人类语言发展过程也是类似的，现代人能看得懂古代的人类的语言和文字，但是古代的人类不一定能看得到现代人的文字和语言。这种场景换到程序设计上就是软件的兼容性问题，开发软件都明白设计一个好的通用型API可以以免后期继续添加其他的新的API接口，例如开发一个本地存储软件需要设计数据存储的格式，在第一个版本开发的时候就要考虑到兼容问题，编写一个存储软件要考虑兼容性分为两种：向后兼容：新的代码可以读取老的代码编写的数据文件。向前兼容：较旧的代码可以读取新的代码编写数据文件。目前来看向前兼容是最难实现的，因为不能时间回到过去，在软件设计初期就要考虑到这些问题，设计提供一些灵活的接口去实现，或者通过动态的配置文件，定义一个新的标准去做。业界很多知名的软件都有相关的解决方法，和兼容性问题解决方案，笔者我将会列举几个知名软件在兼容性方面的设计。如果都写个Java程序都知道Java源代码编译输出的是Class，而运行Java程序的是JVM，但是就是因为这两个东西不能打包在一起就会出现很多兼容问题，使用低版本的JVM不能运行高版本Java编译器编译出来的Class文件，而高版本的JVM会向前兼容底版本Class文件。不能向后兼容的问题很多知名软件都存在，Java为了区分不同版本编译的Class文件，在每个文件头部使用了特殊的编号，如果懂得这些原理的人可以通过十六进制编辑器修改其版本，运行高版本Class文件。存储相关的软件Apache Cassandra在它所产生的数据文件也会有相应的版本标记，方便后续维护的时候能区分数据文件是哪个版本生成的，可以使用对应的程序代码进行操作。数据编码格式程序运行所产生的数据绝大部分在局部栈空间或者在堆空间中，但是都属于计算机内存中的数据，在内存中维护数据结构比在磁盘上维护数据结构要简单的多，但是大部分分布式系统需要多台机器共享内存中的数据，这时就需要使用数据编码和序列化功能了。大部分编程语言内存都支持正反序列化的类库，帮助将内存中的数据序列化成一个特定的数据串。大部分前后端分离项目使用的是XML和JSON做数据交换，XML相比JSON需要更多字符串空间和结构化部分，JSON相比XML要简单以字符串和键值对组成数据对象。XML和JSON对于人类是方便阅读的，因为是纯文件格式又带来另外一个问题被编码的数据没有具体的数据类型。JSON可以区分数字和字符串，但无法区分浮点型的精度。例如服务器端使用Java计算出来的浮点数类型通过JOSN传送到前端给JavaScript时，就会出现不能明确表示，Twitter为了解决这样的问题就是返回的JSON中使用了十进制字符串表示。另外也不用别出心裁使用特定的序列化格式，把格式和特定的语言绑定到一起，这样会导致后面迁移项目或者扩展项目时对使用的编程语言产生依赖关系。二进制编码对应比较传统的方式数据编码采用JSON和XML是完全没有问题的，只是为了把数据能按照特定格式进行编码传输或者存储，但如果是一些对性能和空间利用率的场景下这些方式就不适用了，数据规模达到TB级别的，数据格式的选择会产生很大的影响。JSON和XML都算是在一种文本格式基础之上所进行改造的一种格式，相比二进制格式占用的空间要大的多，特别是XML格式很多冗余的标签信息。目前比较轻量级的二进制数据格式有BSON、UBJSON、Smile)、WEBXML等，这些都是类似于JSON数据格式，但是相比JSON数据组织方式确实二进制，更节省空间在网络传输的时候性能也要相应的提升；例如下面就为BSON和传统JSON的编码差异：Bson: \x16\x00\x00\x00 // 总文档大小 \x02 // 0x02 = 类型：String（字符串） hello\x00 // 字段名 \x06…

数据大端小端编码
type: item
date: 2022-09-14T15:35:00+08:00
description: 在计算机内存上操作数据一般操作系统都通过了一些系统调用接口，直接使用这些接口就可以；相比在磁盘里面操作要是简单的多，程序运行的时候大部分都是从外存加载二进制文件到主存上执行，主存的操作相当简单，在编程语言实现上只需要分配和管理就可以，操作系统提供了虚拟内存来控制。而在磁盘上访问内容我们就需要通过偏移量来访问了，数据是序列化二进制存储在磁盘上的，如何把主存中数据结构存储到磁盘上有相应的存储布局呢？这时本文要介绍一些实现磁盘存储和数据编码原理。在磁盘上的数据布局远比内存上重要，内存速度虽然比磁盘读写数据要快，但是容量会受到限制并且容量可能是磁盘容量的千分之一，并且断电数据会丢失，这时我们就需要通过能持久化的磁盘设备来保存数据，但磁盘的速度又比内存操作慢，这时存储数据我们就要设计一个高效并且合理的数据结构做磁盘的数据布局。这也是之前前面几篇文章要介绍的LSM-Tree系列的数据持久化组织管理方式，磁盘能存储更多的数据但是怎么存储？怎么编码？如何快速获取某部分的数据？设计什么样的数据结构能提供CPU缓存行利用率？怎么能高速正反序列化数据内容？字节序日常开发的时候都使用的着编程语言所通过的类型系统进行开发的，我不需要关系怎么存储的，但是如果需要把数据在网络上进行传输和磁盘上持久化存储这时就需要将其序列化成特定编码的格式。数据记录有字符串、整型、浮点型、布尔型之类的原始类型以及它们的组合构成的，但是要在网络和磁盘上使用的话，就需要转成字节序列了，网络发送数据要进行传输就需要序列化数据，磁盘写入也要序列化数据，然后读取需要反序列化数据。字节序编码方式决定了一组字节的先后顺序是怎么样子的，计算机字节序编码方式有两种分别为Big-Endian和Little-Endian进行的，具体差异如下图：大端字节序： 是从最高有效字节开始，从高位到低位依次排列，换句话说高位字节在前，低位字节在后，这是人类读写数值的方法。小端字节序： 从最底有效字节开始，从低位到高位依次排序，低位字节在前，高位字节在后，这时符合计算机底层电路运作的方法。为什么要有字节序？这就设计到计算机底层一些电路原理了，我们人类大部分阅读一篇文章都是从左往右阅读，当然也有少数国家的人阅读是从右往左阅读；而换到计算机上，计算机处理数据电路先处理低位字节，效率比较高，因为计算都是从低位开始的。所以，计算机的内部处理都是小端字节序；但是人类还是习惯读写大端字节序，因为符合人类阅读习惯，如果想让计算机运行够快还是推荐使用小端字节序，但是注意网络数据流采用的大端序。数据布局所有的原始类型都是固定大小的，然后怎么用原始数据类型组建一个高级的数据类型，可能也是相应类型的数组。这样组成的类型怎么序列化？通过我们可以将其内存计算字节序的大小，然后将大小放到某指定的位置，下次读取时可以根据这个位置读取对应数据反序列化处理；很多语言对应内存对齐概念，保存Java也尝试往原生的数据类型转型，可以参考这篇文章Project Java Valhalla。像布尔类型和一些特殊标记位可以使用0和1直接代替，没有必要使用用很长的数据类型进行编码，例如枚举类型可以使用一个简单是整数类型，标志位典型应用就是TCP包头的特殊的标志位，可以表示一个数据包的类型用途是什么，当数据到达接受端可以提取对应的标志位判断数据包类型做出相应的动作，具体查看这篇文章When to use TCP。在开发者过程中我们可能需要定义一些数据结构或者Java中的class，对应数据库中可能就是一张表结构，但是这个表结构如何存储在磁盘上，某些数据是在运行时产生长度不确定，但是知道其类型，例如文档类型NOSQL类型种类很多，如何将这些数据存储磁盘上持久化？设计一个Schema对应结构进行编码存储，具体实例可以查看前面我的一篇文章Bitcask记录编码组织方式。字节序只是计算机运算的时候需要的地址访问组织方式，具体也看CPU的设计，网络是一个例外使用的大端的方式，至于用什么用方式没有那么重要，主要是在读取数据时要使用正确的方式进行访问还原其值。其他资料字节序Little Endian vs Big EndianBig-endian and little-endianL'endianness, les headers et le BOM从一个Emoji字符说起Endianness（字节序）

基于 LSM-Tree 存储引擎的问题
type: item
date: 2022-09-11T15:36:00+08:00
description: 看过 LSM-Tree 论文的都知道这个存储引擎最核心功能就是 SSTable 层次划分和多个 SSTable 压缩操作，本文将针对两个问题进行深入剖析相关实现的细节和一些容易出现问题的地方；如何划分多个 SSTable 层次关系？如何进行多SSTable 合并？并且如何解决数据版本和操作写入一致性问题？针对这些问题怎么做怎么解决？还有被标记📌为删除过期的记录存在多文件数据版本冲突问题，我看某些基于 LSM-Tree 实现的数据库是在第一次合并的时候遇到被标记的就删除完全没有考虑到多数据版本和写入时间顺序一致性问题？那样做起来我个人感觉是非常不严谨的做法。可以看看业界一些主流的基于 LSM-Tree 实现的数据库是怎么做的，本文将继续探讨这方面问题。RUM模型Read-Updated-Memory 分为存储的读、写开销和内存占用开销比例模型，RUM猜想指出如果要设计一个存储相关应用程序首先是考虑读和写请求处理的速度，还有内存占用率；最理想的状态是最小的化读取开销，还能保证小的写入开销且内存占用也很低，这在现状的存储架构来说几乎是不可能的，想要优化某参数就必须牺牲另外一个参数为代价，所以设计一个存储系统必须要考虑这些因素。层次划分由于 LSM-Tree 的核心就是以追加的方式进行数据操作记录，因为是增删改查的操作都是以追加的方式进行的，这时对硬盘容量就提出了要求，必须设计一个能自动回收无效的数据的算法来解决这个问题。压缩操作在目前主流实现分为两种，第一种为 Leveld Compaction 的层次划分方式，第二种为 Size-Tiered Compaction 的大小划分方式。层次划分： 针对磁盘上的 SST 文件进行等级划分，分为多个等级的文件组，每组都有文件个数限制并且都有相应的 SST 文件的编号，这些编号都是从上往下的，高层次都在底层，而低层次的在高层；每层次文件达到阀值时会挑选几个文件与其高一层的文件进行合并，合并过程可以是并行执行多个文件的层次的，但是注意不要交集执行，那样会数据出现重复压缩情况。压缩会不断排除掉一些重复的记录，注意这里不是指被删除的记录！当压缩完成之后会生成 SST 文件放入高层次中，被压缩的旧文件会被清理掉，这样最新的数据在最低层上，而老的数据在最高层上，就减少了查找数据时要读取多个 SST 文件的情况，并且新数据会处于文件尾部，而读取操作是从文件尾部倒着开始读的，所以新值一定会最先被读到；但是每层的多个 SST 文件相互之间是无序的，所以需要进行层次之间排序：下图就为并行合并压缩情况示意图：那怎么选择每层和下一层合并的文件进行合并？例如第一层容量为3，下一层容量为4，我这里层次容量公式是i * 3 / 1.5 + 4，假设经过什么的公式推算出了的4层架构是：Level 0 ： size = 4 Level 1 ： size = 6 Level 2 ： size = 8 Level 3 ： size = 10 Level 4 ： size = 12如果单个文件大小为 1GB ，那么最后一层可以存储 1GB * 12 的旧数据，对应一个普通型的存储还是完全是够的，如果不够还可以进行添加层次。怎么挑选文件呢？这里我以每层顺序方式进行的，当某个文件被选择中，内存中的压缩器会记录这些文件排除被交集并行重压缩的可能。文件大小划分： 这种方式针对每个 SST 文件按照自身的体积进行划分的归类的，也是基于大小规则但是针对是文件大小，较小的文件和较小在一起，而较大的文件和较大放到一起；例如较低层的表是内存表刷入进来的，也有可能其他大表经过压缩而降级移动过来的。但是弊端也是有的被压缩之后还是很小，数据记录由于墓碑遮蔽了没有合并写入到表里，这样导致合并算法一直在处理小 SST 文件，而高层的 SST 文件墓碑却一直不能被回收的情况，导致出现高层次表饥饿的情况，这时就不得不采用强制执行压缩，过程示意图：两种方式只是针对 SST 文件如何管理做了相应的划分，按照层次划分的话每层文件都有序号最新的数据在最底的层次之上；按大小划分最新的数据在 SST 文件大小最小的文件身上，老数据在大文件身上；因为是随着时间的顺序数据跟着变化的，如果外部频繁删除请求说明对文件压缩的准确度来说不需要那么准备无误，因为频繁删除键记录说明老的数据文件里面的脏数据越多，大概率清理即可，针对这种时间变化的时间 Apache Cassandra 是用了一个滑动窗口的算法进行定期压缩 SST 文件。脏数据剔除由于删除记录也是通过添加的方式进行的，就会带来另外一个问题，数据记录怎么删除？官方论文解决方案是通过添加一条🪦墓碑记录来完成删除记录操作，也是最新的查找的数据记录从最低层往最高层次查找，如果发现记录被记录为 tombstone 状态则说明这条记录已…

数据存储和索引
type: item
date: 2022-09-05T15:38:00+08:00
description: 前面两篇讲解基本数据系统设计原则和还有如何把现实生活中的对象关系抽象成一个数据模型，被存储的数据可以通过一个特定统一的API访问接口进行访问，而且能保证安全控制。但是问题是怎么存储这些数据？如何把这些结构化和非结构化的数据做存储？数据如何编码？本文为DDIA书的数据编码和索引章节的整理和总结。如果是一名应用开发程序员是可以不用关注数据是如何存储和如何快速被查询到的。如果对存储和数据检索非常感兴趣可以看看这篇内容，看完你可以自己实现一套存储引擎玩，在实际开发过程中也能从现有的存储引擎中找出一个适合业务场景来使用，知其然知其所以然。数据存储和编码的方式会影响一些特定场景的性能，例如选择是OLTP还是OLAP？两种方式都有具体应用场景，那对应的底层数据存储格式和编码方式就有要求了，所以使用者要对每种存储方式有充分了解，才能如何选择好用何种存储来组建一个大型数据系统。现在还有OLTP和OLAP联合一起做的HTAP型的数据库，这是由Gartner公司发明的名词，指既可以用做联机数据处理，也可以用做线上数据分析的混合应用数据库架构。列式和行式存储相比计算机内存管理磁盘上的数据就要复杂的多了，因为计算机内存由操作系统帮助我们管理者。基于内存的编程也比在硬盘上编程要简单，而在磁盘上存储数据相比我们必须考虑如何组织数据和管理数据，数据序列化格式，怎么释放存储空间，怎么整理磁盘碎片等...这些非常复杂，但是操作系统为我们提供了更好文件系统结构，对应存储引擎开发者来说我们可以基于现有的文件系统来做开发。一个组数据在关系模型中会被存储为一张表，而表又是列和行组成的，字段是列和行的交集如图上的Job字段，例如某一列的字段有相同的属性和数据类型，如果我们定义一个用户名字段为字符串类型那么这一列都是相同类型；而多个列中的字段可以组成一行，也就是一条数据记录集合，当多个行记录集合就可以构建一个表。那问题哪来了？如何把这些表中记录存储在底层的磁盘中？用什么样的方式存储呢？在计算机硬盘上的数据布局是以快进行划分布局的，可以查看这篇文章Linux文件系统设计，磁盘在存储数据和访问数据都是按照固定大小快进行读写；如果把表中同一行的列数据存储在一块中，这样的存储方式就是行式存储，当访问整条记录时非常有用，但是如果是访问某个特定字段时就不那么理想因为其他字段数据也在被访问的数据块中。而面向列式存储就是把一列的相同类型数据存储在一数据块里，在这种存储布局下我们每次读取一数据块就相当于读取一类数据信息，这样使得面向一些数据分析场景性能不错，不需要去像行式存储那样需要把不符合条件的数据过滤掉，来回频繁读取多个数据块获取需要的列的内容；主要适合于批量数据处理和即时查询。相对应的是行式数据库，数据以行相关的存储体系架构进行空间分配，主要适合于小批量的数据处理，常用于联机事务型数据处理。面向列的存储方式好处还不只是访问方式的问题，如果你知道CPU读取数据工作原理的话，同类型的数据还能被连续的缓存起来，提高缓存利用率和计算利用率；另外相同的数据类型可以通过特定算法进行数据压缩，可以根据不同的数据类型采用不同压缩方法。列和行的存储方式区别，你可以想象成一个果篮，面向行的篮子里面装的水果都是很多种类，而面向列的篮子里的水果种类是就一类为同类，如果让快速取出5个为一类的水果，你觉得哪种方式最快？这就要取决于你访问方式了，换到数据库中就是你要读取一行数据所有列的集合那就使用行，而如果要跨多行并且要在某一列上做计算那还是推荐列存储。存储引擎存储引擎的索引分成两种版本，一种是日志结构化的引擎，另外一种就是面向页面的存储引擎，面向日志型的存储有Bitcask和而面向页面存储的有B-Tree。这里的日志是指的是数据写入的方式，传统我们打开一个文件要修改某个内容的时候就需要找到对应的行进行操作然后保存修改，而用日志的方式数据写入的方式采用追加的方式，这样我们就不需要磁盘寻到了，这些数据可能被程序通过特定的方式进行编码而存储的，对普通人类来说无法查看其内容的，要通过特定的方式进行编解码操作。日志只是每条数据存储记录的方式，而上层通过特定的API接口去访问数据时就要快速查找到那条记录，此时就要索引来配合了。但是索引是一种数据结构，每次索引变动都需要一次对应的数据结构变更，而变更是要付出性能代价的，不能像日志的方式直接追加，并且部分索引要在内存中维护，这是本篇要探讨的问题。关于我这里有最简单Bitcask引擎的设计例如你可以查看这篇文章：Bitcask Design Paper，这是最简单的日志结构模式的设计与实现。这种基于内存索引的实现局限还是蛮多的，索引在内存中是断电丢失的，并且内存大小有局限性也不能支持范围扫描，从头开始查找某一个指定的键，这是很费时间的。如果数据在内存的缓存里面还可以不用读取文件，如…