【避坑指南】从 TinyDB 文件损坏,聊聊文件截断与磁盘刷盘的底层原理

Share
【避坑指南】从 TinyDB 文件损坏,聊聊文件截断与磁盘刷盘的底层原理
Photo by Shubham Dhage / Unsplash

在 Python 轻量级开发中,TinyDB 因其“零部署、文件即数据库、支持对象化查询”的特性,成为了存储配置信息、多租户元数据的神器。

但在高频写入或异常崩溃的场景下,你是否遇到过这样的诡异现象:导出的 JSON 文件末尾莫名其妙多出了几个 NULNUL\x00)空字节,导致整个数据库报 JSON 无法解析的错误?

本文将带你还原这个经典的“文件空洞”Bug,并分享如何通过自定义存储类 MyJSONStorage 彻底解决它。

一、 现象还原:消失的尾巴与诡异的 NUL

在默认情况下,TinyDB 的 JSONStorage 是这样写入文件的:

  1. seek(0) 指针回到文件开头。
  2. write(json_data) 写入序列化后的 JSON 字符串。
  3. truncate() 截断文件,切掉因为新数据变短而残留的旧数据。

看似完美的逻辑,在实际并发、异常中断、或者操作系统缓存延迟落盘时,会触发一个底层漏洞:文件空洞(File Hole)

write() 写入的数据还停留在操作系统的 Page Cache(内存缓存) 中,而没有真正刷新到物理磁盘时,如果直接调用 truncate(),或者指针由于并发发生错乱,操作系统为了填补“指针所在位置”与“实际落盘位置”之间的空白,就会自动填充 \x00(即 NUL 字符)。

最终的结果就是:你的 JSON 文件末尾多了一串不可见的乱码,下次读取时数据库直接崩溃。

二、 终结者:自定义 MyJSONStorage

为了彻底解决这个问题,我们需要重写 TinyDB 的存储引擎,引入强一致性硬刷盘机制。以下是核心实现代码:

Python

import io
import json
import os
from tinydb.storages import JSONStorage

class MyJSONStorage(JSONStorage):
    def write(self, data):
        try:
            # 1. 移动指针到开头
            self._handle.seek(0)

            # 2. 序列化 JSON 数据
            serialized = json.dumps(data, **self.kwargs)

            # 3. 写入数据
            try:
                self._handle.write(serialized)
            except io.UnsupportedOperation:
                raise IOError(
                    f'Cannot write to the database. Access mode is "{self._mode}"'
                )

            # ======= 核心改进 1:强制刷盘与同步 =======
            self._handle.flush()               # 刷出 Python 用户态缓冲区
            os.fsync(self._handle.fileno())    # 强行触发内核态物理刷盘(关键!)

            # ======= 核心改进 2:精准位置截断 =======
            self._handle.truncate()
        except Exception as e:
            self._handle.truncate()
            raise e
        finally:
            # ======= 核心改进 3:极端异常兜底 =======
            self._handle.truncate()
            
db = TinyDB( "db.json", storage=MyJSONStorage)

改进原理解析:

  1. self._handle.flush() + os.fsync()(核心大招): 普通的 write 只是把数据交给了操作系统,操作系统什么时候写到硬盘全看心情。os.fsync() 是一个重量级的系统调用,它会阻塞线程,强制让硬件磁头或 SSD 将数据瞬间物理落地
  2. 绝对精准的 truncate(): 正因为数据已经 100% 落地,此时的文件指针(Cursor)精准地停留在最新 JSON 字符串的最后一个字符后面。这时执行不带参数的 truncate(),可以完美地把后面残存的旧数据切掉,不多不少,绝不留空洞
  3. 三重 truncate() 异常兜底: 无论在写入时发生什么级别的崩溃(try 块报错、except 捕获、还是最后退出的 finally),统统强制执行一次 truncate(),彻底斩断 NUL 字符生成的可能性。

三、 为什么 TinyDB 官方不默认这么改?

你可能会问:“既然这个方法这么好,为什么 TinyDB 官方不把它内置进 JSONStorage 呢?”

这触及到了开源库的设计哲学(Trade-offs)

  • 性能代价太高os.fsync() 是一个非常昂贵的操作,它需要等待物理硬件响应。一旦默认开启,TinyDB 的连续写入性能可能会暴跌 10~100 倍
  • 定位不同:TinyDB 官方给它的定位就是“追求极致轻量、快速”。它刻意保持了几百行的精简源码,并将这种高级的数据安全性需求,留给开发者通过 “自定义存储(Custom Storage)” 独立扩展。

四、 最佳实践场景:什么时候该用它?

既然有性能损耗,我们就不应该盲目滥用。在实际项目中,我们推荐将数据进行动静分离 / 轻重分离

  • 不适合使用 TinyDB / MyJSONStorage 的场景: 高频写入的用户流水、日志系统、涉及多表联查的业务订单系统。这类场景请老老实实上 PostgreSQL / MySQL
  • 最适合使用 TinyDB + MyJSONStorage 的场景: 系统的全局配置多租户路由元数据(Tenant Info)、桌面应用的本地数据。这类数据总量极小、修改频率极低(往往几天才改一次),但对数据的完整性、准确性要求达到了 100%。用轻量级的 TinyDB 配合硬刷盘,既省去了部署大数据库的麻烦,又兜住了数据绝不损坏的底线。

总结

在底层的世界里,没有魔法。一行小小的 os.fsync(),虽然牺牲了一点点写入性能,却为我们的轻量级数据库加上了一道坚固的保险锁。如果你的 TinyDB 也正在饱受文件损坏的困扰,不妨试试把 JSONStorage 替换为 MyJSONStorage 吧!

Read more

阿里云服务器科学上网架构升级指南

阿里云服务器科学上网架构升级指南

本手册旨在指导高级用户将其阿里云海外实例(以硅谷节点为例)的代理服务从传统 Shadowsocks (SS) 协议迁移至 VLESS-REALITY 架构。该方案的核心价值在于:通过模拟合法的 TLS 流量(如访问苹果或微软官网),在保障高速访问的同时,极大地降低了因协议特征被识别而导致 IP 被封锁的风险,从而保护服务器上并存的 Web 服务(如个人博客、简历等)不受牵连。 1. 协议演进:为何弃用 Shadowsocks 转向 VLESS-REALITY随着防火墙(GFW)对加密流量识别能力的提升,传统协议的生存空间已被严重压缩。下表从架构视角对比了两种方案的差异: 维度Shadowsocks (SS)VLESS-REALITY流量特征具有高度可识别的加密特征。无特征:完全模拟合法的 HTTPS 握手流量。安全性容易触发协议主动探测。防主动探测:通过目标网站(Dest)证书链校验。IP 封锁风险极高:一旦识别,IP 往往被阻断。

By 樊泽豪
搭建K3s集群,零成本打造生产级私有云

搭建K3s集群,零成本打造生产级私有云

简介 用一套经典的云原生“黄金组合”( K3s + Rancher + Longhorn + MetalLB),在本地裸机上构建具备调度、高可用存储、独立网络与可视化运维的完整私有云基础设施。打磨出媲美公有云的生产级 K8s 体验。 ┌──────────────────────────────────────────────────────────┐ │ Rancher Web 控制台 (管理与可视面) │ └──────────────────────────┬───────────────────────────────┘ │ 统一管控 ┌──────────────────────────▼───────────────────────────────┐ │ K3s 集群 (容器编排内核) │ ├──────────────────────────┬─────────────────────────────

By 樊泽豪
Docker常用操作

Docker常用操作

安装 Windows安装Docker到F盘(非系统盘) Start-Process -FilePath 'Docker_Desktop_Installer.exe' -Wait -ArgumentList "install --installation-dir=F:\DockerDesktop" 改变容器、镜像文件位置 以管理员权限启动Docker Desktop,Settings-Resources-Disk iamge location 配置dockerhub国内源 阿里云:容器镜像服务 (aliyun.com) 其他源 more /etc/docker/daemon.json 输入以下文件: { "registry-mirrors": [ "https://kk8u6omk.mirror.aliyuncs.com", "https:

By 樊泽豪
如何在 Ghost 博客中添加类似 Word 的左侧固定目录(纯干货)

如何在 Ghost 博客中添加类似 Word 的左侧固定目录(纯干货)

当我们在 Ghost 博客中撰写长文时,一个类似 Word 导航窗格的目录能极大提升读者的阅读体验。本文将分享如何通过 Code Injection(代码注入) 快速实现一个完全静止、不随点击乱跳、长标题自动换行的优雅左侧悬浮目录。 实现效果 * 完全固定: 目录稳居文章左侧,只有正文跟随鼠标滚动。 * 绝对静止: 修复了常见插件点击目录项时,目录自身会产生二次跳动的痛点。 * 清晰完整: 长标题自动换行显示,绝不裁剪文字。 * 移动端友好: 在手机或平板等小屏幕上自动隐藏,防止遮挡正文。 部署步骤 无需修改任何主题源文件,只需登录 Ghost 后台,进入 Settings -> Code Injection(代码注入),将以下两段代码全选覆盖粘贴即可。 1. 注入到 【Site Header】 在 Site Header 框中复制并完全粘贴以下代码(包含 Tocbot 官方样式与自定义微调

By 樊泽豪