作者: 凹凸曼-军军

前言:mongodb 因为高性能、高可用性、支持分片等特性,作为非关系型数据库被大家广泛使用。其高可用性主要是体现在 mongodb 的副本集上面(可以简单理解为一主多从的集群),本篇文章主要从副本集介绍、本地搭建副本集、副本集读写数据这三个方面来带大家认识下 mongodb 副本集。

一、 mongodb 副本集介绍

mongodb 副本集(Replica Set)包括主节点(primary)跟副本节点(Secondaries)。

主节点只能有一个,所有的写操作请求都在主节点上面处理。副本节点可以有多个,通过同步主节点的操作日志(oplog)来备份主节点数据。

在主节点挂掉后,有选举权限的副本节点会自动发起选举,并从中选举出新的主节点。

副本节点可以通过配置指定其具体的属性,比如选举、隐藏、延迟同步等,最多可以有50个副本节点,但只能有7个副本节点能参与选举。虽然副本节点不能处理写操作,但可以处理读请求,这个下文会专门讲到。

搭建一个副本集集群最少需要三个节点:一个主节点,两个备份节点,如果三个节点分布合理,基本可以保证线上数据99.9%安全。三个节点的架构如下图所示:

如果只有一个主节点,一个副本节点,且没有资源拿来当第二个副本节点,那就可以起一个仲裁者节点(arbiter),不存数据,只用来选举用,如下图所示:

当主节点挂掉后,那么两个副本节点会进行选举,从中选举出一个新的主节点,流程如下:

对于副本集成员属性,特别需要说明下这几个:priority、hidden、slaveDelay、tags、votes。

  • priority

对于副本节点,可以通过该属性来增大或者减小该节点被选举成为主节点的可能性,取值范围为0-1000(如果是arbiters,则取值只有0或者1),数据越大,成为主节点的可能性越大,如果被配置为0,那么他就不能被选举成为主节点,而且也不能主动发起选举。

这种特性一般会被用在有多个数据中心的情况下,比如一个主数据中心,一个备份数据中心,主数据中心速度会更快,如果主节点挂掉,我们肯定希望新主节点也在主数据中心产生,那么我们就可以设置在备份数据中心的副本节点优先级为0,如下图所示:

  • hidden

    隐藏节点会从主节点同步数据,但对客户端不可见,在mongo shell 执行 db.isMaster() 方法也不会展示该节点,隐藏节点必须Priority为0,即不可以被选举成为主节点。但是如果有配置选举权限的话,可以参与选举。

    因为隐藏节点对客户端不可见,所以跟客户端不会互相影响,可以用来备份数据或者跑一些后端定时任务之类的操作,具体如下图,4个备份节点都从主节点同步数据,其中1个为隐藏节点:

  • slaveDelay

    延迟同步即延迟从主节点同步数据,比如延迟时间配置的1小时,现在时间是 09:52,那么延迟节点中只同步到主节点 08:52 之前的数据。另外需要注意延迟节点必须是隐藏节点,且Priority为0。

    那这个延迟节点有什么用呢?有过数据库误操作惨痛经历的开发者肯定知道答案,那就是为了防止数据库误操作,比如更新服务前,一般会先执行数据库更新脚本,如果脚本有问题,且操作前未做备份,那数据可能就找不回了。但如果说配置了延迟节点,那误操作完,还有该节点可以兜底,只能说该功能真是贴心。具体延迟节点如下图所展示:

  • tags

    支持对副本集成员打标签,在查询数据时会用到,比如找到对应标签的副本节点,然后从该节点读取数据,这点也非常有用,可以根据标签对节点分类,查询数据时不同服务的客户端指定其对应的标签的节点,对某个标签的节点数量进行增加或减少,也不怕会影响到使用其他标签的服务。Tags 的具体使用,文章下面章节也会讲到。

  • votes

    表示节点是否有权限参与选举,最大可以配置7个副本节点参与选举。

二、副本集的搭建以及测试

安装mongodb 教程:https://docs.mongodb.com/manual/installation/

我们来搭建一套 P-S-S 结构的副本集(1个 Primary 节点,2个 Secondary 节点),大致过程为:先启动三个不同端口的 mongod 进程,然后在 mongo shell 中执行命令初始化副本集。

启动单个mongod 实例的命令为:

mongod --replSet rs0 --port 27017 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /data/mongodb/rs0-0 --oplogSize 128

参数说明:

参数 说明 示例
replSet 副本集名称 rs0
port mongod 实例端口 27017
bind_ip 访问该实例的地址列表,只是本机访问可以设置为localhost 或者 127.0.0.1,生产环境建议使用内部域名 Localhost
dbpath 数据存放位置 /data/mongodb/rs0-0
oplogSize 操作日志大小 128

搭建步骤如下:

  1. 先创建三个目录来分别存放这三个节点的数据

    mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-2

  2. 分别启动三个mongod 进程,端口分别为:27018,27019,27020

第一个:

mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

第二个:

mongod --replSet rs0 --port 27019 --bind_ip localhost --dbpath /data/mongodb/rs0-1 --oplogSize 128

第三个:

mongod --replSet rs0 --port 27020 --bind_ip localhost --dbpath /data/mongodb/rs0-2 --oplogSize 128

  1. 使用 mongo 进入第一个 mongod 示例,使用 rs.initiate() 进行初始化

登录到27018: mongo localhost:27018

执行:

rsconf = {
_id: "rs0",
members: [
{
_id: 0,
host: "localhost:27018"
},
{
_id: 1,
host: "localhost:27019"
},
{
_id: 2,
host: "localhost:27020"
}
]
} rs.initiate( rsconf )

以上就已经完成了一个副本集的搭建,在 mongo shell 中执行 rs.conf() 可以看到每个节点中 host、arbiterOnly、hidden、priority、 votes、slaveDelay等属性,是不是超级简单。。

执行 rs.conf() ,结果展示如下:

rs.conf()
{
"_id" : "rs0",
"version" : 1,
"protocolVersion" : NumberLong(1),
"writeConcernMajorityJournalDefault" : true,
"members" : [
{
"_id" : 0,
"host" : "localhost:27018",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : { },
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 1,
"host" : "localhost:27019",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : { },
"slaveDelay" : NumberLong(0),
"votes" : 1
},
{
"_id" : 2,
"host" : "localhost:27020",
"arbiterOnly" : false,
"buildIndexes" : true,
"hidden" : false,
"priority" : 1,
"tags" : { },
"slaveDelay" : NumberLong(0),
"votes" : 1
}
],
"settings" : {
"chainingAllowed" : true,
"heartbeatIntervalMillis" : 2000,
"heartbeatTimeoutSecs" : 10,
"electionTimeoutMillis" : 10000,
"catchUpTimeoutMillis" : -1,
"catchUpTakeoverDelayMillis" : 30000,
"getLastErrorModes" : { },
"getLastErrorDefaults" : {
"w" : 1,
"wtimeout" : 0
},
"replicaSetId" : ObjectId("5f957f12974186fc616688fb")
}
}

特别注意下:在 mongo shell 中,有 rs 跟 db。

  • rs 是指副本集,有rs.initiate(),rs.conf(), rs.reconfig(), rs.add() 等操作副本集的方法
  • db 是指数据库,其下是对数据库的一些操作,比如下面会用到 db.isMaster(), db.collection.find(), db.collection.insert() 等。

我们再来测试下 Automatic Failover

  1. 可以直接停掉主节点localhost:27018 来测试下主节点挂掉后,副本节点重新选举出新的主节点,即自动故障转移(Automatic Failover)

杀掉主节点 27018后,可以看到 27019 的输出日志里面选举部分,27019 发起选举,并成功参选成为主节点:

2020-10-26T21:43:58.156+0800 I  REPL     [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100694 -- target:localhost:27018 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.156+0800 I REPL [replexec-304] Scheduling remote command request for vote request: RemoteCommand 100695 -- target:localhost:27020 db:admin cmd:{ replSetRequestVotes: 1, setName: "rs0", dryRun: false, term: 17, candidateIndex: 1, configVersion: 1, lastCommittedOp: { ts: Timestamp(1603719830, 1), t: 16 } }
2020-10-26T21:43:58.159+0800 I ELECTION [replexec-301] VoteRequester(term 17) received an invalid response from localhost:27018: ShutdownInProgress: In the process of shutting down; response message: { operationTime: Timestamp(1603719830, 1), ok: 0.0, errmsg: "In the process of shutting down", code: 91, codeName: "ShutdownInProgress", $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } } }
2020-10-26T21:43:58.164+0800 I ELECTION [replexec-305] VoteRequester(term 17) received a yes vote from localhost:27020; response message: { term: 17, voteGranted: true, reason: "", ok: 1.0, $clusterTime: { clusterTime: Timestamp(1603719830, 1), signature: { hash: BinData(0, 0000000000000000000000000000000000000000), keyId: 0 } }, operationTime: Timestamp(1603719830, 1) }
2020-10-26T21:43:58.164+0800 I ELECTION [replexec-304] election succeeded, assuming primary role in term 17
  1. 然后执行 rs.status() 查看当前副本集情况,可以看到27019变为主节点,27018 显示已挂掉 health = 0
rs.status()
{
"set" : "rs0",
"date" : ISODate("2020-10-26T13:44:22.071Z"),
"myState" : 1,
"heartbeatIntervalMillis" : NumberLong(2000),
"majorityVoteCount" : 2,
"writeMajorityCount" : 2,
"members" : [
{
"_id" : 0,
"name" : "localhost:27018",
"ip" : "127.0.0.1",
"health" : 0,
"state" : 8,
"stateStr" : "(not reachable/healthy)",
"uptime" : 0,
"optime" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDurable" : {
"ts" : Timestamp(0, 0),
"t" : NumberLong(-1)
},
"optimeDate" : ISODate("1970-01-01T00:00:00Z"),
"optimeDurableDate" : ISODate("1970-01-01T00:00:00Z"),
"lastHeartbeat" : ISODate("2020-10-26T13:44:20.202Z"),
"lastHeartbeatRecv" : ISODate("2020-10-26T13:43:57.861Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "Error connecting to localhost:27018 (127.0.0.1:27018) :: caused by :: Connection refused",
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"configVersion" : -1
},
{
"_id" : 1,
"name" : "localhost:27019",
"ip" : "127.0.0.1",
"health" : 1,
"state" : 1,
"stateStr" : "PRIMARY",
"uptime" : 85318,
"optime" : {
"ts" : Timestamp(1603719858, 1),
"t" : NumberLong(17)
},
"optimeDate" : ISODate("2020-10-26T13:44:18Z"),
"syncingTo" : "",
"syncSourceHost" : "",
"syncSourceId" : -1,
"infoMessage" : "",
"electionTime" : Timestamp(1603719838, 1),
"electionDate" : ISODate("2020-10-26T13:43:58Z"),
"configVersion" : 1,
"self" : true,
"lastHeartbeatMessage" : ""
},
{
"_id" : 2,
"name" : "localhost:27020",
"ip" : "127.0.0.1",
"health" : 1,
"state" : 2,
"stateStr" : "SECONDARY",
"uptime" : 52468,
"optime" : {
"ts" : Timestamp(1603719858, 1),
"t" : NumberLong(17)
},
"optimeDurable" : {
"ts" : Timestamp(1603719858, 1),
"t" : NumberLong(17)
},
"optimeDate" : ISODate("2020-10-26T13:44:18Z"),
"optimeDurableDate" : ISODate("2020-10-26T13:44:18Z"),
"lastHeartbeat" : ISODate("2020-10-26T13:44:20.200Z"),
"lastHeartbeatRecv" : ISODate("2020-10-26T13:44:21.517Z"),
"pingMs" : NumberLong(0),
"lastHeartbeatMessage" : "",
"syncingTo" : "localhost:27019",
"syncSourceHost" : "localhost:27019",
"syncSourceId" : 1,
"infoMessage" : "",
"configVersion" : 1
}
]
}
  1. 再次启动27018:

    mongod --replSet rs0 --port 27018 --bind_ip localhost --dbpath /data/mongodb/rs0-0 --oplogSize 128

可以在节点 27019 日志中看到已检测到 27018,并且已变为副本节点,通过rs.status 查看结果也是如此。

2020-10-26T21:52:06.871+0800 I  REPL     [replexec-305] Member localhost:27018 is now in state SECONDARY

三、副本集写跟读的一些特性

写关注(Write concern)

副本集写关注是指写入一条数据,主节点处理完成后,需要其他承载数据的副本节点也确认写成功后,才能给客户端返回写入数据成功。

这个功能主要是解决主节点挂掉后,数据还未来得及同步到副本节点,而导致数据丢失的问题。

可以配置节点个数,默认配置 {“w”:1},这样表示主节点写入数据成功即可给客户端返回成功,“w” 配置为2,则表示除了主节点,还需要收到其中一个副本节点返回写入成功,“w” 还可以配置为 "majority",表示需要集群中大多数承载数据且有选举权限的节点返回写入成功。

如下图所示,P-S-S 结构(一个 primary 节点,两个 secondary 节点),写请求里面带了w : “majority" ,那么主节点写入完成后,数据同步到第一个副本节点,且第一个副本节点回复数据写入成功后,才给客户端返回成功。

关于写关注在实际中如何操作,有下面两种方法:

  1. 在写请求中指定 writeConcern 相关参数,如下:
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: "majority" , wtimeout: 5000 } }
)
  1. 修改副本集 getLastErrorDefaults 配置,如下:
cfg = rs.conf()
cfg.settings.getLastErrorDefaults = { w: "majority", wtimeout: 5000 }
rs.reconfig(cfg)

读偏好 (Read preference)

读跟写不一样,为了保持一致性,写只能通过主节点,但读可以选择主节点,也可以选择副本节点,区别是主节点数据最新,副本节点因为同步问题可能会有延迟,但从副本节点读取数据可以分散对主节点的压力。

因为承载数据的节点会有多个,那客户端如何选择从那个节点读呢?主要有3个条件(Tag Sets、 maxStalenessSeconds、Hedged Read),5种模式(primary、primaryPreferred、secondary、secondaryPreferred、nearest)

首先说一下 5种模式,其特点如下表所示:

模式 特点
primary 所有读请求都从主节点读取
primaryPreferred 主节点正常,则所有读请求都从主节点读取,如果主节点挂掉,则从符合条件的副本节点读取
secondary 所有读请求都从副本节点读取
secondaryPreferred 所有读请求都从副本节点读取,但如果副本节点都挂掉了,那就从主节点读取
nearest 主要看网络延迟,选取延迟最小的节点,主节点跟副本节点均可

再说下3个条件,条件是在符合模式的基础上,再根据条件删选具体的节点

  1. Tag Sets(标签)

顾名思义,这个可以给节点加上标签,然后查找数据时,可以根据标签选择对应的节点,然后在该节点查找数据。可以通过mongo shell 使用 rs.conf() 查看当前每个节点下面的 tags, 修改或者添加tags 过程同上面修改 getLastErrorDefaults 配置 ,如: cfg.members[n].tags = { "region": "South", "datacenter": "A" }

  1. maxStalenessSeconds (可容忍的最大同步延迟)

顾名思义+1,这个值是指副本节点同步主节点写入的时间 跟 主节点实际最近写入时间的对比值,如果主节点挂掉了,那就跟副本集中最新写入的时间做对比。

这个值建议设置,避免因为部分副本节点网络原因导致比较长时间未同步主节点数据,然后读到比较老的数据。特别注意的是该值需要设置 90s 以上,因为客户端是定时去校验副本节点的同步延迟时间,数据不会特别准确,设置比 90s 小,会抛出异常。

  1. Hedged Read (对冲读取)

该选项是在分片集群 MongoDB 4.4 版本后才支持,指 mongos 实例路由读取请求时会同时发给两个符合条件的副本集节点,然后那个先返回结果就返回这个结果给客户端。

那问题来了,如此好用的模式以及条件在查询请求中如何使用呢?

  1. 在代码中连接数据库,使用 connection string uri 时,可以加上下面的这三个参数
参数 说明
readPreference 模式,枚举值有:primary(默认值)、 primaryPreferred、secondary、secondaryPreferred、nearest
maxStalenessSeconds 最大同步延时秒数,取值0 - 90 会报错, -1 表示没有最大值
readPreferenceTags 标签,如果标签是 { "dc": "ny", "rack": "r1" }, 则在uri 为 readPreferenceTags=dc:ny,rack:r1

例如下面:

mongodb://db0.example.com,db1.example.com,db2.example.com/?replicaSet=myRepl&readPreference=secondary&maxStalenessSeconds=120&readPreferenceTags=dc:ny,rack:r1

  1. 在mogo shell 中,可以使用 cursor.readPref() 或者 Mongo.setReadPref()

cursor.readPref() 参数分别为: mode、tag set、hedge options, 具体请求例如下面这样

db.collection.find({ }).readPref(
"secondary", // mode
[ { "datacenter": "B" }, { } ], // tag set
{ enabled: true } // hedge options
)

Mongo.setReadPref() 类似,只是预先设置请求条件,这样就不用每个请求后面带上 readPref 条件。

可以在搭建好的集群中简单测试下该功能

  1. 登录主节点: mongo localhost:27018

  2. 插入一条数据: db.nums.insert({name: “num0”})

    在当前节点查询: db.nums.find()

    可以看到本条数据: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

  3. 登录副本节点: mongo localhost:27019

查询:db.nums.find()

因为查询模式默认为 primary,所以在副本节点查询会报错,如下:

Error: error: {
"operationTime" : Timestamp(1603788383, 1),
"ok" : 0,
"errmsg" : "not master and slaveOk=false",
"code" : 13435,
"codeName" : "NotMasterNoSlaveOk",
"$clusterTime" : {
"clusterTime" : Timestamp(1603788383, 1),
"signature" : {
"hash" : BinData(0,"AAAAAAAAAAAAAAAAAAAAAAAAAAA="),
"keyId" : NumberLong(0)
}
}
}

查询时指定模式为 “secondary”: db.nums.find().readPref(“secondary")

就可以查询到插入的数据: { "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }

结语

以上内容都是阅读 MongoDB 官方文档后,然后挑简单且重要的一些点做的总结,如果大家对 MongoDB 感兴趣,建议直接啃一啃官方文档

欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

mongodb 副本集之入门篇的更多相关文章

  1. MongoDB副本集学习(三):性能和优化相关

    Read Preferences/读写分离 有时候为了考虑应用程序的性能或响应性,为了提高读取操作的吞吐率,一个常见的措施就是进行读写分离,MongoDB副本集对读写分离的支持是通过Read Pref ...

  2. MongoDB 副本集的原理、搭建、应用

    概念: 在了解了这篇文章之后,可以进行该篇文章的说明和测试.MongoDB 副本集(Replica Set)是有自动故障恢复功能的主从集群,有一个Primary节点和一个或多个Secondary节点组 ...

  3. nodejs+mongoose操作mongodb副本集实例

    继上一篇设置mongodb副本集之后,开始使用nodejs访问mongodb副本集: 1:创建项目     express 项目名称 2:npm install mongoose    安装mongo ...

  4. Docker下搭建mongodb副本集

    背景 有需求需要对mongodb做一个容灾备份.根据官网,发现mongodb最新版本(4.0)已经抛弃了主从模式而采用副本集进行容灾.副本集的优势在于:"有自动故障转移和恢复特性,其任意节点 ...

  5. mongodb副本集的内部机制(借鉴lanceyan.com)

    针对mongodb的内部机制提出以下几个引导性的问题: 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点. 官方说副本集数量最好是奇数,为什么? mongodb副本集是如何同步的?如果 ...

  6. Fluentd直接传输日志给MongoDB副本集 (replset)

    官方文档地址:https://docs.fluentd.org/output/mongo_replset td-agent版本默认没有包含out_mongo插件,需要安装这个插件才能使用 使用的是td ...

  7. MongoDB副本集的实现与维护实战

    1.建立MongoDB副本集 现利用一台机器完成MongoDB副本集的建立 机器1:127.0.0.1:27017 机器2:127.0.0.1:27018 机器3:127.0.0.1:27019 在D ...

  8. MongoDB副本集学习(一):概述和环境搭建

    MongoDB副本集概述 以下图片摘自MongoDB官方文档:http://docs.mongodb.org/manual/core/replication-introduction/ Primary ...

  9. MongoDB副本集学习(二):基本测试与应用

    简单副本集测试 这一节主要对上一节搭建的副本集做一些简单的测试. 我们首先进入primary节点(37017),并向test.test集合里插入10W条数据: . rs0:PRIMARY> ;i ...

随机推荐

  1. 简简单单入个Redis的门

    Redis介绍 Redis是一种key-value的存储系统,它是一种nosql(Not Only [SQL])非关系型的数据库,它支持string(字符串).list(链表).set(集合).has ...

  2. LC滤波电路分析,LC滤波电路原理及其时间常数的计算

    LC滤波器具有结构简单.设备投资少.运行可靠性较高.运行费用较低等优点,应用很广泛. LC滤波器又分为单调谐滤波器.高通滤波器.双调谐滤波器及三调谐滤波器等几种. LC滤波主要是电感的电阻小,直流损耗 ...

  3. C. Vladik and Memorable Trip 解析(思維、DP)

    Codeforce 811 C. Vladik and Memorable Trip 解析(思維.DP) 今天我們來看看CF811C 題目連結 題目 給你一個數列,一個區段的數列的值是區段內所有相異數 ...

  4. 全球最火的程序员学习路线!没有之一!3天就在Github收获了接近1w点赞

    大家好,我是G哥,目前人在荆州办事,但是干货还是要安排上! 国外有一个爆火的开发人员学习路线,目前已经在 Github收获了 131 k+ star,Star 数量在 Github 所有仓库中排名第 ...

  5. MongoDB简介---MongoDB基础用法(一)

    Mongo MongoDB是一个基于分布式文件存储的数据库.MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的. MongoDB 将数据存储为一 ...

  6. kubernetes 基础知识

    1. kubernetes 包含几个组件 Kubernetes是什么:针对容器编排的一种分布式架构,是自动化容器操作的开源平台. 服务发现.内建负载均衡.强大的故障发现和自我修复机制.服务滚动升级和在 ...

  7. C#3新增语法特性

    C#3,.Net Framework 3.5 ,Visual Studio 2008, CLR 3.0 C#3.0新引进的语法基于.Net Framework 3.5.主要引进的语法:Linq,隐式类 ...

  8. leetcode105: jump-game-ii

    题目描述 给出一个非负整数数组,你最初在数组第一个元素的位置 数组中的元素代表你在这个位置可以跳跃的最大长度 你的目标是用最少的跳跃次数来到达数组的最后一个元素的位置 例如 给出数组 A =[2,3, ...

  9. SWT JFace 小制作 文本阅读器

    1 package swt_jface.demo11; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.i ...

  10. C++ 数据结构 1:线性表

    1 数据结构 1.1 数据结构中基本概念 数据:程序的操作对象,用于描述客观事物. 数据的特点: 可以输入到计算机 可以被计算机程序处理 数据是一个抽象的概念,将其进行分类后得到程序设计语言中的类型. ...