分布式系统对于刚刚入门作研发的程序员还是接触不多，刚刚入行的大部分程序员做的最多工作还是开发单体应用和简单业务操作，遇到最多问题估计也是本地线程并发数据安全问题，和数据状态一致性问题。而往后发展最多把单体应用部署为 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 采用两种分布式算法解决的，Raft 和 Paxos 算法。[scode type="yellow"]Paxos 和 Raft 这两种协议的目标都是在分布式系统中实现一致性，保证分布式系统中的各个节点的数据一致性。这里 Raft 算法在工业级使用的是最多的，它采用领导者选举的方式，保证系统中的多个节点之间的数据一致性。相对于 Paxos 协议，Raft 协议的实现较为简单，而且容错性更好。[/scode][scode type="green"]如何让多个分区中的节点数据保存一致？在不同时间下操作的客户端如何保证一致？时间相对于客户端来说对于自己是相对的，而对于服务器来说是要面临多个客户端时间线的，整个世界都是异步的，如何多个分区保证数据一致性？分区出现了脑裂怎么办？在 Raft 协议中把这些抽象成了 3 个部分，第一个数据采用日志的形式同步，第二个多台服务器之间传输数据听谁的？Raft 采用的是选举机制，从多个节点中选择一个领导节点，第三个容错机制采用多节点之间心跳机制来保证，和我之前设计的 web session 那篇文章的超时机制类似。[/scode]在 Raft 算法通过日志复制来保证系统中各个节点之间的数据一致性，领导者会向其他节点发送日志条目，其他节点会将这些日志条目添加到自己的日志中，并向领导者发送确认消息，当所有节点将日志条目持久化并且发送确认消息之后则认为这条数据状态一致性得到保持，至于实现细节可以查看文末，我列出的 Raft 协议相关文章。不可抗拒因素[scode type="yellow"]分布式系统产生脑裂的原因最多原因就是网络问题，网络层的 IP 协议并不能保证网络数据包传输过程中保证数据包正常无误的传输，会出现丢包、数据包网络延迟、重新发送数据包，这些问题在 TCP 协议得到解决，包括数据包顺序问题也得到解决，但是 TCP 不能消除网络延迟问题。延迟是基于时间的东西，这是不可能得到解决，除非有时间机器，分布式系统脑裂出现原因一部分也是因为数据包延迟达到而产生了，或者请求响应问题产生的。能让程序产生延迟现象的原因有很多，下面 3 种都是归根结底是时间带来的问题：[/scode]网络分区垃圾回收基于时间对于分布式系统网络故障，目前最常见的解决方法为负载均衡、主从复制，这些典型案例为 web 应用服务器通过 nginx 做负载均衡，一台服务器出现了故障之后另外一台服务器还能正常工作；主从架构典型应用为 Redis 哨兵模式和 Linux 网络中所有的多网卡绑定设计，都是用来解决网络故障引起系统故障。[scode type="yellow"]解决上面这种网络故障常见，最常用的办法为心跳检查，轮询的方式检查健康，但是别忘了检测服务器和故障服务器之间也是通过网络连接，这样网络分区问题又回归到本身。[/scode]在开发单体应用的 web 的时候会对 http 请求延迟做测试，如果延迟过长客户端会无法加载出来数据，而这些问题也是数据包在网络中传输延迟导致的。同理在分布式中检查故障的手段也是基于时间和超时控制的，如何设置超时时间窗口又带来一个新的问题？设置过短导致网络频繁，设置过长又导致出现了故障不能立马检查到。[scode type="yellow"]导致相应延长原因也有很多，网络数据包链路延迟，或者服务器程序进程崩溃，进程处理数据堆积压力过大，特别是服务进程已经处于高负荷状态，导致无响应，检测端如何应对这种情况？网络数据拥堵问题和现实生活中道路拥堵一样，一段路程原本能在 d 时间内跑完，而出现拥堵情况则是不确定的，出现网络拥堵情况有：[/scode]交换机故障程序高负载虚拟化环境 CPU 切换资源延迟基于有 GC 的语言出现 STW 情况[scode type="yellow"]并且 TCP 有数据包默认的超时机制来重新发送数据包，如果数据包在交换机中堆积，而 TCP 的超时机制会认为数据包已经丢失，导致数据包重新发送，这样以来本来网络就够拥堵的了，再重新发送数据包会更加堵塞，跟会加深网络的延迟影响。在公有云例如 AWS 中多个物理服务器公用一台交换机的情况，网络的不可抗拒因素又会提高。[/scode]假设现在不同考虑到网络可靠性问题了，数据包能在规定的时间内到达分区节点中，现在剩下的问题为服务器本身和应用程序本身的问题，不同的时区的分区节点如何做时间的协调工作，这种最为常见解决方法是通过时间戳的方式，例如两个客户端同时向分布式数据库写入一条数据，谁会优达到呢？这很难受，取决于服务端如何处理，例如服务器的程序正在做 GC 垃圾回收，导致整个进程出现了 STW 状态，当进程苏醒时时间就会存在偏差了。[scode type="yellow"]基于时间带来的分布式问题，这个研究方向太多了，而且时间戳也有 Long 值不能表示的那一天，这让我想起了，存在看到一个科幻的故事，说一个未来人从未来回到过去，寻找 IBM5100 机器，以此来解决计算机世界中的时间问题。目前分布式数据中心的数据库 Google 采用的 GPS 和原子钟的方式解决，时间同步的问题。[/scode]针对于进程内的 GC 导致程序停止问题，和磁盘 IO 读写导致程序阻塞问题影响的涉及时间偏差问题，目前解决方案是使用无 GC 编程语言，或者对 GC 参数做设置，例如 Java 最新版本采用 ZGC 设计方案，或者使用 Rust 这种特殊内存管理的编程语言进行编写应用，针对分布式常见可以采用滚动停机方法进行 GC 回收，当要进行垃圾回收时把执行任务节点从集群剔除，这也是一种常见的做法。[scode type="yellow"]如果你对实现性要求非常高的场景，例如汽车和飞机的各个传感器，对应时间响应要求要为试试的，那么有 GC 的编程语言就不是很好的选择了，可以使用 C 语言或者现在流行的 Rust 编程语言，RTOS 系统之类软件，例如汽车在发生碰撞的那一刻，安全气囊要即时打开，系统肯定不希望 GC 暂停导致无法及时释放安全气囊导致人生安全的事故。[/scode]不可靠因素太多，怎么确定集群中的数据分区问题？故障检查问题？单靠节点自身来检查是完全不够的，而且网络波动导致分区故障，在同一个地区服务器可能会检查到，其他地区服务器因为网络线路原因导致不能监测到，目前最好办法为法定人数来解决，多个节点对一个节点做仲裁，如果集群中的三分之二节点认为某个节点挂了，那么它就真的挂了，即使它进程正在做 GC 垃圾回收处于假死状态，也等同于从集群中移除废除它的工作职责，目前实现这些算法的常用的为 raft 协议和 quorum 协议，以法定人数来解决这些问题。其他资料Distributed Lock ManagerRaft 一致性算法Raft 协议生命周期变化过程造出时间机器，到底有没有可能？How to do distributed lockingStatic Compilation of Java Applications at Alibaba at Scale
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 Database Express Edition 所依赖的包，安装之下命令：# 下载软件包，必须提前安装 wget https://yum.oracle.com/repo/OracleLinux/OL8/appstream/x86_64/getPackage/oracle-database-preinstall-21c-1.0-1.el8.x86_64.rpm # 这里连接会超时，不是永久外链 wget https://download.oracle.com/otn-pub/otn_software/db-express/oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm[scode type="yellow"]下载速度完全取决于网络带宽和本地磁盘写入速度，下载完整执行 yum -y localinstall oracle-database-xe* 命令进行安装，安装过程中会进行事务测试时间可能需要 10 分钟左右，如果没有依赖包缺失问题都会安装成功。[/scode][root@orcale-xe ~]# yum -y localinstall oracle-database-xe-21c-1.0-1.ol8.x86_64.rpm Failed to set locale, defaulting to C.UTF-8 Last metadata expiration check: 0:04:28 ago on Mon Mar 20 12:25:20 2023. Dependencies resolved. ================================================================================================ Package Architecture Version Repository Size ================================================================================================ Installing: oracle-database-xe-21c x86_64 1.0-1 @commandline 2.2 G Transaction Summary ================================================================================================ Install 1 Package Total size: 2.2 G Installed size: 5.8 G Downloading Packages: Running transaction check Transaction check succeeded. Running transaction test Transaction test succeeded. Running transaction Preparing : 1/1 Running scriptlet: oracle-database-xe-21c-1.0-1.x86_64 1/1 Installing : oracle-database-xe-21c-1.0-1.x86_64 1/1 Running scriptlet: oracle-database-xe-21c-1.0-1.x86_64 1/1 [INFO] Executing post installation scripts... [INFO] Oracle home installed successfully and ready to be configured. To configure Oracle Database XE, optionally modify the parameters in '/etc/sysconfig/oracle-xe-21c.conf' and then execute '/etc/init.d/oracle-xe-21c configure' as root. Verifying : oracle-database-xe-21c-1.0-1.x86_64 1/1 Installed: oracle-database-xe-21c-1.0-1.x86_64 Complete![scode type="green"]当整个安装过程中执行完成之后，在尾部提示 Oracle XE 的配置文件路 /etc/sysconfig/oracle-xe-21c.conf ，最关键一步为设置 Oracle XE 管理员账号密码，执行 /etc/init.d/oracle-xe-18c configure 命令，也可以通过下面命令一键配置：[/scode](echo "password"; echo "password";) | /etc/init.d/oracle-xe-21c configure >> XEsilentinstall.log 2>&1至此初步的 Oracle XE 软件包安装和初始化完成。配置环境[scode type="yellow"]安装完成之后要配置整个 Oracle 数据库的用户环境，让在系统中随时随地生效和能被正确的使用，先切换到 oracle 用户，在用户根目录中的 ~/.bash_profile 文件中添加 Oracle 的安装目录：[/scode]# vim ~/.bash_profile # Oracle 安装目录 export ORACLE_HOME=/opt/oracle/product/21c/dbhomeXE # Oracle SID 是 System IDentifier 的缩写，启动之后实例名 export ORACLE_SID=XE # Oracle 的官方 bin 目录 export PATH=$PATH:/opt/oracle/product/21c/dbhomeXE/bin/ # 刷新环境变量 source ~/.bash_profile至此可以从 Linux CLI 执行 Oracle RDBMS 命令，执行 lsnrctl 命令检查 Oracle Listener 的状态，执行 sqlplus / as sysdba 即可连接到 Oracle 数据库 XE 实例，管理 Oracle XE 服务进程的命令如下：# 查看数据库状态 /etc/init.d/oracle-xe-21c status # 关闭数据库 /etc/init.d/oracle-xe-21c stop # 开启数据库 /etc/init.d/oracle-xe-21c start[scode type="yellow"]当配置设置完成之后如果需要远程网络访问必须允许数据库端口能被外部访问到，在防火墙中添加对应的端口，数据库连接为 1521 ，另外是 Oracle EM Express 端口 5500 ，Oracle EM Express 允许通过浏览器访问和管理 Oracle XE 数据库。[/scode]# 先安装 firewalld 管理工具 yum install -y firewalld # 设置 1521 5500 端口 systemctl start firewalld firewall-cmd --permanent --zone=public --add-port={1521,5500}/tcp firewall-cmd --reload[scode type="yellow"]如果需要远程通过 SQLDeveloper 连接到数据库，必须在服务器命令行 sqlplus 中执行 EXEC DBMS_XDB.SETLISTENERLOCALACCESS(FALSE); 允许远程登录功能，也可以通过浏览器查看 Oracle EM Express 界面。[/scode]总的来说在裸机操作系统之上通过 rpm 软件包安装 Oracle 要解决很多依赖和环境问题，如果图方便推荐使用 Docker 安装 Oracle 数据库，现在大环境好像不用 Docker 就很 LowB 一样，没有必要使用就不要强制使用，但具体取决于你的具体需求和应用场景，如果需要更高的性能或更大的控制权，则原生安装 Oracle 数据库可能更适合。其他资料Linux中的异步I/O模型New Droplet from Custom ImageInstall Oracle Database 21c XE on Rocky Linux 8Oracle Linux 9 java.lang.nullpointerexceptionINSTALLING ORACLE DATABASE 21C ON ORACLE LINUX 7
在前面的分析 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-Transaction 数据的是 Log Body，Log Body 可以存储多条 Mini-Transaction，每块 Log Block 中都有 buf_free 指向这里该位置之后就是空闲的区域，每当执行一个 MTR 过程中会产生多个 Redo 日志都不可以分割的组，属于同一组的日志才能归属为同一组的 MTR 所有，运行过程中 MTR 只能在内存中，直到 MTR 执行结束复制到对应的 Log Block 中持久化。在 Log Block 分为 3 个部分，分别为 Header 、 Body 、和 Trailer，其中 Header 最为数据源信息部分存储每个 Log Block 一些基本信息，而 Trailer 为防止数据丢失或者损坏查错使用的校验码。当 Log Buffer 刷入到磁盘上会有多个日志文件组保存，而这些文件组的数量是固定的，并且文件组的总体占用大小限制为 innodb_log_file_size * innodb_log_files_in_group ，当要写入日志时如果发现大小不够就会从头开始覆盖文件，多个文件会形成一个环路，这个稍作了解即可。Rode 磁盘日志文件格式对应着内存中的 Log Buffer 本质上也是一片连续的存储空间， 被划分成了多个 512 字节大小的 Block，当持久化时也就是将内存的缓冲区的 Block 刷写到磁盘上的日志文件中，整个磁盘上的日志文件可以看成是内存中的多个 512 字节的大小组成一个链表结构。多个日志文件组成一个日志文件组，每个大小和数据组织格式方式也是相同的，前 2048 也就是前 4 个 Block 对应存储着每个日志文件管理信息，从 2048 以后为真实的 Block 数据信息，多个文件日志文件形成了一个环，下图：日志文件最核心功能即为保证系统崩溃或者断电情况下，在数据库恢复正常时还能恢复数据，如果一个事务在执行完成之后内存中的 Buffer Pool 脏页面已经被刷入到磁盘中，则 Redo Log 中对应的 Block 数据块已经毫无存在的意义，那么其所占用空间可以被其他日志覆盖重用，多个日志文件组会形成一个环形以此达成存储空间复用。图上的 2 个日志文件组的分割线在代码实现层面是 2 个变量，分为 checkpoint 和 write_pos，checkpoint 之前的数据页已经被刷写到磁盘中， write_pos 之后的数据页为当前需要刷写的，checkpoint 和 write_pos 之间的数据页新数据页可以插入的位置。UNDO LOG上面的 Redo Log 是属于帮助数据库的事务提供 持久化 的保证一种方案，而 Undo Log 要为数据提供一种数据在某个时间段的数据 一致性 方案，Undo Log 的数据信息属于逻辑级别的，会记录一条数据在被修改前的数据方便事务进行回滚操作，数据库有 Undo Log 的支持才能在执行过程中可以有退回之前数据版本的选项。当像已经存在的某条数据进行修改操作时 Undo Log 就会针对该条数据进行记录相反的操作，例如 执行 DELETE 一条数据的事务，而 Undo Log 会生成一条对应的 INSERT 事务操作，方便后面回滚使用，核心规则有 4 条：在插入一条记录时，至少要吧这条记录的主键值记录下来，如果发生回滚操作可以删除对应值的主键记录。在删除一条数据记录时会记录当前记录数据，备份当前记录的数据到日志中方便后面回滚恢复数据，重新插入到表中。在修改一条记录时，至少要把被更新记录的值记录下来，方便后面回滚到旧值的版本。在执行 SELECT 事务如果是只读事务则不需要进行任何操作，但是 MVCC 模式下可以通过旧值提高读取。在 MySQL 中实现可重复读级别隔离的事务时，大部分数据库采用的 MVCC 多版本并发控制，而实现 MVCC 最主要的基础就是 Undo Log 来实现的，当一个事务读取一行记录时若改记录已经被其他事务占用，则当前事务可以读存储在 Undo Log 中的之前的行记录版本，从而事务非锁定并行多事务读取行记录。BIN LOGBinLog 相比上面的 Redo Log 和 Undo Log 两种日志完全不同，BinLog 是 MySQL Sever 层维护的一种二进制日志，可以帮助多台主从架构的 MySQL Server 能正常复制表数据记录，其主要是用来记录对 MySQL 数据更新或潜在发生更新的 SQL 语句，并以事务的形式保存在磁盘中。它记录了所有的 DDL 和 DML 语句，除了数据查询语句 SELETE 、SHOW 等语句，以事件形式记录，还包含语句所执行的消耗的时间，MySQL 的二进制日志是事务安全型的，主从架构下可以使用 Master 把它的二进制日志传递给 Slave 并回放来达到 Master 节点和 Slave 节点数据一致的目的。默认情况下数据库服务器会自动开启 BinLog 功能，相关的设置可以在配置文件中编写，可以配置参数有很多，一般默认情况下的配置：[mysqld] # 二进制日志的存储目录 log-bin=/home/mysql/binlog/ # 高版本MySQL需要server-id这个参数，提供一个集群中不重复的id值即可 server-id=1 # mysql-bin.*日志文件最大字节（单位：字节） # 设置最大100MB max_binlog_size=104857600 # 设置了只保留7天BINLOG（单位：天） expire_logs_days = 7 # binlog日志只记录指定库的更新 # binlog-do-db=db_name # binlog日志不记录指定库的更新 # binlog-ignore-db=db_name # 写缓冲多少次，刷一次磁盘，默认0 sync_binlog=0查看 MySQL 有没有开启使用下面命令即可查询相关的参数信息：SHOW VARIABLES LINK `%log_bin%`;如果开启 BinLog 后相关的日志文件会在数据目录中，可以使用下面命令查看数据目录文件列表：SHOW BINARY LOGS; 如果开启的主从架构相关的可以使用下面命令查看日志的状态信息：SHOW MASTER STATUS;默认 MySQL 生成的 BinLog 是采用的 ROW 模式，这里的复制模式和之前我技术文章中的分布式数据复制中讲解的概念很类似，在 MySQL 中提供了 3 中模式：STATEMENT：基于SQL语句的复制（statement-based replication, SBR）ROW：基于行的复制（row-based replication, RBR）MIXED：混合模式复制（mixed-based replication, MBR）在 MySQL 5.7.7 版本之后的默认都是采用 ROW 模式进行的，如果单纯基于 SQL 语句的复制会出现一些问题，不能使用一些特殊的 SQL 内置函数，这个问题之前的文章中我也讲过，日志格式通过 binlog-format 进行设置。其他资料Redo Log 的刷盘策略与过程剖析InnoDB 启动选项和系统变量列表InnoDB 的 Redo Log 分析『浅入浅出』MySQL 和 InnoDBMySQL 是怎么运行的Undo Log 的概述与写入过程MySQL · 引擎特性 · 庖丁解InnoDB之UNDO LOG
数据库系统核心就是存储引擎，前面的文章我写过日志类型的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 部分，数据库里面的记录都是存储在 User Records 里面，而 User Records 是从Free Space 里面申请的。Free Space 会在页面初始化时创建完成其实并没有 User Records 部分，当数据库开始工作的时候会从 Free Space 申请，当把整个 Free Space 使用完成之后等于整个页面被使用完，当使用完成之后就会申请新的页面。数据记录平时我们使用 INSERT 语句传入的数据记录在 InnoDB 底层存储也是一条数据记录，也称之为行格式， InnoDB 的作者在设计的时候提供了4种数据记录存储格式分别为：CompactRedundantDynamicCompressed其中 Redundant 是以前使用的旧格式，为了兼容性还一直保留；自 MySQL 5.1 开始，默认的行记录格式为 Compact，而从 MySQL 5.7 开始，默认的行记录格式为 Dynamic，使用 SHOW TABLE STATUS LIKE 'table_name' 可以查看指定表的行格式。目前最常用的还是 Compact 和 Dynamic 的方式进行存储的，这取决于所使用的数据库版本。在创建表或者创建列记录时可以使用 ROW_FORMAT=COMPACT 关键字来指定对应格式，如果默认没有指定存储格式会使用 Compact 格式，Compact 格式如下图：页中的数据是按行进行存放的，每页中存放的行记录最少为 2 行，最多为 16 KB / 2 - 200 行，也就是 7992 行。所谓的行就是上面的 Compact 数据格式，Compact 行记录格式是在 MySQL 5.0 引入的，设计目标是高效的存储数据，一个页中存放的行数据越多，其性能就越高。变长字段长度列表里有 3 个重要的参数为 M、W、L ，这3个参数分别代表着：M 最大占用字符数， W 一个字符集里面一个字符占用的字节数，L 可变长字段实际存储的数据字节长度。变长字段长度列表是存储一些不定长的字符串类型，例如 VARCHAR(M)、VARBINARY(M) 各种 TEXT 类型，例如 BLOB 类型的二进制格式，因为是这些类型不定长所以在存储记录会将其字节数也存起来。其中的 M 表示某一个字符集表示一个字符最多需要的字节数，例如 UTF-8mb3 中 W 就等于 3，例如 VARCHAR(M) 来说 M 则表示最多能存储 M 个字符，可以通过 SHOW CHARSET 命令查看对应的 Maxlen 的值；如果变长字节数占用的比较多，可能就要使用 2 个字节来描述其占用大小，至于用多少有一套默认的算法：如果 M * W <= 255 ，那么使用 1 个字节来表示长度如果 M * W > 255，并且 L <= 127，那么使用 1 个字节来表示长度如果 M * W > 255，并且 L > 127，那么使用 2 个字节来表示长度在 Compact 格式下的变长字段只允许存储非 NULL 的列的值，如果一个列中有 NULL 值都会放到一个统一的地方管理，首先会统计行记录中允许存储为 NULL 的列表有哪些（主键列以及使用 NOT NULL 修饰的列都不满足条件），则其他满足 NULL 条件的列被映射成一个二进制位图，如果二进制位上为 1 时代表该列的值为 NULL，存放顺序为小端逆序排列。其记录头信息都是一些特殊的标记二进制位，这里不经常使用并且维护者是存储引擎自己，主要看列值存储部分，且列值的头3个值为每条记录一些附加信息，分别为 ROW_ID、TRX_ID、ROLL_POINTER，这3个字段是存储引擎自动生成的，ROW_ID 为一个隐藏的主键，如果在定义列的时候没有声明主键或者 UNIQUE 键就会默认添加一个 ROW_ID作为主键，而 TRX_ID 为事务处理的唯一标识，ROLL_POINTER 则为回滚的指针。Redundant 格式是MySQL 5.0之前使用的格式，其最大差异就是没有变长字段长度，这就意味着会通过占用字节数来通过偏移量访问列值，主要的特点就是通过两个相邻的偏移量的差值来计算各个列的长度；也没有 NULL 专属列，而是将每个列的第一个二进制位来判断，如果是 1 则该列为空，如果真是数据长度大于 127 就采用 2 个字节来表示一个列对应的偏移量。 Dynamic 和 Compressed 行格式是在MySQL 5.7之后所使用的行格式，和 Compact 格式很相近，只是在出来溢出数据记录的时候方式不一样。Compact 和 Redundant 在处理溢出数据记录的时候就会有所差异，例如如果定义一个列为 VARCHAR(65532) 此时就会发数据记录溢出情况，上面说过 InnoDB 采用的页存储数据记录一个页大小为 16KB 也就是 16384 字节，而 65532 字节显然超过这个范围就会发生溢出情况。在 Compact 和 Redundant 中如果发生这种情况会，就会只存储 768 字节的数据，而另外 20 字节存储指向其他页的地址信息。Dynamic 和 Compressed 行格式不会存储 768 字节的数据，而是直接将其溢出的数据存储在多个溢出页中，方便通过压缩算法压缩空间。其他资料MySQL configuration documentThe physical structure of InnoDB index pagesInnoDB 行记录格式
本篇为分布式系统数据分区设计章节总结，当一个数据系统存储的数据单位达到TB级别往往都会采用多个服务器机架存储；面对海量的数据集合和非常高的查询请求的压力，往往都会把这些压力分摊到多个数据分片中处理。很多数据库也支持数据分区，可以把分区看成电脑上分区不同的盘管理着不同的资料文件。一个大型的数据库可以拆分成多个小数据库，和上面篇讲解的数据按地理位置分布效果类似，分而治之。数据分区主要的目的就是为了数据做冗余存储和异地多活，防止因为一个数据节点故障导致不能正常访问数据，另外可以提高整体系统查询数据吞吐量，将查询请求路由到不同数据分片；设计数据分区主要的问题是如何进行数据分区？查询请求如何定位到数据所在的分区？数据如何均匀的分布？热点数据如何处理？数据分区数据复制和分区通常一起使用，把整体数据系统分成多个数据分区，每个分区节点都有分区的副本，每条记录属于特定的分区，而同样的数据内有在不同分区存在副本，这样就可以提高整体系统可容错性。分区主要设计目的就是为了将数据分散起来存储，并且能通过分区来负载查询请求压力，理论上如果有 10 个节点，那么相比单节点就可以提供 10 倍的读写吞吐量，也提高 10 倍的系统容错性，如下图：像上图这样将数据分区到不同节点上做冗余存储，这样可以提高整体系统容错性，当某一个分区节点故障时，其他节点也能正常通过服务，但是这种模式会出现热点数据倾斜问题。数据倾斜，在极端情况下会导致分区效率严重下降，例如如果有 10 节点，一个热点数据节点，那么剩下的9个节点就为空闲状态，系统瓶颈就在最忙碌节点之上也会成为整个系统的热点。避免热点数据方法就是将其数据记录分区将其分布到多个分区中，这就会有另外一个问题如何的访问这些数据？如何查找到需要的那条数据？这时就要设计一个数据分片规则来存储数据记录，将其分布到不同的节点中保存？目前数据库相关的存储设计是简单的键和值数据模型，可以通过键计算哈希值，通过特定的算法将其分布到节点，这可以查看我之前写的一篇文章一致性哈希数据分片算法。但是哈希算法缺点不支持范围扫描，只能通过特定的哈希值来取决于具体存放位置，如果算法存在某些问题或者对应键计算处理的值不能满足均匀分布一样会带来另外一个后果数据分片不均匀。例如图书馆书籍按照26个字母进行排列分区，某些书架的区间比较大那么存储的书籍就越多，产生了分片不均匀的情况，这时就要设计一个合理的数据分界条件。例如物联网传感器设备采集的数据，如果按照键存储分区可能会出现分布不均匀情况出现热点数据情况，但是可以按照时间段分区处理，而基于时间戳则可以对应一个时间范围来做，例如将每天的数据存储在为一个分区，我们可以通过键和时间戳一起组合使用来达到区间访问的效果，这叫做复合键。二级索引分区上面介绍分区的方式是通过记录的键进行的分区，还有一种就是通过记录类型进行分区和特定的属性进行分区。例如有一个二手车交易网站存储不同品牌的汽车的记录数据，这时平台查询要通过根据特征的查询操作，这时就可以通过属性进行分区和建立二级索引的方式。可以基于唯一的文档 ID 进行分区，ID 的范围分区，0 至 599 存放在 0 号区域其他以此类推，这时针对这些区域存储数据记录进行构建二级索引，可以是汽车的颜色和车龄等...这种方式的分区要求每个分区自己能针对所存储数记录的属性建立维护一个二级索引，并且每个分区都是独立维护自己的二级索引，索引数据来自本分区；其缺点也很明显，每次查询特定颜色的汽车时，要查询多个分区，并且最后汇总数据，如果是串行的方式很有可能出现尾部响应问题。二级索引的另一种实现将其设置为全局二级索引，每个分区不需要维护本地分区索引，全局的索引方式也可以分区例如通过汽车的颜色进行分区，将颜色为红色的汽车收录到在一个索引color:red词条中，索引本身也分区将其按照 26 个小写字母排列顺序分区，这种分区称之为词条分区，这种方式不需要再查询某个特定颜色的汽车时不需要搜索全局的分区，查找符合条件的数据；但是缺点明显是当写入一个新的数据记录要跨区操作涉及到分布式事务问题，索引的数据要更新速度会受影响。动态分区分区原则是在数据库创建时就要设计好其数量，这样就可以应对未来的容量需求，但是如果设计的太多会导致一部分节点空着没有充分利用。分区的实际大小应该与集群中的数据总量成正比，小了可以缩容，大了可以扩容，数据的数量肯定是不确定的这就给设计带来一个问题如何设计扩容的策略？ 上面都根据键进行的分区策略，边界条件没有设置好可能会出现所有数据出现在少数几个分区里，而大部分分区却没有数据，导致数据倾斜到某几个分区节点上。业界一些有名的存储项目例如 HBASE 采用的动态分区策略，当数据超过某个阀值时进行拆分，当数据被删除时就自动调整合并一个分区，这和 LSM-Tree 的按大小策略扩容很相似，与 B-Tree 树的分裂操作很接近。动态分区的一个优点就是分区数量可以自动适配数据的数量，如果数据少可以使用少量的分区节点，降低系统开销，如果数据多就添加节点分区。 MogngoDB 的做法是预先分区，在初始化的时候预先分区； Cassandra 则在节点维护一个最大值和最小值的策略，来维护分区的个数，让分区的数量和数据记录数量成正比，采用的分区策略也是上文提到过的一致性哈希算法，如下图：不管什么样的分区策略都为了面对数据量增长和减少能让分区更好应对，动态分区在调节的时候数据记录会发生一些迁移活动，会产生短暂性能开销，所以好的分区策略会降低这些问题，可以设计一个自动检测算法和自动故障调节算法来自动完成这些操作是最理想，但是因为网络因素可能还会出现一些问题，这时就需要引入管理员手动配置才是最可取的。数据查询因为数据记录被分区存储在不同的节点之上，又会带来一个问题是如何知道需要查询的数据在集群中第几个分区里？当客户端连接时该连接到那个节点获取数据？而且分区可能在运行过程中出现了分区调整，会动态扩缩容分区节点，这时该如何面对？肯定大家的了解过微服务中的服务发现通过注册中心来解决，查询到相应的 IP 和端口然后继续读取数据完成查询操作，这种方式相比上一篇讲解的 Amazon Dynamo 采用多节点读取的方式，来说直接读取数据所存储分区节点，而不是批量读取多个节点。目前主流做法就通过中间层、或数据源信息共享机制、客户端路由表形式来做，各种方式的对比图如下：由于分区节点通过网络连接那么就要依赖着网络，这时分区的元数据信息就要依靠着分布式的协议来做数据信息交换，例如使用ZooKeeper来协调分区元数据信息，当有节点动态删除或者添加时就要通知路由层处理新状态；这里还可以使用其他一些协议gossip这种最终一致性协议，这是 Redis 集群模式默认使用的数据分区协调协议，下图为基于 Zookeeper 的方式架构图：小 结说话天下大势，分久必合，合久必分！这句话在分布式系统上体现的淋漓尽致，将大规模的数据集划分成小集合来管理，将数据均匀得分布在不同分区节点中来降低热点数据问题，每个分区能独立运行并且还能按照地里位置进行分区，且能提高系统整体容错性和可用性。但就因为分区节点又会带来一个问题就是如何保证节点之间通过网络连接状态的一致性，这方面可以阅读后面更新的分布式事务的文章。其他资料Zookeeper：分布式系统入门到实战《Designing Data-Intensive Applications》ch 6—Partitioning
事务作为每个数据库提供的一层高度抽象机制，保障着数据的安全性和一致性和持久化，这时每个数据库必备的功能；只是不同类型的数据库实现可能存在某种差异化，有了事务的保障应用层程序才能简单处理一些复杂的业务逻辑，如果没有数据库的事务保障那么对于一个数据库来说这时一个很难解决的问题，程序要面对进程崩溃和网络中断，并发数据竞争问题。最重要的是数据库要面对多个客户端连接的操作，此时客户端完全在异步环境下操作的，时间是固定的，但是每个客户端操作事务可能是异步进行，当在某个时间段内处理多个事务就会面对其并行事务问题，让异步的操作变成同步操作。本篇文章对几个场景的事务处理方式举例，表明事务对于这些场景的重要性。读提交事务例如一个计数器场景用户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 所操作的修改也不可见，不管是否成功提交事务。除此之外的其他所有操作的写入的数据查询可见。快照隔离级别还有另外一种实现为通过索引快照隔离，将索引指向多个不同的数据版本，然后想办法过滤掉对当前事务不可见的那些版本，后台的垃圾回收进程决定删除那些旧对象，只保留最新的数据版本，这实现可以查看之前我这篇文章 Wisckey索引的方式。这种实现方式类似于 OS 内存管理所采用的写时复制技术一样，采用一种追加的方式当需要更新数据时，不需要更新现有的页面而是创建一个新的修改副本，让索引指向新的数据页面，这种在 B-Tree 索引的数据库中实现比较多。写偏斜和幻读快照隔离级别在多对象事务更新操作场景下可能会出现写偏斜，通常不同的事务可能更新不同的对象，则可能发生写倾斜，而不同的事务如果更新的同一个对象则可能发生脏写和更新失败情况。常见的例子就是医生换岗的场景，不管在什么时候医生换岗的时候必须前提是还有一个医生在岗位上，都要同时去操作自己排班表记录，如果是同时去查询这个条件可能就发生幻读和写偏斜情况。这种场景就是因为是在数据库中操作，而如果是在现实中肯定是两个医生口头上协商才能到达换班目的，但是如果是在数据库中，那么整个操作都是异步进行的，那么协商条件依赖于数据库的隔离级别实现，这种操作是完全不可靠的，针对这种问题的最为有效的解决办法就是显示加锁串行化事务。另外一种场景就是注册用户名不能重复被使用，这时就需要通过采用唯一性约束来解决这个问题，如果其中一个事务违反约束就会终止创建相同的用户名。实体化冲突解决方法，例如开发一个酒店房间预定系统，可以提前建立一张时间表，在事务执行操作的时候先检测对应表实体行是否被其他事务锁占用，通过这个条件来解决实体化冲突问题。例如下面这种为领克汽车用户保养服务的 App 预约功能，其后台实现就可能会出现写偏斜问题，如下图：出现这种写偏斜问题原因就是因为在执行某个事务之前要检测某个条件是否满足，才能执行下一步，因为这次事务是分为两个步骤不能原子完成，导致多个事务可能某一时刻来查询前置条件都对其进行修改，影响到另外一个事务执行结果，写偏斜由于幻读引起的。串行化引发数据竞争最大问题就是多个事务同时去操作某一条数据所引发的问题，读提交和快照级别的隔离可以防止一些问题，但是例如幻读和写偏斜目前还没有很好的办法解决。说到数据竞争问题在编程语言竞争都通过锁来控制，或者想使用一些静态分析工具来检测存在数据竞争问题，例如在 Go 语言中提供了静态数据竞争检查问题 Race Detector ， 另外一种方案就是最为时髦的编程语言 Rust 官方号称为无畏的并发，其数据竞争问题在编译阶段就可以被提前发现，这个就利用到 Rust 编译器静态分析相关的功能，所有权规则和生命周期相关的东西来避免数据竞争。静态分析相关研究大部分还在学术界，目前也有一些成熟的商业产品 DeepSource 这种 SaaS 服务的公司，可以将其集成到代码仓库中帮助开发者审查代码的安全，已经支持多种编程语言相关静态分析实现，未来这种安全相关的服务会越来越多，也不可能是哪个特定语言的优势。目前常用的串行化实现就是采用单线程来执行事务的请求，采用在一个线程中按照顺序方式每次只执行一个事务，这就就可以避免数据竞争和上下文切换的相关的，也不需要解决事务冲突，这种方式事务处理目前 Redis 所采用在内存中处理的事务，使用单线程循环执行事务。事务本身就是要用户的多个操作变成一个操作序列，例如在电商网站上搜索商品和下单商品就要将多个步骤捆绑成一个事务去操作，但是如果事务中一个操作要等待其他因素就会导致这个事务等待，在大并发需求之后那么整个系统大部分时间将处于空闲的状态，数据库就无法高效的运行，Redis 的做法是将其使用 IO 多路复用。采用单线程这种模式不支持交互式的多语句事务，传统的做法是通过把事务代码打包成存储过程发送到数据库中，数据库会读取到相关的数据读取内存中高效执行，无需等待磁盘IO操作。 存储过程缺点： 不同版本的关系数据库，Oracle 和 SQL 等商业数据库都有不同的实现规则和方言，没有一个共同的标准，这就使得如果想迁移数据到不同的数据库上很难，和特定的数据库绑定到了一起。存储过程的代码是在数据库中运行调试更加困难并且版本控制也不容易维护，并且运行存储过程要求高的性能。像 Redis 这种采用的是 Lua 脚本语言来做，而 VoltDB 采用的 Java 或者 Groovy 来实现存储过程，使用特定的编程语言来做，例如 Oracle 实验型项目 GraalVM 来支持多语言做。如果是使用串行化的方式要考虑的因素，事务必须简短不用影响到其他事务；将仅限于活动的数据集完全可以加载到内存中，少量数据可以存储在磁盘中，单线程访问会造成严重性能问题；写的吞吐量必须足够低，读事务可以使用快照隔离级别进行读取，而写事务必须在单核上处理，否则需要采用分区，分区处理可以提高写的吞吐量，但是最好不能有跨区事务处理。两阶段加锁使用两阶段锁作为事务的隔离机制是目前大多数关系数据库中用的最多方式，两阶段锁分为两种锁，第一种是共享锁针对是多个事务读取一个数据记录时必须要申请共享锁，第二种占用锁针对的是某个事务要修改和创建一条记录时必须先持有占有锁，否则可以直接拒绝事务，让客户端应用重试。两阶段锁和传统的串行化锁存在某种分歧，传统的锁针对事务操作的某个对象上锁，如果这个事务存在多对象数据操作时锁不应该立即执行事务，如果立即事务执行操作改数据记录，被阻塞的事务会被唤醒会在前一个事务执行基础之上执行，结果会受到前一个事务影响；所以必须逐步加锁将每次事务锁需要用到数据记录上锁，直到此处事务被提交再将其全部解锁。两阶段锁一般都是锁管理器或者事务管理器所管理着，对受保护的资源进行权限控制，每个事务加锁时，锁管理器会检测这个锁是否被其他事务占用或排他模式所占用，如果被其他事务占用则需要等待其他事务提交或者终止再次尝试。其两阶段锁实现事务隔离可能会出现有死锁问题，死锁的问题在前面同步原语控制并发的博文中讲过因为不满足串行化的 3 个条件。两个事务都在等待对方锁住的资源才能正常执行，这样就产生了死锁，事务 A 正在等待 B ，而 B 又在等待 A ，解决办法有在锁管理器加一个定时检查机制检查有没有环形等待图，如果出现锁的环路情况，可以让锁管理器主动中断其中一个事务，来打破死锁情况。另外一种方式是基于事务的时间戳来判断事务之间的优先级，较低的时间通常意味着有较高优先级，这特性和之前的基于 LSM-Tree 存储引擎设计很相似，如果两个事务尝试获取锁，事务 A 想获取事务 B 所持有的锁，事务 A 有较高的优先级，则 A 进阻塞等待，事务只能被时间戳更高的事务阻塞。MySQL隔离等级最理想的事务状态是互不干扰，事务之间的操作没有依赖关系，事务之间可以以任何顺序进行执行这样对数据记录就没有任何的影响，结果也是完全独立的，这是数据库事务最理想的状态。但是这在多个客户端对数据库操作时是不可能实现的，因为物理世界的时间是直线运动的，多个事务并行操作绝对不可能出现同步情况，想要同步就得隔离让事务之间互不干扰。读未提交 < 读已提交 < 可重复读 < 串行化 这些是 MySQL 中的几种默认隔离级别，目前默认使用的可重复读级别，如果使用的串行化可能数据库处理的事务吞吐量会下降，但是能防止写偏斜和幻读的情况，各个级别的隔离效果对比图如下：在数据库隔离等级都是针对的是读异常和写异常情况进行定级的，读异常包含 dirty read 、nonrepeatable read 和 fuzzy read 同属于读异常级别的问题，而 lost update 、dirty write 、write skew 属于写异常级别的。 下面为一个数据库的事务执行的生命周期，一个事务从创建到结束的执行过程状态转换图：Active： 活跃状态的事务表示正在执行中事务，也就是处于活跃状态的事务。Failed：事务处于活跃状态时可能遇到错误终止，断电或者操作系统错误导致事务终止，称之为事务失败状态。Aborted：事务处于活动状态或者部分提交状态，执行某个事务时发生错误导致之前的状态回滚被撤销，当回滚完毕之后次状态称之为中止状态。Partially Committed：当一个事务完成最后一个操作时，由于操作在内存中执行的，如果还没有刷盘持久化就可以说该事务处于部分提交状态。Committed：当一个处于提交状态的事务被成功刷入到磁盘数据持久化时之后称之为事务提交状态。在 MySQL 数据库中可以设置全局的隔离级别和会话隔离级别，配置会话级别的命令如下：select @@session.tx_isolation;会话级别的隔离等级针对的是某个客户端连接级别的隔离级别，这种可以针对某种业务场景来设置隔离级别，而另外全局级别针对是数据库服务器上所有的会话链接，配置如下：select @@global.tx_isolation;还可以使用 set tx_isolation 命令直接修改当前 session 的事务隔离级别，SQL 语句和运行结果如下：mysql> set tx_isolation='READ-COMMITTED'; Query OK, 0 rows affected, 1 warning (0.00 sec) mysql> select @@session.tx_isolation; +------------------------+ | @@session.tx_isolation | +------------------------+ | READ-COMMITTED | +------------------------+ 1 row in set, 1 warning (0.00 sec)使用 SET TRANSACTION 语句分别修改会话和全局的事务隔离级别 SQL 语句和运行结果如下：mysql> select @@session.tx_isolation; +------------------------+ | @@session.tx_isolation | +------------------------+ | SERIALIZABLE | +------------------------+ 1 row in set, 1 warning (0.00 sec) mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; Query OK, 0 rows affected (0.00 sec) mysql> select @@global.tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec)小 结最理想的数据库事务设计是多个事务可以并行的执行，提高效率，但是并行执行事务要保证事务之间的正确性，事务必须严格按照 ACID 性质。数据库的事务隔离级别就是用对并行执行事务可能会引发的写异常和读异常做的描述，每个级别针对的问题都不一样，并发控制策略决定了如何调度事务和执行事务。其他资料[两阶段锁 [中文讲解] CMU-15445 数据库内核](https://www.bilibili.com/video/BV1AZ4y1Q7vx)DDIA Chapter7: Transaction (2)如何回答 MySQL 的事务隔离级别和锁的机制Innodb中的事务隔离级别和锁的关系MySQL事务的隔离级别InnoDB的事务设计OceanBase 分布式事务DDIA CH7 — Transactions数据库的隔离级别与2PL/MVCC算法原理CMU-15445 Two-Phase Locking (PPT)Announcing the first Postgres extension to run WebAssembly
分布式数据系统设计会面对很多的问题，整体系统出错的地方有很多例如：硬件问题或者断电问题，部署在不同节点上软件程序会出现 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 严格意义上来说只是多线程并发下的术语。 在关系型数据库中一次事务处理所需要的数据可能由多个表组合而成，表与表之间的关系可能存在某种约束，例如外键，这时如果操作一条数据记录就可能涉及到多个对象，多对象事务要保证就是当事务操作完成之后能确保这些外键的引用的正确性。而在文档型数据库执行一次事务大部分场景针对的当个文档对象操作，更新某个文档一个字段，就被视为单对象事务，而如果操作文档涉及到多个文档对象，就为多对象事务，因为文档模型不支持关系模型中的Join对表联合操作，没有规范化的数据时就得一次性更新多个文档对象，此时多对象的事务就能保证非规范化的数据更新出现不同步的情况。而针对分布式数据库中涉及到问题是在此基础之上延伸出来的其他问题，分区索引涉及到多个节点，如果没有事务会出现部分索引更新的情况；例如事务已经在各个节点上执行成功，但是由于网络问题导致客户端没有确认，那么客户端则认为事务执行失败，会重新执行事务导致额外的重复数据记录；如果事务执行失败是因为系统负载导致，那么事务重试机制会导致系统更加糟糕；如果是客户端问题导致事务失败，如果没有重试机制可能会出现本次事务丢失情况。综上所述的一部分问题可以在没有事务的情况执行，可以依靠着应用层的软件编写代码逻辑完成，但是不能保证出错之后的错误处理和错误恢复能变得简单，因为事务有提供相应的保证就可以简化一些问题，但是事务不是一个天然型的东西，虽然概念也很简单，但是数据库开发者们要实现这些功能，事务机制是被开发者们编写出来的，这对相关的开发者技能要严格意义上的要求，这时笔者的看法。弱隔离级别通过上面的了解不难推理出如果是两个不同的事务进行操作会发生竞争的情况，但是还一种情况是两个事务操作的数据记录互不干涉，例如一些个人信息更新场景，每个事务操作都是属于自己的数据记录，这时的完全可以使用并行执行事务不需要隔离机制。这种说法只能是对于应用层程序是透明的，因为程序开发者知道做了些什么，而大型数据库系统完全不知道这个问题的，并且一般大型分布式数据库系统会有多个客户端连接并且执行事务，并发出现数据竞争肯定是必会出现的。对于一个数据库来说完全不知道连接的客户端正在执行什么样的代码？有哪些代码正在被执行访问数据库？因此数据库的开发者就要对其执行事务代码进行隔离，连接数据库执行 SQL 客户端可以不用关心这些问题，而数据库实现的开发者却要实现对于应用层的程序隐藏内部的并发数据竞争问题。目前主流的数据库在解决这些问题上采用的弱隔离机制，因为如果对某些并发操作的事务想要不出现数据竞争情况，必须将其事务执行排队或者串行化执行，串行化执行带来一个问题如果是高并行情况下这些是完全接受不了的，串行化的隔离方式和之前我在并发锁那篇文章讲解的排号锁🔐类似，严重影响整体系统性能，这是不可取的。如何设计一个事务隔离级别呢？目前几种设计方案如下：读提交： 防止脏读，读取数据库时只能读取到已经成功提交的数据，可以防止脏读；写数据库时只能覆盖已经成功提交的数据，可以防止脏写，这种方式称之为读提交的方式。实现读提交： 此种方式要针对表行进行加锁或者对操作对象进行加锁，持有对象锁的事务才能执行，其他事务退出等待，这种方式对写事务优好；但是对读事务就不那么理想因为读取的事务比写多，可能因为在执行写事务的时候很多读事务被阻塞，所以大部分实现都维护原操作对象的两个副本，当被未提交时读操作返回旧的版本，当写操作提交合并留住最新版本，此种方式称之为实现读提交的方式。多版本并发控制： 这个要求事务有版本快照隔离级别，可以防止读取不完整的结果和防止并发写的混合情况，因为时间差异导致的度倾斜等问题...让在某一段时间内执行的多个事务能处理好数据之间的一致性问题，正在执行的事务可能在不同时间并发查询数据的状态，如果对其做版本控制的话可以保留不同操作时间的数据状态，这种技术为多版本并发控制。索引与快照隔离： 通过索引的方式让数据的索引指向多个版本的数据记录，过滤当前事务不可见的那些版本，当后台的垃圾回收进程会决定删除某个旧对象版本，索引也会更新；典型的例子就是B-Tree数通过写时复制技术来说实现更新操作，不会修改其数据页面而是创建一个新的数据记录版本让索引指向它，脏页面会被后台回收掉。防止更新失效CAS操作： 某些场景会从数据库中读取某个对象，将其修改，在写回去，当两个事务操作其对象时会发生，后者覆盖前者写入的值，最终导致第一次写入的值丢失；解决办法通过原子操作或者并行自动检测更新丢失功能，相对于原子操作要强制加锁，而自动检测更新失败可以人程序并行执行，自动中断某个执行失败的事务。串行化程序： 事务竞争最大问题就是多个并行执行对某个资源进行修改，如果把这些事务通过串行的方式进行执行就不会出现竞争的情况，目前最强的事务隔离实现就是让异步的程序变成同步的；实现串行化的方式有很多例如 Redis 使用的单线程，通过将事务包装成数据库的存储过程来执行，但是缺点是不同的数据库用的存储过程可能不一样不兼容，最新的一些数据库采用第三方脚本语言去实现例如 Redis 使用的 Lua，而 Oracle 未来可能会使用 GraalVM 来做。数据分区： 串行化发挥不了多核 CPU 的性能，对应数据库这种应用程序来说要面对多个客户端连接处理事务，单线程的设计很容易造成事务处理的瓶颈；要支持多个 CPU 可以将整体数据进行分区，然后将分区绑定到单个线程上执行，但是如果是跨分区事务一样会回归到竞争问题的本质上。两阶段加锁： 两阶段锁可以也是算是事务实现中隔离级别最高的机制，允许多个事务同时读取一个对象，但是只要出现了如何写操作则必须加锁来保证事务的安全性，2PL 实现的效果能达到类似于同步原语，但是缺点也很明显其实事务吞吐量和响应时间相比于其他的弱隔离级别下降非常多，性能不好，因为2PL必须有一个前提事务要操作的对象必须等待前一个事务完成之后才能工作，所有受时间影响比较大。索引区间锁： 所为的区间锁就是只在索引上锁住其范围，例如预定某个时间段内的房间情况，可以对其房间的索引ID进行加锁，其他事务看到索引上有锁得等待其他事务执行完成再继续执行。小 结无法什么方式来实现事务，事务都是一个数据库必备的功能，而事务中的隔离性是整个数据库中关键，市面上关于事务的隔离性的研究有很多论文，最主要的问题就是解决多个事务操作单元操作对象冲突解决，目前就两大类悲观锁和乐观锁派系，两阶段锁就是典型的悲观锁实现对事务完成采用同步执行的方式，来保证多个事务操作同一个对象的绝对安全性；而相比衍生快照隔离是乐观锁的实现，相当于皮特森算法一样，数据库之后检测潜在的冲突会继续执行事务会继续执行而不是中断，而当事务提交时是串行同步的方式才能被提交。其他资料事务的隔离级别What is a Distributed Transaction?DDIA Chapter 7. TransactionsACID vs BASE: Comparison of two Design Philosophies让构建 MySQL 分布式数据库变得更高效深入解析分布式数据库核心：分布式事务DDIA Chapter 7: transaction
一个公司的产品随着时间的发展单机的架构肯定无法满足其负载状态，产品迭代也是随着用户量进行的，当把单体架构拆分成多个微服务时就考虑一些数据同步问题，例如两个服务同时操作缓存中的一条数据或者是数据库中的数据，都会带来一些数据上的问题。这篇文章主要是针对的数据库系统，主从架构，主从切换、主从数据同步一些问题展开，也是DDIA第5章开始的数据复制和分布式系统设计一些挑战问题的总结。当单机服务不能满足需求时系统往往需要扩容，扩容之前讲过有两种，一种垂直扩容的方式进行的，就是无条件的堆机器硬件，这种方式即使把机器性能加到很强的状态也不一定能满足系统高可用的设计；另外一种就是水平扩展，通过将程序代码部署到不同地理位置的数据中心里运作，这些节点通过网络进行数据交换，这种方式有良好系统扩展性，但是会依赖网络，网络是很不可靠的，这时系统设计时就要考虑单节点故障时能保证整体可靠性。分布式系统节点进行数据复制可不像是在本机共享内存，本机复制数据只需要操作系统提供的API即可；而分布式系统重数据量太多的时候，往往会将其数据分区到不同的子系统上，也可以不同的网络节点上，这些数据如何同步？如果有主从关系如何保证数据的完备性？主从架构当一个数据库表记录达到某个上限可能就会考虑分库分表来缓解外部事物读写请求压力，有时候没有读写请求压力时也会考虑做主从架构考虑数据库的高可用性。例如主节点用来作为事务处理，而从库做一些数据分析相关的工作，另外就是当线上业务读事务大于写事务的时候，可以部署多台从节点来帮助主节点分担读压力。关系数据库做主从架构有一个很大问题就是如何保证事务的 ACID ，因为主从架构都是通过网络连接的，虽然大部分业界主流的数据库都提供了主从架构配置设置选项。但是不能保证多台节点做联机事务处理，目前支持这种也就是 NewSQL 系列的数据库。且大部分写请求是操作主数据库的，如何把主数据库的数据记录同步到从节点上？如何设计同步规则？怎么实现数据多节点同步完整性?目前主从架构的另外一个问题就是如何在主节点失效情况下，其他子节点如何晋升为主节点？如何处理节点失效问题？如何处理添加节点问题?数据同步即架构为分布式主从结构就要考虑解决数据多节点同步问题，目前数据同步分为3种：同步复制、异步复制、半同步复制；同步复制当用户向主节点写入一条数据记录时，此记录就会同步到其他节点，同步过程必须是当所有子节点写入成功之后才能返回请求，一段向用户确认说明主从节点数据都是最新版本的，缺点也很明显如果出现网络故障一直迟迟没有成功写入子节点请求就一直不会返回，其他写请求也会被阻塞。另外一种方式就是异步复制，将集群内部少数节点的数据复制设置同步的方式，其他多节点为异步的方式，异步复制可以在后台进行，当一条数据被同步到部分从节点时就可以返回写入成功请求，这样可以节约写入响应时间，这种同步和异步一起使用我更多认为是半同步复制。当集群中某个节点失效时，如需要增加新的子节点提高系统整体容错能力时，如何同步之前的数据？因为主节点是在并行运行着的，而新加入节点想快速同步全量数据该如何做？如何保证数据一致性？数据同步光靠着即使同步数据文件是无效的，因为此时主节点也在写入其他事务请求，而新加入的节点只能同步数据文件中数据而不是及时的；这里 Redis 主从架构设计的方式采用的是数据快照和增量同步方式，当需要同步时主节点快速生成数据快照通过网络传递给从节点，从节点则恢复数据，至于主节点新写入数据采用记录日志的方式同步，从节点为追赶模式，直到数据达到一致性；增量同步方式也适用于少量的网络抖动情况，当出现暂时性节点失联情况数据也能完成增量同步。不管是同步还是异步都会产生一些矛盾的地方，例如增加从节点是为增高读的吞吐量和系统整体可用性，如果将集群中的所有节点都设置为同步模式也会带来另外一个问题，每添加一个从节点，那么每次往主节点写入一条记录需要同步的数据记录就要写入到其他子节点中，子节点越多那么系统整体可用性就会下降，因为是全同步模式导致一次写请求要多次复制写入情况，每多一次复制写入就会多一次请求写失败概率，同步式要等着所有节点写入成功才能返回响应。这里可能有人会想全部换成异步模式不好吗？当把数据记录请求写入到主库中，其他节点就后台异步复制数据，但是然而因为是异步所以数据同步肯定会出现时间偏差的情况；当因为业务方可能是并发操作的不会立即等待异步数据写入才去读取，这时不现实的，当主节点写入的数据的成功，但下一个请求读取落到子节点而此时子节还没有完全同步数记录，导致读取不到数据记录情况发生，时序情况为下图：异步数据同步数据一不是像同步一样做强一致性的，而类似于 DNS 域名解析生效过程，采用的最终一致性，这也就是复制滞后问题，如果采用异步复制的模式可以为数据记录设置版本号。我个人想法是在写入更新请求之后一段时间内读请求还是发送给主节点，当过了异步复制时间窗口期时才读取从节点并且可以配合缓存来使用，例如 SingleFight 模式；如果是用户修改自己资料场景，可以将用户自己的新的读请求放到主节点上，而其他用户的读请求则在从节点上资料显示可以允许不是及时更新的，但是这时针对特定的场景不适应所有场景。所以在数据同步问题上没有十全十美的解决方法，只有根据业务场景做定制化设计。同步和异步问题都是由于请求顺序和时间关系引起的，关于时间在分布式系统设计里面的问题太多了，因为基于时间每分每秒都是在流动的，但是你不能从一个时间点跳到另外一个时间点，但是数据读取可能就会出现这种超自然情况，这种场景只能在科幻片里才能看到的；在这时间窗口内就会出现访问到老数据和新数据交叉的情况，业界很多相关讨论：单调读、写后读一致性、前缀一致读。节点切换在分布式系统难免会遇到某个节点出现问题的情况，在主从数据库架构，最大问题就是主节点出现了故障无法正常运作，此时业务方的写入事务请求就要转向新的节点了，此时一个问题是如何选择集群中其他的节点晋升为主节点？怎么提前发现主节点出现了故障？如何设计一个故障检查算法？目前最常见就是通过心跳包方式判断节点是否存活着，心跳包是有时间间隔的，如何设计这个间隔时间？分布式最大的问题就是依赖于网络，网络是非常不可靠的，可能出现网络拥堵或者抖动，导致心跳包超时，如果心跳包设置时间间隔过长会导致出现故障时难以即使发现处理，时间太短又频繁发送心跳包；如果发现故障了如何选择新的主节点？这也是分布式选举共识问题，业界有很多主流的算法Raft和Paxos，低端一点就是通过运维手动控制。如果是异步复制模式下主从架构，在主节点出现故障时很有可能出现一个问题，主节点的数据并没有完全同步到从节点导致从节点上的数据不是最新版本，此时从节点晋升为主节点后会出现一些莫名奇妙后果；例如主数据库在故障之前使用了自增 ID 做表记录，而一些新写入请求在原主节点执行成功，但是由于故障切换到新主节点上，由于异步复制未能同步到自增 ID 的计数器原值，导致后面其他请求到了重复的ID数据记录，这样会影响整个系统数据。日志复制分布式架构下数据怎么实现同步也是一种挑战，主流的设计方案是：请求同步转发、日志复制、逻辑日志复制。最简单的方式就是主节点把收到的 SQL 语句重新发给子节点，然后子节点重新运行一遍，这是最简单的实现方式。但是问题也很明显，如果 SQL 语句里面使用了 now 和 rand 函数或者存储过程，这时就会参数某些不确定数据记录，因为是两台并行运行机器无法预判具体函数产生的值。另外一个问题就是SQL语句有前后执行顺序问题也要考虑，例如多个并发事务时要注意。另外一种基于WAL日志复制传递的，主从通过复制日志文件的方式同步数据，通过网络把文件传输到子节点上，子节点收到数据文件就开始处理完成数据记录同步工作，数据文件格式依赖于特定版本，如果两台机器的版本不一致可能会出现数据文件无法兼容情况。如果想要兼容和数据记录可靠性就得使用行逻辑日志复制技术，这也是大多数数据库实现的方式，具体复制与存储引擎剥离，逻辑日志是通过一些列的数据表行的记录，对应写请求日志会包含所有相关的列的新值，删除记录有足够的条件来表示符合条件记录，更新也是通过唯一表示更新行以及所有列的新值。 MySQL 的 5.1 之后主从同步架构就是通过语句复制和逻辑复制混合使用，如果语句中存在不确定性，就会切换成逻辑复制紧凑模式，这样所有的节点都复制正确并且一致的结构了。这是都是基于数据库存储引擎内部实现的，还有一种需求是针对于自定义数据复制的需求，复制部分的数据到另外一个节点上或者解析某部分数据记录的需求，这时大部分数据库都有通过SQL触发器功能)，开发者可以使用触发器。多主架构主从结构如果是单一主节点架构算是只能为了提高读吞吐量和高可用设计的，并没有考虑到用户所在地理位置请求响应延迟等问题。如果是主节点和从节点不是在同一个数据中心，那么一个写请求的数据记录复制要跨数据中心才能完成，且读数据的时候也要跨数据中心，没有考虑到把在地理位置上均匀分布。这个问题可以在每个数据中心采用主从架构，而多个数据中心连接起来就是分布式多主架构，每个地区的数据中心都有自己的主节点写入数据时就不需要跨区写入延迟问题，此架构如下图：单主从架构缺点帮你充当解决异地容灾问题，最多当主节点失效的时候从节点零时升级为主节点继续为用户提供服务；而多主从架构可以分散保证可用性，也能提供地理位置的就近提供访问服务的问题，当A数据中心出现问题， B 数据中心可以继续提供着服务，多主数据中心架构对网络依赖很强因为主节点的写入也同步到不同数据中心主节点中，但是问题也很明显会出现脑裂的情况。最大的问题是有多个主节点时，用户们并发去操作其中一个主节点时会发生数据冲突问题，可能同时修改每条数据但是这条数据存在多个主节点中副本，这时就要考虑引入分布式锁🔐来解决这个问题，分布式锁是分布式系统设计一个难点。无主架构上面的多主架构因为数据写入并行操作导致数据记录存储时间先后顺序问题，如果要解决就要使用和本地锁一样的方案来解决处理写入冲突问题，如果多个用户在操作一条记录必须等待其他人释放锁才能正常执行操作，后面请求要么阻塞要么就丢弃不执行；另外一种方式就是将其路由到同一个主节点上执行，但是这违背了按地里位置就近服务的原则，然后自定义冲突解决逻辑，在写入数据的时候处理冲突通过用户自己决定，这个设计就和 GIT 版本控制软件很像；另外一种就是服务器返回多个数据版本，由读取客户端自己决定从多个数据记录中挑选需要的版本。与主从架构相比无主架构又是另外一种架构方式，所有的服务器节点没有主从关系都为对等关系，也就是P2P模式即去中心化的架构和区块链架构很像。这种架构最典型的例子就是 Amazon DynamoDB 它是分布式 NoSQL 数据库的代表，主要特点就是一个数据记录写入到集群中的多个节点中，而读取则可以从集群中多个节点中读取数据记录，读写流畅如下：假设整个数据库集群有3个节点组成的，当有数据记录写入到集群中 2 以上就认为写入成功则，当读取的时候客户端可以从多个节点中获取数据副本，然后根据本地算法查看数据记录版本号，例如上图读取回来有一个版本为 6 和两个版本为 7 的，少数服从多数，那么最终数据记录就为 7 ，客户端并且将返回 6 的数据记录端设置 7 放回去，保证最终一致性。另外再每个存储引擎上后台都会一个线程在和进群中的其他节点做数据协调，检测数据记录版本的差异，自动修复缺少的数记录，自动从一个副本复杂到另外一个副本，而与复制日志形式不同的是不能保证写入顺序也要明显的数据同步滞后问题。Quorum一致性这种方式做就是数据记录冗余存储，如果有相同数据记录副本存储在集群中多数节点中，那么读取的时候大概率会读取到最新版本记录，如果配置的写入节点越多，那么整体系统可用率就越高，这是根据一个Quorum一致性原则推理过来的实现方案。 Quorum 机制是一种分布式系统中常用的，用来保证数据冗余和最终一致性的投票算法，其主要数学思想来源于鸽巢原理，有 10 只🕊鸽子但是有9个鸽笼，如果把这10只鸽子要全部放到笼子里，那么其中一个笼子里面肯定为两只，如下图：根据这个规律，换到分布式数据库中，笼子就相当于节点数 n ， w 表示鸽子数，r读取数据访问笼子数据，断定只要 w + r > n 那么一定能放到最新的数据记录，在 Amazon Dynamo 风格的数据库中是可以通过配置进行修改这些参数的，到达法定仲裁条件，场景设置方案是n为奇数通常为 3 或者 5， w = r = (n + 1) / 2 向上舍入。另外一种 Sloppy Quorum 提供配置整体系统可容忍的某些节点故障个数，也不需要执行故障切换。本质上在 Quorum 就没有主节点这种说法，可以容忍部分网络故障导致客户端连接到n个节点读取数据，对于系统高可用性和低延迟要求来是这种方式确实不错；但是因为网络原因和地里分布的原因，可能一部分客户端能连接到某些节点，一部分连接不上某些节点，导致某些节点再客户端面前就等于失效状态，或者将不能正常连接的节点请求转移到其他节点之上。小结多集群数据同步是一个复杂的问题，因为多个节点的数据写入和操作都是并行执行着并且都是有先后顺序的和时间顺序的，主从同步需要数据写入主节点再写入从节点，写只能操作主节点，读只能操作从节点，但是会出现写放大问题，一个写要被复制到多个节点上；异步复杂提供响应速度失去了数据一致性问题，会出现读滞后问题；而P2P模式则需要更强一致性算法保证数据记录，因为数据存储多个版本，要协调多个数据版本问题；分布式系统设计最大的问题就是将多个异步的操作变成同步并且保证数据一致性，依赖于网络并且还依赖于时间，处理时间的时候是属于物理学的东西会和狭义相对论联系起来，因为大部分操作都是异步进行的。其他资料DDIA Chapter 5. Repliation
计算机程序在通过网络进行数据交换过程中会使用到数据格式编码，一些本地持久化文件也是需要进行编码存储，前面讲到了数据密集型应用设计中一个需要考虑的有可扩展性和可演变性，本文将对数据编码的可演进性进行探讨，改如何设计数据的编码方式？用什么样的数据格式？应用程序随着时间的变化数据格式和编码也会发生变化，怎么做到向前兼容？在关系模型中数据表结构在项目需求分析阶段就已经确定了，通过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\x00\x00\x00world\x00 // 字段值（值大小，值，空终止符） \x00 // 0x00 = 类型：EOO（'end of object'，对象结尾）但是有这些二进制数据编码格式还是远远不够的，日常开发需求和定义的数据结构复杂多样性，这时就需要能根据特定数据类型描述来生成对应二进制数据编码的工具了。例如现在微服务使用的gRPC协议传输使用的Protocol Buffers，在分布式应用程序中经常使用这些二进制序列化数据结构格式来做不同系统之间的数据交换工作，在分布式系统之间通过[接口描述语言](https://zh.wikipedia.org/zh-cn/%E6%8E%A5%E5%8F%A3%E6%8F%8F%E8%BF%B0%E8%AF%AD%E8%A8%80)进行接口的数据结构定义。相关的技术实现有Apache Thrift和Google Protocol Buffers，这些都需要开发者在使用之前进行数据结构建模，也叫模板定义，提供特定模板来创建生成对应编程语言所需要的代码结构，这些代码将用来生成或解析代表这些数据结构的字节流。这些模式定义描述二进制编码格式，相比JSON模式要简单，但是支持更详细的验证规则，对于静态类型编程语言来说，生成的代码可以编译时进行类型类型检查。通过Protocol Buffers进行编码的数据满足程序兼容性，Protocol Buffers没有使用字段进行编码而是使用的Tags进行的，每个字段都有标识，可以轻松修改模式中代码字段名称，而编码永远不直接引用字段名称，但是不能修改编码中标签，这样会导致被编码数据失效。数据流传递在一个大型数据系统中往往不是单个服务节点运作的，大多数都是支持水平扩展并且有不同服务节点进行运作的，每个节点所能操作的只是本地内存，如果需要共享内存中的数据，就要将其序列化编码通过网络字节流传到另外一台计算机上。因为是不同的计算机共享数据，就要考虑到数据编码的向后兼容性问题，例如发布新功能使用滚动发布，这时一个大型系统上有新代码，也有旧代码同时运行着，如何保证互相不受影响正常工作？通常开发微服务一般使用的都是RPC和REST进行数据交换工作，基于WEB大部分数据交换格式采用的JSON和XML被称为Ajax技术，这种模式之下服务器不需要渲染整个页面给前台，而是通过JSON编码数据对象，也称之为RESTful API不限于浏览器客户端，只要支持发送HTTP请求的程序都可以算是客户端，使用JSON能适用于各类的程序编程语言和不同的客户端，这也是JSON的优点也是公共API主流风格。另外一种就是基于服务内部之间的数据传输RPC的方式，例如Java语言特有的RMI，但RMI和特定的语言绑定到一起了，不能适用于其他编程语言场景。RPC一般要有多个服务返回，这是要考虑很多网络因素并且，优点就是可以使用模式定制语言来描述数据格式，和编程语言的无关性，RPC多用于同一组织内多项服务之间的请求，通常发生在同一数据中心内，而HTTP这种只能对外访问使用。其他资料Language Is Baffling - The Story of the Tower of BabelProtocol Buffers 101
在计算机内存上操作数据一般操作系统都通过了一些系统调用接口，直接使用这些接口就可以；相比在磁盘里面操作要是简单的多，程序运行的时候大部分都是从外存加载二进制文件到主存上执行，主存的操作相当简单，在编程语言实现上只需要分配和管理就可以，操作系统提供了虚拟内存来控制。而在磁盘上访问内容我们就需要通过偏移量来访问了，数据是序列化二进制存储在磁盘上的，如何把主存中数据结构存储到磁盘上有相应的存储布局呢？这时本文要介绍一些实现磁盘存储和数据编码原理。在磁盘上的数据布局远比内存上重要，内存速度虽然比磁盘读写数据要快，但是容量会受到限制并且容量可能是磁盘容量的千分之一，并且断电数据会丢失，这时我们就需要通过能持久化的磁盘设备来保存数据，但磁盘的速度又比内存操作慢，这时存储数据我们就要设计一个高效并且合理的数据结构做磁盘的数据布局。这也是之前前面几篇文章要介绍的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（字节序）