Leon Ding Blog - BigData
type: channel
link: https://blog.leonding.me/category/bigdata.html
description: 收录关于 BigData 技术
items: 基于日志结构文件系统的 NoSQL 实现
items: 分布式系统设计的挑战
items: 免费体验 Oracle Database 新特性
items: 日志结构在 InnoDB 应用
items: InnoDB 数据页的设计
items: 分布式数据分区设计
items: 数据库事务隔离性
items: 数据库为什么要有事务支持
items: 分布式数据同步复制
items: 存储数据格式编码

基于日志结构文件系统的 NoSQL 实现
type: item
date: 2024-12-22T23:45:00+08:00
description: NoSQL 的抽象数据库系统的核心作用在于高效地存储和管理数据，并支持从海量信息中快速检索所需记录。传统的 SQL 关系型数据库在数据规模持续增长的情况下，容易遭遇单机性能瓶颈。目前提升性能的主流做法多依赖垂直扩展增强单机硬件能力，而非水平扩展增加节点以线性扩容。由于其架构限制，关系型数据库在弹性扩展性方面存在障碍，难以充分适应多节点分布式环境。同时为满足 ACID 事务特性，这类系统在支持分布式事务处理时也面临一定的技术挑战。相比之下，NoSQL 数据库更适用于大规模数据存储和高吞吐、低延迟的应用场景。由于 NoSQL 不受固定数据结构和关系的限制，具有更灵活的扩展能力，并且支持节点的动态扩展，从而更好地适应分布式环境。无论是关系型 SQL 数据库还是 NoSQL 数据库，它们都有一个共同的核心组件存储引擎，这是数据库设计与实现的关键核心部分。目前数据库存储引擎可以分为两大类，一类是基于内存的 （In-Memory）实现，例如 Redis 和 Memcached 这类的将数据全部存储在内存中；另外一种则是类似于 MySQL 的 InnoDB 和 LevelDB 这种将数据存储在磁盘中。两者都有各自的优势和劣势，基于内存的存储引擎通常存储的数据量较少，但其访问速度往往比基于磁盘的存储引擎快好几个数量级，而基于磁盘的存储引擎访问速度较慢，但存储数据量要比内存引擎多几个数量级。通常内存的价格要比磁盘的贵很多，这也是在选择存储硬件时要考虑的成本问题。存储引擎的开发者需要权衡读写性能和硬件成本的同时，重点在于如何高效存储数据和检索数据。数据库的存储引擎相当于在操作系统文件系统之上，建立的一套用于数据组织、存储与查询的逻辑管理层，最终实现是依赖于操作系统的文件系统 API 接口的，最终数据是被持久化到磁盘文件中保存，这个就对应着 ACID 中的 Durability 数据持久性。持久性（Durability）是存储引擎最为至关重要的功能实现，目前很多基于内存版本的 NoSQL 数据库例如 Memcached 和 Redis 在这方面做的就比较差，在 NoSQL 服务器运行过程中突然崩溃断电就会导致数据没有被持久化存储的情况，从而导致数据丢失影响到上层的业务程序。[scode type="yellow"]所谓的持久性就是如何将计算机运算的结果持久化存储在一些物理介质中；持久性是指系统能够将状态变更的结果可靠保存，使其在系统重启或故障后仍然能够恢复。[/scode]存储是现代计算机最核心的功能之一，计算机中的程序和文件以数据块的形式保存在各种存储介质中，主要可分为持久化存储和非持久化存储两类。非持久化存储通常指内存（如 RAM），其特点是在断电或关机后，数据会被清除。尽管容量通常小于持久化存储，但其高速读写性能使其广泛应用于程序运行过程中的数据处理和临时缓存。相比之下，持久化存储如硬盘、固态硬盘、DVD、磁带等，能够在断电后保留数据，适用于长期保存程序和文件信息。鉴于数据可能存储在多种物理介质（如磁盘、SSD、光盘等）上，主流的操作系统需要提供一致的抽象接口，以屏蔽底层硬件差异，使用户能够高效、统一地进行文件的读写、创建与删除等操作。当前主流操作系统均实现了完整的文件系统，开发者可以通过如 POSIX 这样的标准 API 接口来访问和管理存储设备，而无需关心具体设备的物理实现方式。尽管操作系统的文件子系统通过标准如 POSIX 提供了对存储设备的访问能力但仍存在一些限制；例如存储性能受限于设备的物理读写速度，同时文件系统通常采用页缓存 Page Cache 或写缓冲机制，这意味着数据写入操作并不会立即持久化到磁盘，一旦系统发生故障或意外断电，可能会导致数据丢失，影响系统的可靠性。基础架构综上所述，针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现，存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录，存储引擎会将数据记录索引信息全部保存在内存中，从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据，并且还能减少读取磁盘索引所需要的时间，通过一次索引定位来读取数据记录，写入和查询流程图：其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ，在对数据执行任何操作前，都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色，也作为主要的数据存储载体。在数据库进程崩溃后，只需从 WAL 日志中顺序读取各条 Segment 记录，即可高效恢复内存中的索引结构。在 UrnaDB 中对这些 WAL 数据文件有一个统称为…

分布式系统设计的挑战
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 采用两种分…

免费体验 Oracle Database 新特性
type: item
date: 2023-03-19T16:35:00+08:00
description: Oracle 数据库目前是世界最为领先的数据库，也是很多有钱的企业使用的数据库，与开源数据库不一样的是 Oracle 数据库针对企业用户是收费，而且费用也不是一些小公司能承受的起的，和 SQL Server 一样针对是企业级应用市场。我第一次接触 Oracle 数据库是 2017 年的时候，培训使用过 Oracle 课程，但是目前我用的最多还是 MySQL 数据库，因为它是开源和免费的，商业场景使用没有法律顾虑，而 Oracle 针对教育使用是免费的，如果商业使用那么就要付费。最近在闲逛的时候发现 Oracle 有一款 Oracle Database Express Edition ，有核心数 2 颗和内存大小 2G 限制和磁盘数据大小 12G 限制，适用于小型应用程序和我这种想体验 Oracle 最新特性的开发人员。准备工作Oracle XE RPM 安装包去 Oracle 官方网站下载 Oracle DataBase Express Edition 选择对应版本，我使用的 Linux 版本和 AMD 架构 CPU ，也可以在通过 SFTP 上传下载好的 rpm 包到服务器中，如果服务器在没有连接到外网的情况下。我目前使用的 DigitalOcean 所提供的 IaaS 服务，他们家没有提供 rhel8 的 ISO 文件需要自己去 rhel 控制中创建 .vmdk 文件上传到 DO 中，目前 Oracle XE 是原生支持 rhel8 和 oel8 的系统，其他发行版本 Linux 安装过程中会提示 yum repo 仓库密钥错误或者一些莫名其妙的错误。[scode type="yellow"]由于 Oracle XE 要求硬件最低配置是 1G 内存和 12GB 磁盘，如果未达到要求安装过程中会提示安装失败，最好使用 2G 内存机器。[/scode][scode type="yellow"]如果使用的 RHEL 系统初始化完成之后，必须对系统进行注册和检查极好状态，可以运行下面的命令：[/scode]# 查看系统当其系统发行版本 cat /etc/redhat-release # 检查是否注册 subscription-manager identity # 注册系统 subscription-manager register # 检查订阅列表 subscription-manager list --available # 检查订阅状态 subscription-manager status没有特殊情况一般都会显示注册成功。安装 Oracle XE[scode type="lbule"]在安装 Oracle DataBase Express Edition 之前会需要检查依赖 libaio 软件包，因为 Oracle XE 版本采用了 AIO 模型，这个库 libaio 为异步 I/O 提供了 Linux 原生 API 被 Oracle XE 程序所采用。从这里就可以看出来 Oracle 数据库在这方面技术领先性，目前大部分网络和磁盘 IO 程序还是用的 NIO 或者 BIO 的方式。[/scode]# 检查依赖是否安装 rpm -qa | grep libaio # 如果没有就安装，并且安装一些依赖软件 yum install -y libaio vim wget # 安装一些开发工具依赖 yum -y groupinstall "Development Tools" 安装过程中可能会提示没有安装 compat-openssl10 依赖包，这是需要手动下载并且安装到系统中：# 下载依赖 wget https://repo.almalinux.org/almalinux/8/AppStream/x86_64/os/Packages/compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm # 安装依赖 yum install -y compat-openssl10-1.0.2o-4.el8_6.x86_64.rpm[scode type="yellow"]Oracle XE 相比其他免费的数据库安装起来要复杂，其他开源数据库大部分不用考虑主机名和主机 IP 映射关系，在安装之前一定要保持主机名和 IP 地址是对应的，否则后面安装会提示 No valid IP Address returned for the host，需要自己手动更改 hostname 和 hosts 文件中的记录。[/scode]接下来下载对应的 Oracle EX 版本的软件安装包，分别为 oracle-database-preinstall-21c 和 oracle-database-xe-21c 软件包，这是 Oracle …

日志结构在 InnoDB 应用
type: item
date: 2022-11-22T11:34:00+08:00
description: 在前面的分析 MySQL 的存储引擎相关的文章中，已经明确知道了在数据记录在以 16KB 的页进存储的，当操作 SQL 时数据库会针对数据所在的页面进行增删改查操作，读页面、写页面、创建新页面。在数据库执行事务的过程中会将所需要的数据页读取到内存中的 Buffer Pool 中进行操作，这样就使得内存中对事务进行隔离运算，当完成了数据更新操作就会进行刷盘持久化，这是一个完整的事务生命周期。当在内存中 Buffer Pool 的修改页面时，突发发生断电情况，那么内存中的数据就会丢失，如果数据未提交持久化就发生丢失情况，如何保证数据不丢失？最简单的办法记录日志，将在内存中对数据产生修改的页面信息记录到一个日志中，通过日志的方式好处是不需要对整个页面进行刷盘操作产生的随机 IO 性能问题，日志文件系统都是以 Append Only 方式进行的。REDO LOG为了防止在执行事务过程中程序突然崩溃导致内存数据丢失，MySQL 的设计者在引入 Redo 日志的设计，即使程序崩溃也能通过 Redo 日志里面信息恢复数据。Redo Log 相比传统数据页占用的内存空间非常小，只会存储表空间的 ID 和 页面号、和具体修改的数据偏移量，和被修改数据的新值，Redo Log 和之前博客中介绍的 Bitcask 存储引擎很相似。一条通用的 Redo Log 日志结构如下，Type 日志类型、Space ID 表空间、Page Number 页号、Data 真实的数据，至于 Len 和 offset 针对的是一些特殊类型的日志记录的附加信息：在 Redo Log 中的 Type 字段的类型有 53 种，日志数据的类型是根据页面中写入的数据类型进行划分的，整理一个常见的类型表格：类 型作用说明MLOG_1BYTE对应十进制数字为 1 表示在偏移量处写 1 字节MLOG_2BYTE对应十进制数字为 2 表示在偏移量处写 2 字节MLOG_4BYTE对应十进制数字为 4 表示在偏移量处写 4 字节MLOG_8BYTE对应十进制数字为 8 表示在偏移量处写 8 字节MLOG_WRITE_STRING对应十进制数字为 30 表示在偏移量处写入一个字节序列设计这么多的 Type 类型是为了节省空间，能劲量不要使用 offset 和 len 字段时就劲量不要使用，提高空间利用率积少成多，其 Type 字段类型远远不止本文中列出来的，能节省一个字节是一个字节。在 Redo Log 日志里面不仅仅有物理日志，还有一部分的逻辑日志，逻辑日志针对事务操作的类型日志，Type 类型是数据操作的动作，具体其他类型可以查看其他资料。在 MySQL 默认的存储引擎 Redo Log 大小可以通过命令进行查看，默认是 16MB 被存储引擎提前申请好存储在 /var/lib/mysql 目录中：SHOW VARIABLES LIKE 'innodb_log_buffer_size';当一个事务执行时 Redo Log 的工作原理图为下图，先是解析 SQL 语句，生成执行器然后去执行器里面执行加载需要的数据页，然后开始记录事务操作逻辑日志信息：正如上图 Redo Log 作为文件存储在磁盘上，那么就需要将内存中的日志操作记录刷入到磁盘文件里面，因为数据库是用户态下的程序所以他文件写入依赖于操作系统的文件缓存的刷入频率，Innodb 提供多种刷盘策略，默认 MySQL 的刷盘策略可以通过下面命令查看：SHOW VARIABLES LINK 'innodb_flush_log_at_trx_commit'分有 3 种策略，默认值为 1 下图：目前已 3 种值类型操作，分别为下面列表中：值类型作用说明0每次事务提交时不进行强行执行刷入磁盘，而是根据后台的线程进行的1每次事务提交时都会执行一次刷入磁盘的操作2每次事务提交时只把内存缓冲区的数据刷入到系统的 Page Cache 中依赖于系统在日志中还有一种特别的类型为 Mini-Transaction 类型，它是指一次事务对 Innodb 底层数据页的一次原子访问的过程。也就是说在一张有索引表中插入一条记录，那么聚族索引 B+ 树的数据页也会插入这条记录的索引信息，这个过程就称为对底层页的一次原子访问，如果这个过程中运行到一半就宕机了，如果 Redo Log 日志只记录一部分日志不是完整的，下次恢复数据就不能恢复完整的数据记录，所以这里的存储引擎设计者设计一组 Redo Log 要么把全部的日志都恢复掉，要么就一条也不恢复，结构如下图：在 Redo Log Buffer 中的多个 Mini-Transaction 可以组成一个 Log Block 块，多而个 Log Block 又可以组成 Redo Log Buffer，而负责存储 Mini…

InnoDB 数据页的设计
type: item
date: 2022-10-06T15:09:00+08:00
description: 数据库系统核心就是存储引擎，前面的文章我写过日志类型的LSM-Tree存储引擎，本篇文章将介绍基于B+树的 InnoDB 存储引擎实现细节，存储引擎一般都是有不同专业领域人员开发的，目前在MySQL5.7之后使用的默认存储引擎就为 InnoDB ，下面为笔者整理的 InnoDB 引擎相关的实现细节笔记总结。配置设计MySQL 是如何对配置选项进行设计的？设置这个可能大部分软件都有手机上大部分是 GUI 图形化设置界面。而大部分的基础软件或者运行在 Linux 平台上的软件都是通过命令行或者指定的配置文件进行设置的，MySQL 作为一个数据库基础软件也提供很多种配置参数的方式，分别为命令行参数、环境变量、配置文件。权限最高的就是命令行方式，如果在启动数据库的时候再命令行上传入参数那么可以覆盖其他方式配置的环境参数，例如下面的：mysqld --skip-networking # 等价于 mysqld --skip_networking上面这种方式通过做参数标记，这种没有具体的参数值，而另外一种命令行的方式就是有默认值可以通过命令行修改其值的，方式如下：# 修改默认的存储引擎 mysqld --default-storage-engine=MyISAM如果是传入参数值时不容许有空格必须用等号划分，多个参数值可以通过空格分割传递起来；上面这些都为长参数配置选项，还有一种就是 MySQL 客户端所使用的默认短参数，主要短参数是区分大小写的，例如 -p 代表密码，而 -P 表示的是端口号，如下图：mysql -h127.0.01 -uroot -p Enter password:上面的方式每次都要通过命令行参数传入其配置参数值，如果要配置很多的话需要写多个参数集合，这使得很多的参数需要一起写到命令行中并且每次都要这么传入，非常麻烦，但是命令行在所有配置方式中命令行上传入的参数优先级最高。另外一种方式是通过配置文件的方式设置其参数值，采用的是 ini 的配置文件格式，例如上面的命令行方式配置的参数可以编写配置文件的格式：[client] port=3306 [mysql] default-character-set=utf-8 [mysqld] # mysql服务端默认监听(listen on)的TCP/IP端口 port=3306 # mysql数据库文件所在目录 datadir="xxxx/Data" # 服务端使用的字符集,默认为Latin，安装时一般选择utf-8 character-set-server=utf-8 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB 像上面这种方式称之为配置组的方式，每个组的都有自己对应的配置值，而组名大部分是对应的bin下面的可执行二进制名称，同一个配置文件中的配置出现多次，将以出现在最后一组的参数值为准；如果是命令行传入参数那么最终使用的是命令行参数，命令行参数级别最高；当然部分配置可以在使用 DDL 语言进行修改操作，例如数据库的字符集和表使用的默认存储引擎。MySQL的可配置信息远不止这么多，这些配置参数一部分是系统变量的成员，可以使用 SHOW VARIABLES 来查看相关的系统变量信息，设置系统环境变量的方式就是上面我所整理的命令行的方式和配置文件的方式，当数据库启动的时候这些被设置的参数就会被设置为其对应的值，如果没有配置则有默认的值。对应某些环境变量在程序运行的是无法动态修改的，且部分变量例如存储引擎都有各自作用域，例如字符串和字符串排序规则有数据级别、表级别、行级别，这时设计是因为可以合理的使用存储空间提高数据库性能。另外一些变量分为 GLOBAL 和 SESSION 级别的变量，服务器启动的时候会为分配初始化一些全局的变量，而会话级别则是针对于当前连接到服务器的某个客户端的环境变量设置，例如 max_connections 为全局变量，而 insert_id 客户端表示自增 ID 的初始化启始值，还有 default_storage_engine 则为服务器端和会话端共有的值。存储单位在 InnoDB 中数据表中的每条数据记录采用页存储，以页作为存储磁盘上的基本单位，默认的页大小为 16KB ，如果需要处理数据记录也就是要把页从磁盘中加载到内存中再操作，一次性批量操作 16KB 大小的页。当然这个页面大小页可以通过 innodb_page_size 系统环境来设置，当数据库启动之后这个值就不能被再次修改，运行过程中是不可变的系统变量。InnoDB 一些设计细节运用一些巧妙的设计来提高空间利用率，官方将存储数据的记录的页称之为 INDEX PAGE ，下面就为页布局图：这里主要关注的是 User Records 部分，数据库里面的记…

分布式数据分区设计
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…