1. BSON协议与数据类型
MongoDB为什么会使用BSON?
JSON是当今非常通用的一种跨语言Web数据交互格式,属于ECMAScript标准规范的一个子集。JSON(JavaScript Object Notation, JS对象简谱)即JavaScript对象表示法,它是JavaScript对象的一种文本表现形式。
作为一种轻量级的数据交换格式,JSON的可读性非常好,而且非常便于系统生成和解析,这些优势也让它逐渐取代了XML标准在Web领域的地位,当今许多流行的Web应用开发框架,如SpringBoot都选择了JSON作为默认的数据编/解码格式。
JSON只定义了6种数据类型:
大多数情况下,使用JSON作为数据交互格式已经是理想的选择,但是JSON基于文本的解析效率并不是最好的,在某些场景下往往会考虑选择更合适的编/解码格式,一些做法如:
BSON由10gen团队设计并开源,目前主要用于MongoDB数据库。BSON(Binary JSON)是二进制版本的JSON,其在性能方面有更优的表现。BSON在许多方面和JSON保持一致,其同样也支持内嵌的文档对象和数组结构。二者最大的区别在于JSON是基于文本的,而BSON则是二进制(字节流)编/解码的形式。在空间的使用上,BSON相比JSON并没有明显的优势。
MongoDB在文档存储、命令协议上都采用了BSON作为编/解码格式,主要具有如下优势:
BSON的数据类型
MongoDB中,一个BSON文档最大大小为16M,文档嵌套的级别不超过100
https://www.mongodb.com/docs/v6.0/reference/bson-types/
Type |
Number |
Alias |
Notes |
Double |
1 |
"double" |
|
String |
2 |
"string" |
|
Object |
3 |
"object" |
|
Array |
4 |
"array" |
|
Binary data |
5 |
"binData" |
二进制数据 |
Undefined |
6 |
"undefined" |
Deprecated. |
ObjectId |
7 |
"objectId" |
对象ID,用于创建文档ID |
Boolean |
8 |
"bool" |
|
Date |
9 |
"date" |
|
Null |
10 |
"null" |
|
Regular Expression |
11 |
"regex" |
正则表达式 |
DBPointer |
12 |
"dbPointer" |
Deprecated. |
JavaScript |
13 |
"javascript" |
|
Symbol |
14 |
"symbol" |
Deprecated. |
JavaScript code with scope |
15 |
"javascriptWithScope" |
Deprecated in MongoDB 4.4. |
32-bit integer |
16 |
"int" |
|
Timestamp |
17 |
"timestamp" |
|
64-bit integer |
18 |
"long" |
|
Decimal128 |
19 |
"decimal" |
New in version 3.4. |
Min key |
-1 |
"minKey" |
表示一个最小值 |
Max key |
127 |
"maxKey" |
表示一个最大值 |
$type操作符
$type操作符基于BSON类型来检索集合中匹配的数据类型,并返回结果。
db.books.find({"title" : {$type : 2}})//或者db.books.find({"title" : {$type : "string"}})
2. 日期类型
MongoDB的日期类型使用UTC(Coordinated Universal Time,即世界协调时)进行存储,也就是+0时区的时间。
db.dates.insertMany([{data1:Date()},{data2:new Date()},{data3:ISODate()}])db.dates.find().pretty()
使用new Date与ISODate最终都会生成ISODate类型的字段(对应于UTC时间)
3. ObjectId生成器
MongoDB集合中所有的文档都有一个唯一的_id字段,作为集合的主键。在默认情况下,_id字段使用ObjectId类型,采用16进制编码形式,共12个字节。
为了避免文档的_id字段出现重复,ObjectId被定义为3个部分:
4字节表示Unix时间戳(秒)。
5字节表示随机数(机器号+进程号唯一)。
3字节表示计数器(初始化时随机)。
大多数客户端驱动都会自行生成这个字段,比如MongoDB Java Driver会根据插入的文档是否包含_id字段来自动补充ObjectId对象。这样做不但提高了离散性,还可以降低MongoDB服务器端的计算压力。在ObjectId的组成中,5字节的随机数并没有明确定义,客户端可以采用机器号、进程号来实现:
属性/方法 |
描述 |
str |
返回对象的十六进制字符串表示。 |
ObjectId.getTimestamp() |
将对象的时间戳部分作为日期返回。 |
ObjectId.toString() |
以字符串文字“”的形式返回 JavaScript 表示ObjectId(...)。 |
ObjectId.valueOf() |
将对象的表示形式返回为十六进制字符串。返回的字符串是str属性。 |
生成一个新的 ObjectId
4. 内嵌文档和数组
内嵌文档
一个文档中可以包含作者的信息,包括作者名称、性别、家乡所在地,一个显著的优点是,当我们查询book文档的信息时,作者的信息也会一并返回。
db.books.insert({ title: "撒哈拉的故事", author: { name:"三毛", gender:"女", hometown:"重庆" }})
查询三毛的作品
db.books.find({"author.name":"三毛"})
修改三毛的家乡所在地
db.books.updateOne({"author.name":"三毛"},{$set:{"author.hometown":"重庆/台湾"}})
数组
除了作者信息,文档中还包含了若干个标签,这些标签可以用来表示文档所包含的一些特征,如豆瓣读书中的标签(tag)
增加tags标签
db.books.updateOne({"author.name":"三毛"},{$set:{tags:["旅行","随笔","散文","爱情","文学"]}})
查询数组元素
# 会查询到所有的tagsdb.books.find({"author.name":"三毛"},{title:1,tags:1})#利用$slice获取最后一个tagdb.books.find({"author.name":"三毛"},{title:1,tags:{$slice:-1}})
$silice是一个查询操作符,用于指定数组的切片方式
数组末尾追加元素,可以使用$push操作符
db.books.updateOne({"author.name":"三毛"},{$push:{tags:"猎奇"}})
$push操作符可以配合其他操作符,一起实现不同的数组修改操作,比如和$each操作符配合可以用于添加多个元素
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"]}}})
如果加上$slice操作符,那么只会保留经过切片后的元素
db.books.updateOne({"author.name":"三毛"},{$push:{tags:{$each:["伤感","想象力"],$slice:-3}}})
根据元素查询
#会查出所有包含伤感的文档db.books.find({tags:"伤感"})# 会查出所有同时包含"伤感","想象力"的文档db.books.find({tags:{$all:["伤感","想象力"]}})
嵌套型的数组
数组元素可以是基本类型,也可以是内嵌的文档结构
{ tags:[ {tagKey:xxx,tagValue:xxxx}, {tagKey:xxx,tagValue:xxxx} ]}
这种结构非常灵活,一个很适合的场景就是商品的多属性表示
一个商品可以同时包含多个维度的属性,比如尺码、颜色、风格等,使用文档可以表示为:
db.goods.insertMany([{ name:"羽绒服", tags:[ {tagKey:"size",tagValue:["M","L","XL","XXL","XXXL"]}, {tagKey:"color",tagValue:["黑色","宝蓝"]}, {tagKey:"style",tagValue:"韩风"} ]},{ name:"羊毛衫", tags:[ {tagKey:"size",tagValue:["L","XL","XXL"]}, {tagKey:"color",tagValue:["蓝色","杏色"]}, {tagKey:"style",tagValue:"韩风"} ]}])
以上的设计是一种常见的多值属性的做法,当我们需要根据属性进行检索时,需要用到$elementMatch操作符:
#筛选出color=黑色的商品信息db.goods.find({ tags:{ $elemMatch:{tagKey:"color",tagValue:"黑色"} }})
如果进行组合式的条件检索,则可以使用多个$elemMatch操作符:
# 筛选出color=蓝色,并且size=XL的商品信息db.goods.find({ tags:{ $all:[ {$elemMatch:{tagKey:"color",tagValue:"黑色"}}, {$elemMatch:{tagKey:"size",tagValue:"XL"}} ] }})
5. 固定(封顶)集合
https://www.mongodb.com/docs/manual/core/capped-collections/
固定集合(capped collection)是一种限定大小的集合,其中capped是覆盖、限额的意思。跟普通的集合相比,数据在写入这种集合时遵循FIFO原则。可以将这种集合想象为一个环状的队列,新文档在写入时会被插入队列的末尾,如果队列已满,那么之前的文档就会被新写入的文档所覆盖。通过固定集合的大小,我们可以保证数据库只会存储“限额”的数据,超过该限额的旧数据都会被丢弃。
使用示例
创建固定集合
db.createCollection("logs",{capped:true,size:4096,max:10})
max:指集合的文档数量最大值,这里是10条
size:指集合的空间占用最大值,这里是4096字节(4KB)
这两个参数会同时对集合的上限产生影响。也就是说,只要任一条件达到阈值都会认为集合已经写满。其中size是必选的,而max则是可选的。
可以使用collection.stats命令查看文档的占用空间
将普通集合转换为固定集合
db.runCommand({"convertToCapped": "mycoll", size: 100000})
测试
尝试在这个集合中插入15条数据,再查询会发现,由于文档数量上限被设定为10条,前面插入的5条数据已经被覆盖了
for(var i=0;i<15;i++){ db.logs.insert({t:"row-"+i})}
适用场景
固定集合很适合用来存储一些“临时态”的数据。“临时态”意味着数据在一定程度上可以被丢弃。同时,用户还应该更关注最新的数据,随着时间的推移,数据的重要性逐渐降低,直至被淘汰处理。
一些适用的场景如下:
存储股票价格变动信息
在股票实时系统中,大家往往最关心股票价格的变动。而应用系统中也需要根据这些实时的变化数据来分析当前的行情。倘若将股票的价格变化看作是一个事件,而股票交易所则是价格变动事件的“发布者”,股票APP、应用系统则是事件的“消费者”。这样,我们就可以将股票价格的发布、通知抽象为一种数据的消费行为,此时往往需要一个消息队列来实现该需求。
结合业务场景:利用固定集合实现存储股票价格变动信息的消息队列
1. 创建stock_queue消息队列,其可以容纳10MB的数据
db.createCollection("stock_queue",{capped:true,size:10485760})
2. 定义消息格式
{ timestamped:new Date(), stock: "MongoDB Inc", price: 20.33}
timestamp指股票动态消息的产生时间。
stock指股票的名称。
price指股票的价格,是一个Double类型的字段。
为了能支持按时间条件进行快速的检索,比如查询某个时间点之后的数据,可以为timestamp添加索引
db.stock_queue.createIndex({timestamped:1})
3. 构建生产者,发布股票动态
模拟股票的实时变动
function pushEvent(){ while(true){ db.stock_queue.insert({ timestamped:new Date(), stock: "MongoDB Inc", price: 100*Math.random(1000) }); print("publish stock changed"); sleep(1000); }}
执行pushEvent函数,此时客户端会每隔1秒向stock_queue中写入一条股票信息
4. 构建消费者,监听股票动态
对于消费方来说,更关心的是最新数据,同时还应该保持持续进行“拉取”,以便知晓实时发生的变化。根据这样的逻辑,可以实现一个listen函数
function listen(){ var cursor = db.stock_queue.find({timestamped:{$gte:new Date()}}).tailable(); while(true){ if(cursor.hasNext()){ print(JSON.stringify(cursor.next(),null,2)); } sleep(1000); }}
find操作的查询条件被指定为仅查询比当前时间更新的数据,而由于采用了读取游标的方式,因此游标在获取不到数据时并不会被关闭,这种行为非常类似于Linux中的tail-f命令。在一个循环中会定时检查是否有新的数据产生,一旦发现新的数据(cursor.hasNext()=true),则直接将数据打印到控制台。
执行这个监听函数,就可以看到实时发布的股票信息