10、BLOB大对象存储:二进制数据存储、图片/文件入库、读取与还原

说实话,很多刚入行的朋友一听到BLOB就头大。觉得二进制数据嘛,不就是一堆0和1,存进去读出来不就完了?嗯,道理是这个道理,但真做起来,坑可不少。

我个人习惯把BLOB比作一个黑箱子。你把文件扔进去,它帮你存着。但怎么扔、怎么取、取出来是不是原封不动,这里头就有讲究了。今天咱们就把这个黑箱子拆开看看。

什么是BLOB?

BLOB全称是Binary Large Object,翻译过来就是二进制大对象。说白了,就是用来存那些不能用普通文本表示的数据。比如图片、音频、视频、压缩包、Word文档等等。

在SQLite里,BLOB类型就是直接存二进制字节流。没有编码转换,没有字符集问题。你给什么,它就存什么。这一点跟TEXT类型有本质区别。

类型 存储内容 典型用途
TEXT 字符串(UTF-8/16) 文本、JSON、XML
BLOB 原始字节流 图片、文件、加密数据

我在项目中遇到过有人用TEXT存图片,结果存进去再读出来,图片就花了。为什么?因为TEXT会做编码转换,把二进制数据当字符串处理,某些字节就被"修正"了。所以记住:二进制数据,必须用BLOB。

BLOB的存储方式

SQLite对BLOB的处理其实很直接。它不关心你存的是什么,只关心字节长度。你传一个字节数组进去,它就原样存下来。读取时也是原样返回。

但这里有个性能问题。SQLite默认每个BLOB最大是1GB。听起来很大对吧?但如果你频繁读写几百MB的大文件,数据库文件会膨胀得很快。我个人建议,超过10MB的文件,还是存文件路径更合适。数据库里只存路径字符串,文件放磁盘上。

经验之谈: 小文件(<10MB)用BLOB直接存,大文件存路径。这是很多嵌入式系统的通用做法。

图片入库:从文件到BLOB

咱们来看一个实际例子。把一张图片存到SQLite里,需要三步:打开文件、读取字节、绑定到SQL语句。

// 图片入库示例
int store_image(sqlite3 *db, const char *filename) {
    sqlite3_stmt *stmt;
    FILE *fp;
    long file_size;
    unsigned char *buffer;
    
    // 1. 打开文件
    fp = fopen(filename, "rb");
    if (!fp) return -1;
    
    // 2. 获取文件大小
    fseek(fp, 0, SEEK_END);
    file_size = ftell(fp);
    rewind(fp);
    
    // 3. 分配缓冲区并读取
    buffer = (unsigned char*)malloc(file_size);
    fread(buffer, 1, file_size, fp);
    fclose(fp);
    
    // 4. 准备SQL语句
    const char *sql = "INSERT INTO images(name, data) VALUES(?, ?)";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    
    // 5. 绑定文件名和BLOB数据
    sqlite3_bind_text(stmt, 1, filename, -1, SQLITE_TRANSIENT);
    sqlite3_bind_blob(stmt, 2, buffer, file_size, SQLITE_TRANSIENT);
    
    // 6. 执行
    sqlite3_step(stmt);
    sqlite3_finalize(stmt);
    free(buffer);
    
    return 0;
}

注意看sqlite3_bind_blob这个函数。它的最后一个参数是析构回调。用SQLITE_TRANSIENT表示SQLite会自己拷贝一份数据。如果你用SQLITE_STATIC,那你就得保证buffer在SQL语句执行完之前一直有效。

曾经踩过的坑: 我刚开始用SQLITE_STATIC,然后紧接着就free了buffer。结果SQLite执行时读到了野指针,程序直接崩溃。后来就老老实实用SQLITE_TRANSIENT了。

读取与还原:从BLOB到文件

读取BLOB数据,核心是sqlite3_column_blobsqlite3_column_bytes。前者拿数据指针,后者拿数据长度。

// 读取BLOB并还原为文件
int read_image(sqlite3 *db, int id, const char *output_path) {
    sqlite3_stmt *stmt;
    FILE *fp;
    
    const char *sql = "SELECT name, data FROM images WHERE id = ?";
    sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
    sqlite3_bind_int(stmt, 1, id);
    
    if (sqlite3_step(stmt) == SQLITE_ROW) {
        // 获取BLOB数据
        const void *blob_data = sqlite3_column_blob(stmt, 1);
        int blob_size = sqlite3_column_bytes(stmt, 1);
        
        // 写入文件
        fp = fopen(output_path, "wb");
        fwrite(blob_data, 1, blob_size, fp);
        fclose(fp);
        
        printf("还原成功,大小:%d 字节\n", blob_size);
    }
    
    sqlite3_finalize(stmt);
    return 0;
}

这里有个细节:sqlite3_column_blob返回的指针指向的是SQLite内部缓冲区。你必须在调用sqlite3_step获取下一行之前,把数据用完或者拷贝出来。否则指针就失效了。

BLOB操作的性能考量

你想想看,如果一张表里存了几百张图片,每张几MB。那么查询时,即使你只想要文件名,SQLite也得把整个BLOB加载到内存里。这显然不合理。

我的做法是:把BLOB单独放一张表,用外键关联。查询主表时不加载BLOB,只有需要时才去读BLOB表。

-- 推荐的表结构
CREATE TABLE file_meta (
    id INTEGER PRIMARY KEY,
    name TEXT,
    size INTEGER,
    mime_type TEXT
);

CREATE TABLE file_data (
    id INTEGER PRIMARY KEY,
    meta_id INTEGER UNIQUE,
    content BLOB,
    FOREIGN KEY (meta_id) REFERENCES file_meta(id)
);

这样设计的好处是:列表查询只扫file_meta表,速度快。只有用户点击下载时,才去读file_data表里的BLOB。

BLOB的增量读写

SQLite还支持增量读写BLOB。什么意思呢?就是你可以只修改BLOB的一部分,不用把整个BLOB读出来再写回去。这个功能叫Incremental BLOB I/O。

我记得有一次做固件升级功能,需要往数据库里的BLOB末尾追加数据。如果用传统方式,得先把整个BLOB读出来,追加,再写回去。几十MB的数据,来回拷贝,效率极低。

用增量I/O就简单了:

// 增量写入BLOB
sqlite3_blob *blob_handle;
int rc = sqlite3_blob_open(db, "main", "file_data", "content", 
                           row_id, 1, &blob_handle);

if (rc == SQLITE_OK) {
    // 从偏移量100开始写入1024字节
    sqlite3_blob_write(blob_handle, data_buffer, 1024, 100);
    sqlite3_blob_close(blob_handle);
}

这个API直接操作数据库文件中的BLOB区域,避免了数据在应用层和数据库层之间的拷贝。对于大文件处理,性能提升非常明显。

避坑指南

做BLOB开发这几年,我总结了几条血泪教训:

  • 不要用TEXT存二进制——编码转换会破坏数据,这是最基础的坑
  • 大文件别直接存BLOB——超过50MB的文件,数据库性能会急剧下降
  • 记得设置缓存大小——PRAGMA cache_size调大一些,BLOB读写会快很多
  • 注意内存占用——读取大BLOB时,确保有足够的内存,否则程序会OOM
  • 事务要合理使用——批量插入BLOB时,用事务包裹,否则每条记录都触发一次磁盘写入
小技巧: 如果你不确定BLOB数据是否完整,可以在写入时计算MD5或SHA256,存到元数据表里。读取时再校验一次。这样能确保数据在存储和传输过程中没有被篡改。

知识体系总览

下面这张图把BLOB的核心知识点串起来了。从存储方式到读写策略,再到性能优化,你可以对照着梳理自己的理解。

BLOB大对象存储 存储方式 直接存储:小文件 < 10MB 路径存储:大文件 > 10MB 读写操作 bind_blob / column_blob 增量I/O:blob_open/write 性能优化 表分离:元数据与数据分离 缓存调优:cache_size 常见陷阱 TEXT存二进制 → 数据损坏 数据校验 MD5/SHA256 完整性校验 事务管理 批量插入用事务包裹

BLOB这东西,说难不难,说简单也不简单。关键是要理解它的本质——就是一堆字节。你把它当文件处理,用fread/fwrite的思路去理解,就顺了。别被"大对象"三个字吓到,它就是个存储容器而已。

好了,这一章的内容就到这儿。代码示例都是我在实际项目中验证过的,你可以直接拿去用。遇到问题,多看看SQLite官方文档,那里写得其实挺清楚的。

公众号:蓝海资料掘金营,微信deep3321