一、从日常开发痛点看BSON的诞生
很多做过Web开发的朋友都有过这样的经历:存用户信息时,要把用户名、年龄、地址这些零散的数据凑成一个整体往数据库里塞,要么拆成一堆字段,要么转成一串长文本。拆字段的话,要是用户新增了“爱好”“紧急联系人”这类属性,就得改数据库表结构,改不好还会影响线上服务;转成文本的话,取数据时又得专门解析,麻烦得很。后来大家都开始用JSON存数据,因为JSON不用提前定义结构,想加什么字段就加什么字段,改起来方便。但用久了就会发现问题:比如要快速找“年龄大于30岁的用户”,JSON得先把整个字符串转成对象才能提取年龄,效率特别低;存数字、日期这些类型时,JSON还会把它们当成字符串处理,不仅占空间,还容易出精度问题。
MongoDB的开发者就是盯着这些痛点,搞出了BSON这个东西。说白了BSON就是专门给数据库用的二进制版JSON,它把JSON的灵活性和二进制的高效性结合在了一起,既不用提前定义表结构,又能快速处理各种数据类型。
1.1 为什么JSON不适合直接存数据库?
举个实际的例子,假设要存一个用户的基本信息,用JSON写出来是这样的:
{
"userId": 1001,
"userName": "张三",
"age": 28,
"registerTime": "2024-01-15T10:30:00Z",
"address": {
"province": "广东省",
"city": "深圳市"
},
"tags": ["前端开发", "MongoDB", "开源"]
}
这个JSON字符串看起来很清楚,但它本质上是一串纯文本,数据库要处理它得做这些事:第一,解析整个字符串才能拿到里面的字段;第二,所有内容都是字符,比如userId是数字1001,在JSON里是四个字符“1001”,占4个字节,要是用二进制存整数,只需要4个字节(32位整数),看起来一样,但二进制更紧凑;第三,日期是字符串,要比较日期大小的话,得先转成时间戳再比,慢很多。
而BSON就是把这些问题都解决了,它把每个字段的类型、长度都提前标好,数据库不用解析整个内容,直接就能定位到要找的字段,速度快很多。
二、BSON的核心底层机制拆解
要搞懂BSON为什么快,得先知道它到底是怎么存数据的。BSON的全称是Binary JSON,也就是二进制JSON,它的核心设计思路是“自描述”——每个字段都自带自己的类型、长度和值,数据库拿到一个BSON数据,不用提前知道结构,就能把它拆成一个个字段。
2.1 BSON的基本结构
一个完整的BSON文档,最外层是一个“对象”类型,整体结构可以分成三部分:
- 文档总长度:用4个字节的整数表示整个BSON文档占多少字节,数据库拿到后先读这4个字节,就知道要读多少内容了;
- 字段列表:每个字段都按“类型+字段名+值”的顺序排列;
- 结束标记:最后用一个0x00的字节表示文档结束。
这里的类型是BSON专门定义的,比如32位整数是0x10,字符串是0x02,日期是0x09,嵌套对象是0x03,数组是0x04,布尔值是0x08。每个类型都有固定的存储规则,比如字符串是“长度+内容+结束标记”,长度用4个字节表示,内容是UTF-8编码的字符,最后加一个0x00的结束字节。
2.2 常用数据类型的BSON存储示例
为了更清楚,我们用刚才的用户JSON转成BSON的过程来举例,技术栈统一用Node.js,因为Node.js自带BSON解析库,能直接生成BSON数据。
首先写一段Node.js代码,把用户JSON转成BSON,再打印出每个字段的二进制内容:
// 技术栈:Node.js 18.x + BSON库(npm install bson)
const BSON = require('bson');
// 定义用户数据
const userData = {
userId: 1001, // 32位整数
userName: "张三", // 字符串
age: 28, // 32位整数
registerTime: new Date("2024-01-15T10:30:00Z"), // 日期
address: { // 嵌套对象
province: "广东省",
city: "深圳市"
},
tags: ["前端开发", "MongoDB", "开源"] // 数组
};
// 把JS对象转成BSON Buffer
const bsonBuffer = BSON.serialize(userData);
// 把BSON Buffer转成十六进制字符串,方便查看
const bsonHex = bsonBuffer.toString('hex');
console.log('完整BSON十六进制:', bsonHex);
运行这段代码后,得到的BSON十六进制可以拆解成各个字段:
- 开头4个字节是总长度:比如得到的开头是“9d000000”,转成十进制是157,说明整个BSON文档占157个字节;
- 第一个字段userId:类型是0x10(32位整数),字段名是“userId”,后面跟一个0x00结束,然后是值1001的二进制(0xe9030000);
- 第二个字段userName:类型是0x02(字符串),字段名是“userName”,后面跟0x00,然后是字符串长度(0x06000000,转成十进制是6,说明“张三”占6个字节,因为一个中文占3个UTF-8字节),然后是“张三”的UTF-8编码(e5bca0e4b889),最后加0x00结束;
- 日期字段registerTime:类型是0x09(日期),字段名是“registerTime”,后面跟0x00,然后是日期转成的时间戳(2024-01-15T10:30:00Z的时间戳是1705309800000,转成8字节的二进制就是00e41d0f82010000);
- 嵌套对象address:类型是0x03(嵌套对象),字段名是“address”,后面跟0x00,然后是嵌套对象的总长度,再是嵌套对象内部的字段,最后加0x00结束;
- 数组tags:类型是0x04(数组),字段名是“tags”,后面跟0x00,然后是数组的总长度,数组内部的字段名是数字字符串(比如第一个元素的字段名是“0”,第二个是“1”),最后加0x00结束;
- 最后一个字节是0x00,表示整个文档结束。
从这个拆解能看出来,BSON的每个字段都有明确的边界,数据库要找userId,只要找到类型是0x10、字段名是userId的位置,就能直接拿到值,不用解析整个文档,速度自然快。
三、BSON的应用场景和技术特性
BSON不是通用的二进制格式,它是专门为MongoDB这类文档型数据库设计的,所以它的应用场景和技术特性都围绕着“文档存储”展开。
3.1 核心应用场景
BSON最核心的应用场景就是MongoDB的数据存储,包括用户数据、商品数据、日志数据这些非结构化或半结构化数据的存储。比如电商网站的商品信息,不同商品有不同的属性,手机有“CPU”“内存”,衣服有“尺码”“颜色”,用BSON存的话,不用提前定义表结构,想加什么属性就加什么属性,非常灵活。
除了MongoDB,BSON还被用到一些中间件和缓存系统里,比如用BSON做序列化格式,存到Redis里,因为BSON比JSON小,解析速度快,能提高缓存的读写效率。比如用Node.js把BSON存到Redis的代码:
// 技术栈:Node.js 18.x + BSON库 + Redis库(npm install bson redis)
const BSON = require('bson');
const redis = require('redis');
// 连接Redis
const client = redis.createClient({
url: 'redis://localhost:6379'
});
client.on('error', err => console.log('Redis错误:', err));
// 定义商品数据
const productData = {
productId: 2001,
productName: "华为Mate60",
price: 4999,
specs: {
cpu: "麒麟9000S",
ram: "8GB"
}
};
// 把商品数据转成BSON
const bsonBuffer = BSON.serialize(productData);
// 存到Redis
client.connect().then(() => {
client.set('product:2001', bsonBuffer);
console.log('BSON数据已存到Redis');
});
3.2 BSON的技术优缺点
BSON的优点很明显: 第一,存储效率高。BSON用二进制存数字、日期这些类型,比JSON的文本格式小很多,比如一个32位整数,JSON存占4个字符(比如“1001”占4个字节),BSON存占4个字节(和JSON一样大,但如果是64位整数,JSON存占最多20个字符,BSON存只占8个字节);一个日期,JSON存占20多个字符,BSON存只占8个字节,节省了很多存储空间。 第二,解析速度快。BSON的自描述结构让数据库不用解析整个文档就能定位到字段,比如要找“年龄大于30岁的用户”,MongoDB只要找到类型是0x10、字段名是age的位置,直接拿值比较,不用解析整个JSON字符串,速度比JSON快很多。 第三,支持更多数据类型。BSON支持JSON没有的类型,比如日期、二进制数据、Decimal128(高精度小数),这些类型在数据库里非常有用,比如存金额用Decimal128不会有精度问题,存图片用二进制数据不用转成Base64字符串。
BSON的缺点也有: 第一,可读性差。BSON是二进制格式,人眼没法直接读,要解析成JSON才能看,不像JSON直接就能看懂。 第二,存储小字段占空间。比如一个字段值是1,JSON存占2个字符(“1”占1个字节,加引号占1个字节),BSON存要加类型(1个字节)、字段名(比如“age”占3个字节,加结束标记1个字节)、值(4个字节),总共占9个字节,比JSON大很多,所以BSON适合存大的文档,小字段多的文档反而不划算。 第三,兼容性差。BSON是MongoDB主导的格式,不像JSON是通用的,很多其他数据库不支持BSON,比如MySQL、PostgreSQL只能存JSON,不能直接存BSON。
四、BSON使用的注意事项
用BSON的时候,有几个地方要特别注意,不然会踩坑。
4.1 数据类型的兼容性
不同语言的BSON实现可能有细微的差别,比如Java的BSON实现和Node.js的BSON实现,对日期的处理可能不一样,Java的日期是从1970年开始的毫秒数,Node.js的也是,但如果是自定义的类型,可能会有问题。所以跨语言用BSON的时候,一定要用官方推荐的BSON库,比如Java用MongoDB官方的bson库,Python用pymongo自带的bson库,不要自己写序列化代码,不然会出现解析错误。
比如Python存一个日期,用pymongo转成BSON,再用Node.js解析,代码是这样的:
# 技术栈:Python 3.10 + pymongo库(pip install pymongo)
from pymongo import MongoClient
from datetime import datetime
# 连接MongoDB
client = MongoClient('mongodb://localhost:27017/')
db = client.test_db
collection = db.test_collection
# 存一个带日期的文档
doc = {
"userId": 1002,
"userName": "李四",
"registerTime": datetime(2024, 2, 1, 10, 0, 0)
}
collection.insert_one(doc)
print('文档已存入MongoDB')
然后用Node.js读出来,解析成BSON:
// 技术栈:Node.js 18.x + mongodb库(npm install mongodb)
const { MongoClient } = require('mongodb');
// 连接MongoDB
const client = new MongoClient('mongodb://localhost:27017/');
client.connect().then(async () => {
const db = client.db('test_db');
const collection = db.collection('test_collection');
// 读取文档
const doc = await collection.findOne({ userId: 1002 });
console.log('解析后的文档:', doc);
console.log('日期类型:', typeof doc.registerTime); // 输出object,是Date类型
});
这样跨语言就能正确解析日期,要是自己写序列化代码,可能会把日期转成字符串,导致类型错误。
4.2 大文档的存储限制
BSON文档有大小限制,MongoDB里的BSON文档最大不能超过16MB,超过的话会报错。所以存大文档的时候,比如存一个包含很多字段的商品数据,或者存一个大的日志,要注意拆分,比如把大的内容拆成多个小文档,或者用GridFS存超过16MB的文件。
比如存一个大的商品描述,超过16MB的话,就不能直接存到BSON文档里,要把描述拆成多个部分,或者用GridFS存描述的内容,代码示例:
// 技术栈:Node.js 18.x + mongodb库
const { MongoClient, GridFSBucket } = require('mongodb');
// 连接MongoDB
const client = new MongoClient('mongodb://localhost:27017/');
client.connect().then(async () => {
const db = client.db('test_db');
// 创建GridFS Bucket
const bucket = new GridFSBucket(db, { bucketName: 'files' });
// 大的商品描述内容
const bigDescription = Buffer.from('很长的商品描述...'); // 假设内容超过16MB
// 把描述存到GridFS
const uploadStream = bucket.openUploadStream('product:2002:description');
uploadStream.write(bigDescription);
uploadStream.end();
console.log('大描述已存到GridFS');
});
4.3 字段名的长度限制
BSON的字段名不能太长,虽然没有明确的最大长度,但太长的字段名会占用很多存储空间,比如字段名是“user_register_time_in_2024”,占30多个字节,要是有很多这样的字段,整个文档的大小会增加很多,所以字段名尽量简洁,比如用“regTime”代替“user_register_time_in_2024”。
五、文章总结
BSON作为MongoDB的核心存储格式,它的设计完全围绕着文档型数据库的需求,解决了JSON存储效率低、解析速度慢的问题,同时保留了JSON的灵活性。它的自描述结构让数据库能快速定位字段,提高了读写效率;支持更多的数据类型,满足了数据库的各种存储需求;二进制的格式让存储更紧凑,节省了存储空间。
当然BSON也不是万能的,它适合存大的半结构化或非结构化数据,不适合存小字段多的文档;它是MongoDB的专属格式,兼容性不如JSON。但在MongoDB的场景下,BSON是最优的存储格式,它的底层机制决定了MongoDB的高性能和灵活性。
对于开发者来说,了解BSON的底层机制,能更好地使用MongoDB,比如设计字段时尽量简洁,避免存超过16MB的大文档,跨语言用BSON时用官方推荐的库,这些都能提高开发效率,避免踩坑。
Comments