数据库系统的核心作用在于高效地存储和管理数据，并支持从海量信息中快速检索所需记录。传统的 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、磁带等，能够在断电后保留数据，适用于长期保存程序和文件信息。[scode type="yellow"]鉴于数据可能存储在多种物理介质（如磁盘、SSD、光盘等）上，主流的操作系统需要提供一致的抽象接口，以屏蔽底层硬件差异，使用户能够高效、统一地进行文件的读写、创建与删除等操作。当前主流操作系统均实现了完整的文件系统，开发者可以通过如 POSIX 这样的标准 API 接口来访问和管理存储设备，而无需关心具体设备的物理实现方式。尽管操作系统的文件子系统通过标准如 POSIX 提供了对存储设备的访问能力但仍存在一些限制；例如存储性能受限于设备的物理读写速度，同时文件系统通常采用页缓存 Page Cache 或写缓冲机制，这意味着数据写入操作并不会立即持久化到磁盘，一旦系统发生故障或意外断电，可能会导致数据丢失，影响系统的可靠性。[/scode]基础架构综上所述，针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现，存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录，存储引擎会将数据记录索引信息全部保存在内存中，从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据，并且还能减少读取磁盘索引所需要的时间，通过一次索引定位来读取数据记录，写入和查询流程图：其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ，在对数据执行任何操作前，都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色，也作为主要的数据存储载体。在数据库进程崩溃后，只需从 WAL 日志中顺序读取各条 Segment 记录，即可高效恢复内存中的索引结构。在 UrnaDB 中对这些 WAL 数据文件有一个统称为叫 Region 文件，这些文件有单个固定大小限制，当一个数据文件写满之后就会被关闭，会重新创建一个新的 Active 活跃的 Region 文件进行数据记录的进行写入。被关闭的 Region 文件会被视为冷数据文件，随着数据库不间断长时间运行 Region 文件会不断递增逐渐占用磁盘空间。此时数据库进程就需要对旧的 Region 文件执行压缩 Compaction 和定期清理，以降低存储压力并提高查崩溃数据恢复时的启动效率，压缩流程原理图：目前工业级的存储系统中，RaikDB 是一个采用类似模型实现的数据库产品，RaikDB 是基于 Amazon Dynamo 论文的设计理念构建而成。其 RaikDB 底层也是采用的顺序写入的日志式存储引擎，并将其命名为 Bitcask 存储模型，以提升写入性能和读写效率。该模型与 UrnaDB 所采用的存储引擎在设计理念上颇为相似。Bitcask 存储引擎在执行数据压缩 Compaction 时会生成 Hint 文件，其作用是在存储引擎重启时，辅助快速重建内存索引，从而避免每次启动都需全量扫描数据文件，显著提升系统的启动效率。然而 Hint 文件仅在 Compaction 过程中生成，所记录的只是当时内存索引的快照，无法反映实时状态。[scode type="yellow"]针对这一问题 UrnaDB 在存储引擎中进行了改进，将 Hint 文件机制替换为 Checkpoint 功能，系统会在固定时间间隔内，定期将当前内存索引持久化为 checkpoint 文件写入磁盘。当服务器进程发生异常崩溃，也可通过读取 checkpoint 快照快速恢复内存索引状态。在实际恢复流程中，UrnaDB 会优先加载最近一次的 checkpoint 快照，并从该时间点之后的 Region 文件中继续回放增量数据。通过这种方式，有效提升了系统启动恢复的速度，减少了启动耗时，同时兼顾了数据一致性与运行效率。[/scode]在该存储引擎设计的基础上，UrnaDB 构建了内置的 HTTP 服务器，并引入了多种抽象化的数据结构层。整体架构如下图所示：这些模块相互协作构成了一个完整的单机版 NoSQL 数据库，实现了从底层存储到服务接口的一体化设计。检查点技术[scode type="yellow"]由于 UrnaDB 数据库存储引擎采用将所有索引数据存储在内存中的设计，一旦数据库进程意外崩溃，内存中的索引信息将会丢失，未能持久化到磁盘。这样在下一次启动时，系统需要重新扫描所有数据文件，以重建内存中的索引结构，如果数据文件体量较大，将导致启动过程变得缓慢，启动耗时较长。[/scode]Checkpoint 功能机制会通过定期将内存中的索引状态保存到磁盘，在发生异常重启时即可快速加载 checkpoint 文件，从而大幅缩短数据库的启动时间。开启 checkpoint 功能可以在数据库崩溃后显著加快启动恢复的速度。然而 checkpoint 文件的生成时间间隔设置同样需要谨慎权衡。如果生成间隔过短，将导致 checkpoint 文件过于频繁地写入磁盘，占用大量 I/O 资源，可能影响数据库的整体性能；而如果间隔设置过长，崩溃恢复时仍需扫描大量 Region 数据记录，反而无法发挥 checkpoint 的优势，导致启动时间较长。因此合理配置 checkpoint 的生成频率，需在性能开销与数据恢复效率之间做出平衡，建议根据实际业务的数据写入频率和系统负载，动态调整该参数以获得最佳效果。磁盘垃圾回收器[scode type="yellow"]由于 UrnaDB 存储引擎采用顺序日志写入结构，随着系统运行时间的增长，每个 Region 区域对应的磁盘数据文件会不断增大。默认情况下 Region 文件中的每条数据记录未经过压缩，导致磁盘空间占用随数据量增加而持续上升。为了解决这一问题 UrnaDB 存储引擎内置了数据压缩算法，启用该压缩器功能后，可以显著减少磁盘文件的存储体积，从而有效降低存储成本。[/scode]在配置 Region 区域的垃圾回收功能时，需要综合考虑多种因素。若不启用该功能，随着系统长时间运行，磁盘空间占用将持续增长，甚至可能过高，影响系统稳定性。若启用垃圾回收，则必须合理设定其执行周期：周期过长：会导致空间回收不及时，磁盘占用上升，同时在压缩 Region 时耗时较长。周期过短：会导致垃圾回收过于频繁，增加系统资源开销，进而影响存储引擎的整体性能。在 UrnaDB 中垃圾回收器的执行周期同样依赖于 Time 触发，并且其执行过程会占用一定的系统资源。如果采用并行执行机制，这段时间对外部应用基本无感，系统可持续对外提供服务。这种设计理念与许多现代编程语言的垃圾回收器类似，UrnaDB 的 Region 区域划分为 Active Region 和 Read Region，其架构与 Java 虚拟机（JVM）中的垃圾回收机制具有相似性。当垃圾回收开始运行时，它不会对 Active Region 中的活跃数据进行操作，而是仅针对旧的 Read Region 区域执行垃圾分析与清理，从而避免对当前读写路径造成干扰，不会出现类似于编程语言虚拟机中的 Stop-The-World 世界时钟停止问题，减少压缩磁盘空间的抖动，保障数据库稳定性。时间就是一个问题[scode type="yellow"]在整个 NoSQL 数据库的设计过程中，许多复杂问题的根源其实都与物理世界中的 Time 有关。当前的 UrnaDB 实现为单机版本，因此主要面临的是本地磁盘 I/O 和线程调度带来的延迟问题。然而一旦在未来引入多个存储节点，发展为分布式架构，就必须应对网络延迟、时钟同步、事务一致性等与时间密切相关的挑战。因此无论是当前的 NoSQL 单机实现，还是面向未来的分布式扩展，如何合理建模和管理时间 Time 维度，始终是数据库架构设计中的核心研究问题之一。[/scode]在单机版本中还面临另一个挑战如何为数据记录提供过期时间（Time-To-Live）支持，尽管 TTL 是常见的需求，但其实现往往较为复杂，目前主流 NoSQL 数据库普遍采用惰性删除机制，即仅在数据被访问时才判断其是否过期并删除，这种方式依赖于外部读取请求的触发，可能导致过期数据长期滞留在存储中。因此如何合理权衡 Checkpoint 的执行频率与垃圾回收的触发时机？是目前 UrnaDB 数据库系统优化的重要考量。如果在数据高频读写期间执行垃圾回收操作如压缩磁盘空间，可能会带来短暂的读写延迟。同时存储引擎在数据迁移和压缩过程中，也可能导致磁盘空间临时占用上升，进而影响整体性能和资源利用。对此我更推荐采用主动回收机制，通过周期性运行的后台垃圾回收线程，专门处理过期数据的清理任务，该方式可以更好地控制存储空间的占用，提升系统的可预测性与稳定性。
在 MySQL 8.x 带来了许多新特性和改进包括 JSON 数据类型和 Java Script 作为 JavaScript Shell 支持，可以直接使用 JavaScript（JS）作为数据库查询语言或存储过程的语言，更好的性能和安全性，例如连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这些都是新版本特性。本文将记录完整的基于 Ubuntu 最新版本中安装 MySQL8.x 版本的过程。安装和配置在安装 MySQL8.x 之前可以尝试先更新一下操作系统和软件包，执行下面命令可以更新：sudo apt update && apt upgrade -y更新系统和软件包版本需要一点时间，这个过程取决于你网络和所使用的 apt 软件包数据源地址，数据源配置文件路径 /etc/apt/sources.list。[scode type="yellow"]如果不确定系统中是否安装其他版本的 MySQL 版本，可以先执行清理命令。清空完成之后，提供 apt search mysql 包管理工具搜索最新的 MySQL8.x 软件安装包，最后通过 apt install 执行进行安装。[/scode]sudo apt remove mysql* sudo apt autoremove sudo apt autoclean清空完成之后提供 apt 所提供的 MySQL 软件包进行安装：sudo apt search mysql sudo apt install mysql-*安装完成之后，可以使用 sudo mysql_secure_installation 来初始化 root 用户的密码，这是 MySQL 新版本所提供的初始化数据库程序：sudo mysql_secure_installation[scode type="yellow"]如果需要让 MySQL 服务器能被远程的用户提供客户端访问，需要配置修改服务器所绑定监听的 ipv4 地址，默认是本地环路地址，在这个 /etc/mysql/mysql.conf.d/mysqld.cnf 中找到 bind-address 的值修改它为 0.0.0.0 即可。[/scode]# changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir # tmpdir = /tmp # # Instead of skip-networking the default is now to listen only on # localhost which is more compatible and is not less secure. bind-address = 0.0.0.0 #这一行 ✅ mysqlx-bind-address = 127.0.0.1通过上面的配置目前 root 还是只能通过本机的客户端进行连接，有远程登录需求的可以配置一个远程用户并且允许远程登录，并且设置初始化密码，我这里开放的是 root 用户，执行下面命令：GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED WITH mysql_native_password BY '新密码' WITH GRANT OPTION;解决安全连接问题在 MySQL8.x 版本中如果要使用正常的密码连接到 Server 中连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这个是新特别特意到安全功能增强，如果要使用这种方式连接必须要配置 SSL/TLS 密钥对进行连接。[scode type="yellow"]指定用户使用的密码验证方法 mysql_native_password ，这让 MySQL 使用的默认身份验证插件，它支持基于用户名和密码的常规身份验证机制，执行下面命令使用默认密码验证方式进行连接：[/scode]ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'new password';执行上面的 SQL 之后，刷新一下 MySQL 授权表：flush privileges;最后记得在服务器上允许开放 3306 端口数据包的连接。
现在注册一个域名和购买一台服务器部署一个 Web 网站非常容易，现在流行的 Serverless 架构更是连服务器都不需要购买，直接在各个云厂商的后台点点鼠标就可以部署一个在线的 Web 程序，虽然相比传统建站的方式简单很多，但是也有考虑一些很基础因素和配置，这篇博文我讲以博客系统为例子讲解如何维护一个 Web 服务程序。现在有很多流行的博客系统，例如静态的 Jekyll、Hugo、Hexo，动态以 PHP 为首的 WordPress 系统，WordPress 有着完善的生态系统是世界上目前二次开发使用最多的 Web 系统，静态的博客可以部署在 Github Pages 和 Vercel 等，直接将编写好的网站源代码上传到免费的空间即可。而对于动态博客系统还说，需要 LNMP 环境的支持才能正常运行，现在也流行直接购买已经部署好的数据库环境和运行环境来跑博客，对于动态博客系统还是数据才是最重要的，这篇博客将介绍一些方法一名合格的站长该如何运维自己博客。基础防御[scode type="yellow"]博客系统存放在主机之上通过互联网提供世界各地访问，这些访客里面有普通人也有一些黑客，黑客可以利用博客程序的漏洞例如 XSS 和 SQL 注入漏洞来破坏程序，这对于博客系统安全有了新的要求。普通的静态博客完全可以不用管这些，但是对于 PHP 这类的动态脚本语言来说更需要关注，因为 PHP 是一门动态语言如果被黑客注入了恶意代码会导致程序数据泄露，严重的会破坏程序的运行，目前场景的作为就是在博客核心代码里面对用户输入的数据进行过滤或者做合法性校验，或者在 PHP 使用 disable_functions 配置项来禁用一些敏感函数，来保证程序安全性。[/scode]如果考虑不是在应用程序嵌入代码来做防御，可以使用一些带有代码注入攻击检查防御的网关产品，博客作为一种 Web 程序提供的服务是要经过 HTTP 网络请求的，可以考虑采用带有防御型的网关产品。例如 Cloudflare 它可以提供基于 SASE 和 SSE 平台，将网站的域名的 DNS 接入到 Cloudflare 并且使用它来解析服务网站，可以改善安全和韧性减少攻击参数传入到主程序中，国内也有长亭科技的 SafeLine 的 WAF 产品。Cloudflare 并且提供 CDN 服务，可以隐藏真实的主机 IP 地址，也能确保域名和 DNS 设置的安全，防止域名劫持和 DNS 污染攻击。对于 HTTP 协议来说才有基于 TLS 安全层的 HTTP 协议，启用 SSL/TLS 证书，确保所有数据在传输过程中都是加密的，防止 Cookie 和 Session 信息被截取或篡改。对于基于使用 LNMP 作为基础设施运行的系统，要考虑是不是使用的长期支持版本的软件，如果使用的不是长期支持的版本的软件，如果出现了漏洞，对于这些已经不维护的基础软件会很麻烦，不能得到相应的补丁更新。操作系统环境还是建议使用基于 RHEL 系列的 Linux 发行版本，现在 CentOS 已经是最优的，我推荐使用 Rocky Linux 这个系统是 CentOS 创始人创建的一个克隆 RHEL 的开源操作系统，是在 RHEL 的基础上进行重新编译的，该系统在很大程度上兼容 RHEL 版本的，并且可以通过相同的方式安装 RHEL 软件包，还能得到长期的更新支持。当整个基础环境搭建完成之后，强烈不建议使用 root 账号去运行网站的基础设施，更应该创建一个专用的用户组和用户来运行对于的软件，这样即使黑客绕过前面的基层防护，也没有更高的权限去访问其他的数据和程序。服务器防火墙软件还是推荐使用目前主流的 firewalld ，这是 REHL 企业级的服务器管理软件，相比一些老的 iptables 来管理出入站流量复杂的操作更容易操作。不要开放不必要的端口，一个 Web 服务器最多开发基础的端口 80 、 443 、22 端口，其他端口可以一律禁止外部访问使用，最好的方式使用白名单 IP 的方式进行访问控制，通过跳板机进行管理。对于 root 用户来说应该不使用 root 密码进登录，而使用基于 SSH 的非对称密钥进行登录，可以将登录电脑和被管理的服务器进行绑定，将公钥上传到远程服务器来进行登录管理。此外此种方式还要考虑私钥的安全性，不要将私钥公开，避免在页面或日志中输出敏感信息，如数据库凭据、API 密钥等。数据备份在线系统的核心是数据，这些数据一般会被存储在 MySQL 这些关系型数据库中，目前主流的动态博客系统数据都是存储在关系数据库中，前面提到黑客通过一些特殊的手段获取到了服务器执行权限，从而可能会导致数据库被拖库导致数据泄漏。对于数据库系统来说管理也是不推荐开放 root 权限给远程用户登陆，对于 Web 程序所依赖的数据库表所在数据库提供对于数据库账户来管理，并且取该账户的一些敏感权限例如删除数据表，添加新字段等，最后 Web 程序的 IP 和能被访问数据库 IP 白名单进行绑定，这样以免泄露了账户和密码也能阻挡恶意的用户远程连接到数据库。建议在数据库中的敏感字段进行加密存储，并且定期对数据库的数据进行周期性备份。对于普通博客系统建议每天执行一次数据库备份计划，例如使用 crontab 进行周期性执行备份脚本，这个备份可以使用 mysqldump 这类工具部分，对其备份的数据可以考虑加密存储。前面所说的是传统备份，部分云厂商提供了主机快照功能，可以对整个主机的数据进行快照备份储存，当然要额外支付一些费用，不管以何种方式进行数据备份，最重要的是养成备份数据习惯，即使数据丢失也能通过备份数据恢复。
关系型数据库目前有很多产品共同特点是分为商业版本和社区开源免费版本，目前社区开源版本的关系型数据库用的最多的数据库是 MySQL Community Server Edition ，最大特点为免费商业使用也不用顾虑任何法律相关的问题，MySQL 被 Oracle 公司收购之后也推出了商业服务，原有的 MySQL 开发人员为了能摆脱 Oracle 相关的控制也在 MySQL 原有代码的分支上开发出来了 MariaDB 开源社区驱动开发的版本。[scode type="lbule"]对于一个想要学习数据库技术的新手来说要解决的问题当然是如何拥有数据库服务器端，数据库也是一种 C/S 架构的程序，对于多数开发者来说用的最多的是客户端，服务器管理大部分由 dba 操作，这篇博客将介绍如何通过 tar 包的方式在 RHEL9 上安装 MySQL 服务端程序。[/scode]准备工作首先需要一台已经安装 RHEL9 的操作系统，目前 rhel 官方的仓库已经支持直接使用 dnf 命令安装 MySQL 服务器，这种针对是完全没有经验用户傻瓜安装方式。如果需要自定义安装路径和用户组还是推荐使用 tar 包的方式安装，首先安装一些依赖包和工具包：yum -y groupinstall "Development Tools" yum -y intsall vim wget libaio[scode type="yellow"]最后去 MySQL 官方网站找对应的 Linux Generic Archive 安装包下载到本地：[/scode]wget https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz[scode type="yellow"]创建一个用于存储 MySQL 服务器程序的目录，通过命令 mkdir -p /usr/local/mysql ，将其解压到这个目录中：[/scode]# 解压到指定目录中 tar -xvf mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz -C /usr/local # 重新命名目录 mv mysql-8.0.32-linux-glibc2.12-x86_64 mysql解压完成之后接下来需要创建两个目录，一个用来存放 mysql 运行时产生的日志文件，另外一个为存放 mysql 生成的数据文件目录，需要创建两个目录命令如下：# 创建 log 目录 和 创建数据目录 sudo mkdir -p /usr/local/mysql/data /usr/local/mysql/logs将 mysql 的支持文件拷贝到 /etc/systemd/system/ 目录中，该目录存放都是一些开机自动化启动程序，能够使用 systemctl 程序的子命令 start 、 stop 、restart 、reload 来管理某个具体的应用，例如这里的 mysql 程序。# 复制支持文件，并且重新命名为 mysqld sudo cp /usr/local/mysql/support-files/mysql.server /etc/systemd/system/ # 所有者应有执行权限 sudo chmod +x /etc/systemd/system/mysql.server[scode type="blue"]接下来要创建用来运行 mysqld 的用户，和将上面的文件夹所有者和所属组全部设置为新创建的 mysql 用户和组：[/scode]# 创建用户 useradd -M -s /sbin/nologin mysql # 更改文件所属组和所有者 chown -R mysql:mysql /usr/local/mysql[scode type="yellow"]最后将 mysql 的二进制命令工具目录加到环境变量中，以方便在任何地方都能使用 mysql 提供的命令工具：[/scode]# 在 /etc/profile 最后一行添加 echo "PATH=$PATH:/usr/local/mysql/bin" >> /etc/profile # 刷新环境变量 source /etc/profile到此整个安装前置条件已经满足，剩下就是对数据库进行安装和初始化了。初始化安装当准备工作完成之后就需要对数据库进行初始化，这里初始化使用 mysqld 命令，要提供 --initialize 参数，--user 指定运行程序用户，--basedir 指定数据库生成的数据文件存放的目录，--defaults-file 指定配置文件路径，执行命令会自动初始化数据库并且设置默认的 root 登录密码：[root@rhel9 ~]# sudo mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data 2023-03-22T12:08:36.476715Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.32) initializing of server in progress as process 23676 2023-03-22T12:08:36.486664Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started. 2023-03-22T12:08:36.983513Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended. 2023-03-22T12:08:38.468726Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: %ajsN=2G_)nf[scode type="red"]如果是比较新的系统可能会出现 libtinfo.so.6.1 文件版本过高，导致 mysql 客户端链接的 libtinfo.so.5 ，所以创建一个软链接：[/scode]ln -s /usr/lib64/libtinfo.so.6 /usr/lib64/libtinfo.so.5[scode type="green"]最后使用 mysql 客户端登录连接到服务器，第一步为重置 mysql 到密码，一定要重新设置 root 的密码，否则不允许任何操作：[/scode]# 重置密码 alter user 'root'@'localhost' identified by 'xxxxx'; # 开启远程登录 RENAME USER 'root'@'localhost' TO 'root'@'%'; # 或者直接创建一个新的用户 CREATE USER 'root'@'%' IDENTIFIED BY '12345678'; # 再给予权限操作 GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;最后如果要自定义配置 mysqld 话，需要在 /etc 目录中创建一个 my.conf 文件，这个文件就为 mysqld 启动时所需要的配置文件：[mysql] # 设置mysql客户端默认字符集 default-character-set=utf8mb4 [mysqld] skip-name-resolve #设置3306端口 port = 3306 # 设置mysql的安装目录 basedir=/usr/local/mysql # 设置mysql数据库的数据的存放目录 datadir=/usr/local/mysql/data # 允许最大连接数 max_connections=200 # 服务端使用的字符集默认为8比特编码的latin1字符集 character_set_server=utf8mb4 # 创建新表时将使用的默认存储引擎 default-storage-engine=INNODB lower_case_table_names=1 max_allowed_packet=16M #配置免密登录 skip-grant-tables [mysqld_safe] # 配置日志文件和进程文件 error-log=/usr/local/mysql/logs/error.log pid-file=/usr/local/mysql/logs/mysql.pid最后如果想方便管理，使用系统 systmctl 命令管理 mysqld 程序，此时就的使用 chkconfig 命令添加一个服务管理项目：# 添加 mysqld chkconfig --add /etc/systemd/system/mysql.server # 这样就可以启动一个 mysql 服务 systemctl start mysql.server配置成功之后管理 mysql 就可以使用 systemctl 命令进行管理了。其他资料RHEL9 数据库指南Systemd 入门教程：实战篇
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
PostgreSQL 是一个功能强大的开源对象关系数据库系统，经过超过 35 年的积极开发，在可靠性、功能稳健性和性能方面赢得了良好的声誉，该数据库产品在开源 BSD 协议下发行，这意味着如果你有能力在此基础之上进行二次开发的，可以基于它来构建你商业型产品，作为一名开源软件爱好者一个多多使用的开源软件。 MySQL 在被 Oracle 收购之后出现了 MariaDB 分支这些都是兼容 SQL 标准关系数据库，在数据库引擎排行榜上显示 PostgreSQL 正在慢慢上升趋势，作为一款开源数据库能和一些企业级专有数据库软件排名不分上下，已经表示出它的优势所在了。PostgreSQL 和传统的关系数据库相比，还有一个特别特性是对象关系型数据库，写过面向对象编程语言 Java 会熟悉多个对象继承关系，类似的 Postgres 它弥补了关系型编程和面向对象编码之间的差距，这使得对象的定义以及表的继承成为可能，从而导致创建更复杂的数据库系统；此外 PostgreSQL 除了传统关系数据库支持的数据类型以外还有一些特殊自定义数据类型，例如 Array 、Points 、 Inet 、JSON 、XML 等类型。安装 PostgreSQL这里我采用的通过 Homebrew 的方式进行安装 PostgreSQL ，通过 Homebrew 安装的软件包可以方便管理也是一种比较即可的方式；在使用 Homebrew 安装任何东西之前，你应该始终确保它是最新的并且是健康的：brew update & brew doctor这两条命令会更新你电脑上的 Homebrew 并且自动检测健康状态和自动修复一些常见的问题，并且也会报告一些无法自行修复的错误。接下来就可以使用 Homebrew 进行安装 PostgreSQL 使用命令如下：brew reinstall postgresql@14这个安装过程可能比较缓慢，取决于当前电脑所在地区的网络情况，可以切换到国内的软件源进行安装，使用下面命令就可以切换阿里云的软件源，如下命令：# 替换brew.git cd "$(brew --repo)" git remote set-url origin https://mirrors.aliyun.com/homebrew/brew.git # 替换homebrew-core.git cd "$(brew --repo)/Library/Taps/homebrew/homebrew-core" git remote set-url origin https://mirrors.aliyun.com/homebrew/homebrew-core.git # 刷新源 brew update安装完成之后可以使用 brew services list 查看当前支持管理的软件服务，启动 Postgres 也很简单，只需要使用下面的命令来启动 Postgres 服务：brew services start postgresql@14当启动成功之后，在最新的 MacOS 系统设置中的登录项可以查看到此数据库的进程信息，如下图所示：可以通过 psql --version 命令来查看是否成功安装，停止 Postgres 只需要使用下面命令停止进程：brew services stop postgresql@14如果是使用 Linux 环境可以在官网查看具体安装步骤文档，但是要注意的是在 Linux 系统环境下 Postgres 采用的系统用户的方式进行管理，默认超级用户是一个名为 postgres 用户，需要使用下面的命令切换：# 切换换默认超级管理员用户登录到数据库中 sudo -u postgres psql # 切换用户 sudo -i -u postgres默认使用端口 5432 ，默认的 Postgres 用户是 postgres ，不需要密码进行身份验证因此如果需要添加密码，我们必须首先以 postgres 用户身份登录并连接。修改密码需要注意的如果要修改默认用户的密码必须有足够的权限，如果这里出现了默认权限不够情况，可以在 /opt/homebrew/var/postgresql@14/pg_hba.conf 配置文件中进行修改，身份验证配置文件是身份验证规则列表：# "local" is for Unix domain socket connections only local all all md5 # IPv4 local connections: host all all 127.0.0.1/32 md5 # IPv6 local connections: host all all ::1/128 md5 # Allow replication connections from localhost, by a user with the # replication privilege. local replication all md5 host replication all 127.0.0.1/32 md5 host replication all ::1/128 md5上方的配置项是要求每次进行连接的时必须要进行鉴权，只要鉴权成功之后才能连接，默认的 trust 代表可信的连接，意思是没有密码也行为没有校验过程，这里全部修改为 md5 的方式了，意思是密码会以 md5 形式传送给数据库较安全，现在就可以在 psql 命令行提示符下建立了到 Postgres 的连接，并且输入一下命令来更改默认用户的密码：postgres=# ALTER USER dings PASSWORD 'myPassword'; ALTER ROLEPostgres 还有另外一种方式可以修改默认的账号密码，在命令行提示符下使用 \password postgres 来给 postgres 用户设置密码，效果是一样的。另外如果是生产环境数据库部署在网络云主机环境中，这时还需要设置允许远程连接，在配置文件 postgresql.conf 中修改配置如下：listen_addresses = '*'重启 Postgres 后生效，这时候任何远程 IP 都可以连接。使用 Homebrew 默认安装的软件会开机自动启动，如果要禁止自动启动需要执行下面这条命令，如果不知道对应软件位置可以使用 brew services list 命令进行查看，然后使用如下命令禁止开机自启动：launchctl unload -w ~/Library/LaunchAgents/homebrew.xxxx.xxx.plist其 launchctl unload -w <路径> 的作用为停止并卸载和禁用任务，该任务将不会在下次登录时重新启动。操作命令作为一名开发人员最常也是操作数据库里面的数据信息，这时就需要掌握一些基本 DBA 命令，下面是我列出的一些常用的运维操作命令：# 创建一个用户名为 postgres 用户并且设置密码 createuser postgres -P # 为 postgres 用户创建一个名为 test_db 的数据库编码为utf8 createdb -Opostgres -Eutf8 test_db # 删除一个数据库 dropdb test_db; # 创建新表 CREATE TABLE user(name VARCHAR(20), signup_date DATE); # 插入数据 INSERT INTO user_info(name, signup_date) VALUES('张三', '2023-01-01'); # 查询记录 SELECT * FROM user_info; # 更新数据 UPDATE user_info set name = '李四' WHERE name = '张三'; # 删除记录 DELETE FROM user_info WHERE name = '李四' ; # 添加字段 ALTER TABLE user_info ADD email VARCHAR(40); # 更改字段类型 ALTER TABLE user_info ALTER COLUMN signup_date SET NOT NULL; # 设置字段默认值（注意字符串使用单引号） ALTER TABLE user_info ALTER COLUMN email SET DEFAULT 'x@example.com'; # 去除字段默认值 ALTER TABLE user_info ALTER email DROP DEFAULT; # 重命名字段 ALTER TABLE user_info RENAME COLUMN signup_date TO signup; # 删除字段 ALTER TABLE user_info DROP COLUMN email; # 表重命名 ALTER TABLE user_info RENAME TO backup_tbl; # 删除表 DROP TABLE IF EXISTS backup_tbl; # 删除库 DROP DATABASE IF EXISTS hello;还有一些常见命令行提示命令说明：\l 列出所有数据库 \c 选择切换数据库 \password 设置密码 \q 退出 \h 查看SQL命令的解释，比如\h select \? 查看psql命令列表 \c [database_name] 连接其他数据库 \d 列出当前数据库的所有表格 \d [table_name] 列出某一张表格的结构 \du 列出所有用户 \e 打开文本编辑器 \conninfo 列出当前数据库和连接的信息其他资料Install PostgreSQL LinuxHow to Set the Default User Password in PostgreSQLPostgreSQL与MySQL的比较PostgreSQL Data Types
在前面的分析 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
在数据库事务中如何保证数据一致性这也是给开发者带来许多的挑战，我们将现实生活中的东西抽象成数据库的关系模型，存放在数据表中，但是现实生活中某些东西有一些约束的，例如付款不能出现负数，人民币最大的面值是 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 的效果。触发器缺点触发器不是没有缺点的，虽然能解决一部分业务场景下的问题，但是需要注意合理使用。例如一些变态的需求，假设一个系统有张用户表里面存储用户账户和余额信息，需求要让每个用户在完成每笔交易之后都会对其让整个所有用户余额减去用户支出金额，然后看整体系统的金额是否相同，以此来检查整体系统金额完整性。这个需求就不适合触发器，因为每笔交易都要对所有的数据记录进行操作，如果数据量过大，会导致整体每笔交易异常缓慢，所以要分业务场景合理使用触发器。其他资料MySQL / MariaDB 触发器应用场景
数据库系统核心就是存储引擎，前面的文章我写过日志类型的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 行记录格式
事务作为每个数据库提供的一层高度抽象机制，保障着数据的安全性和一致性和持久化，这时每个数据库必备的功能；只是不同类型的数据库实现可能存在某种差异化，有了事务的保障应用层程序才能简单处理一些复杂的业务逻辑，如果没有数据库的事务保障那么对于一个数据库来说这时一个很难解决的问题，程序要面对进程崩溃和网络中断，并发数据竞争问题。最重要的是数据库要面对多个客户端连接的操作，此时客户端完全在异步环境下操作的，时间是固定的，但是每个客户端操作事务可能是异步进行，当在某个时间段内处理多个事务就会面对其并行事务问题，让异步的操作变成同步操作。本篇文章对几个场景的事务处理方式举例，表明事务对于这些场景的重要性。读提交事务例如一个计数器场景用户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