mongodb 副本集之入门篇
作者: 凹凸曼-军军
前言: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 |
搭建步骤如下:
先创建三个目录来分别存放这三个节点的数据
mkdir -p /data/mongodb/rs0-0 /data/mongodb/rs0-1 /data/mongodb/rs0-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
- 使用 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
- 可以直接停掉主节点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
- 然后执行 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
}
]
}
- 再次启动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" ,那么主节点写入完成后,数据同步到第一个副本节点,且第一个副本节点回复数据写入成功后,才给客户端返回成功。
关于写关注在实际中如何操作,有下面两种方法:
- 在写请求中指定 writeConcern 相关参数,如下:
db.products.insert(
{ item: "envelopes", qty : 100, type: "Clasp" },
{ writeConcern: { w: "majority" , wtimeout: 5000 } }
)
- 修改副本集 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个条件,条件是在符合模式的基础上,再根据条件删选具体的节点
- Tag Sets(标签)
顾名思义,这个可以给节点加上标签,然后查找数据时,可以根据标签选择对应的节点,然后在该节点查找数据。可以通过mongo shell 使用 rs.conf() 查看当前每个节点下面的 tags, 修改或者添加tags 过程同上面修改 getLastErrorDefaults 配置 ,如: cfg.members[n].tags = { "region": "South", "datacenter": "A" }
- maxStalenessSeconds (可容忍的最大同步延迟)
顾名思义+1,这个值是指副本节点同步主节点写入的时间 跟 主节点实际最近写入时间的对比值,如果主节点挂掉了,那就跟副本集中最新写入的时间做对比。
这个值建议设置,避免因为部分副本节点网络原因导致比较长时间未同步主节点数据,然后读到比较老的数据。特别注意的是该值需要设置 90s 以上,因为客户端是定时去校验副本节点的同步延迟时间,数据不会特别准确,设置比 90s 小,会抛出异常。
- Hedged Read (对冲读取)
该选项是在分片集群 MongoDB 4.4 版本后才支持,指 mongos 实例路由读取请求时会同时发给两个符合条件的副本集节点,然后那个先返回结果就返回这个结果给客户端。
那问题来了,如此好用的模式以及条件在查询请求中如何使用呢?
- 在代码中连接数据库,使用 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
- 在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 条件。
可以在搭建好的集群中简单测试下该功能
登录主节点:
mongo localhost:27018
插入一条数据:
db.nums.insert({name: “num0”})
在当前节点查询:
db.nums.find()
可以看到本条数据:
{ "_id" : ObjectId("5f958687233b11771912ced5"), "name" : "num0" }
登录副本节点:
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 副本集之入门篇的更多相关文章
- MongoDB副本集学习(三):性能和优化相关
Read Preferences/读写分离 有时候为了考虑应用程序的性能或响应性,为了提高读取操作的吞吐率,一个常见的措施就是进行读写分离,MongoDB副本集对读写分离的支持是通过Read Pref ...
- MongoDB 副本集的原理、搭建、应用
概念: 在了解了这篇文章之后,可以进行该篇文章的说明和测试.MongoDB 副本集(Replica Set)是有自动故障恢复功能的主从集群,有一个Primary节点和一个或多个Secondary节点组 ...
- nodejs+mongoose操作mongodb副本集实例
继上一篇设置mongodb副本集之后,开始使用nodejs访问mongodb副本集: 1:创建项目 express 项目名称 2:npm install mongoose 安装mongo ...
- Docker下搭建mongodb副本集
背景 有需求需要对mongodb做一个容灾备份.根据官网,发现mongodb最新版本(4.0)已经抛弃了主从模式而采用副本集进行容灾.副本集的优势在于:"有自动故障转移和恢复特性,其任意节点 ...
- mongodb副本集的内部机制(借鉴lanceyan.com)
针对mongodb的内部机制提出以下几个引导性的问题: 副本集故障转移,主节点是如何选举的?能否手动干涉下架某一台主节点. 官方说副本集数量最好是奇数,为什么? mongodb副本集是如何同步的?如果 ...
- Fluentd直接传输日志给MongoDB副本集 (replset)
官方文档地址:https://docs.fluentd.org/output/mongo_replset td-agent版本默认没有包含out_mongo插件,需要安装这个插件才能使用 使用的是td ...
- MongoDB副本集的实现与维护实战
1.建立MongoDB副本集 现利用一台机器完成MongoDB副本集的建立 机器1:127.0.0.1:27017 机器2:127.0.0.1:27018 机器3:127.0.0.1:27019 在D ...
- MongoDB副本集学习(一):概述和环境搭建
MongoDB副本集概述 以下图片摘自MongoDB官方文档:http://docs.mongodb.org/manual/core/replication-introduction/ Primary ...
- MongoDB副本集学习(二):基本测试与应用
简单副本集测试 这一节主要对上一节搭建的副本集做一些简单的测试. 我们首先进入primary节点(37017),并向test.test集合里插入10W条数据: . rs0:PRIMARY> ;i ...
随机推荐
- 简简单单入个Redis的门
Redis介绍 Redis是一种key-value的存储系统,它是一种nosql(Not Only [SQL])非关系型的数据库,它支持string(字符串).list(链表).set(集合).has ...
- LC滤波电路分析,LC滤波电路原理及其时间常数的计算
LC滤波器具有结构简单.设备投资少.运行可靠性较高.运行费用较低等优点,应用很广泛. LC滤波器又分为单调谐滤波器.高通滤波器.双调谐滤波器及三调谐滤波器等几种. LC滤波主要是电感的电阻小,直流损耗 ...
- C. Vladik and Memorable Trip 解析(思維、DP)
Codeforce 811 C. Vladik and Memorable Trip 解析(思維.DP) 今天我們來看看CF811C 題目連結 題目 給你一個數列,一個區段的數列的值是區段內所有相異數 ...
- 全球最火的程序员学习路线!没有之一!3天就在Github收获了接近1w点赞
大家好,我是G哥,目前人在荆州办事,但是干货还是要安排上! 国外有一个爆火的开发人员学习路线,目前已经在 Github收获了 131 k+ star,Star 数量在 Github 所有仓库中排名第 ...
- MongoDB简介---MongoDB基础用法(一)
Mongo MongoDB是一个基于分布式文件存储的数据库.MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的. MongoDB 将数据存储为一 ...
- kubernetes 基础知识
1. kubernetes 包含几个组件 Kubernetes是什么:针对容器编排的一种分布式架构,是自动化容器操作的开源平台. 服务发现.内建负载均衡.强大的故障发现和自我修复机制.服务滚动升级和在 ...
- C#3新增语法特性
C#3,.Net Framework 3.5 ,Visual Studio 2008, CLR 3.0 C#3.0新引进的语法基于.Net Framework 3.5.主要引进的语法:Linq,隐式类 ...
- leetcode105: jump-game-ii
题目描述 给出一个非负整数数组,你最初在数组第一个元素的位置 数组中的元素代表你在这个位置可以跳跃的最大长度 你的目标是用最少的跳跃次数来到达数组的最后一个元素的位置 例如 给出数组 A =[2,3, ...
- SWT JFace 小制作 文本阅读器
1 package swt_jface.demo11; 2 import java.io.File; 3 import java.io.FileInputStream; 4 import java.i ...
- C++ 数据结构 1:线性表
1 数据结构 1.1 数据结构中基本概念 数据:程序的操作对象,用于描述客观事物. 数据的特点: 可以输入到计算机 可以被计算机程序处理 数据是一个抽象的概念,将其进行分类后得到程序设计语言中的类型. ...