|
一、概述 (1)版本历程- 0.x 起步节点
- 1.x 支持复制集和分片
- 2.x 更加丰富的数据库功能
- 3.x 合并了一家专门做数据库引擎的Wired Tiger公司,更加完善的周边生态环境
- 4.x 支持 分布式事务
MongoDB的正式版本都是 偶数版本,x.x.x,主要版本(x.x)大约每年升级一次,小版本主要是修复问题,通常1-2个月发布一次。
MongoDB支持原生高可用:Application通过Driver连接到Primary节点,一个Primary节点连接多个Secondary节点。
MongoDB支持 水平扩展,分片集群:Driver连接多个Mongos,Mongos连接多个Shard,每个Shard都是一个Primary和多个Secondary。
二、复制集 主要用于 实现服务的高可用
(1)特征 MongoDB的复制集主要具备如下特征:
- 快速复制:数据写入时将数据迅速复制到另一个节点。
- 故障转移:在接受写入的节点发生故障的时候自动选择另一个新的节点代替。
- 其他作用:数据分发、读写分离、异地容灾。
(2)MongoDB的数据复制原理- 一个修改 操作会被记录到oplog,有一个 线程监听oplog,如果有变动就会将这个变动应用到其他的数据库上。
- 从节点在主节点上打开一个 tailable游标,不断获取新加入的oplog,并在从库上 回放。
(3)节点间选举- 一个典型的复制集由 3个以上具有投票权的节点构成,一个 Primary接受写入操作和选举时投票,两个Secondary复制Primary节点数据和选举时投票。
- 具有投票权的节点 两两发送心跳数据,主节点 5次没有收到心跳被定义为失联状态。然后MongoDB 基于Raft协议进行选举。
- Replica Set中 最多50个节点,具有投票权的节点 最多只有7个。
- 影响选举的因素:整个集群必须有 大多数节点存活。
- 被选举为主节点的条件:
- 能够与多数节点 建立连接
- 具有 较新的oplog
- 具有较高的 优先级(可以配置)
- 常用的 配置选项:
- 是否具有投票权( v参数):有就会参与投票。
- 优先级( priority参数):优先级越高的节点越优先称为主节点。 优先级为0的节点无法成为主节点。
- 隐藏( hidden参数):复制数据,但是对应用不可见。 隐藏节点具有投票权,但是优先级必须为0。
- 延迟( slaveDelay参数):复制N秒之前的数据,报纸与主节点的 时间差。
- 复制集的部署事项:主从数据库配置一样,硬件具有独立性,软件版本一致。
三、分片集群
(1)mongos路由节点- 提供集群的单一入口
- 转发应用端请求
- 选择合适的数据节点进行读写
- 合并多个数据额节点的返回结果
- 无状态
- 至少有两个mongos节点
(2)config配置目录节点 (3)数据节点- 以复制集为单位
- 横向扩展
- 最大1024个分片
- 分片之间数据不能重复
- 所有分片在一起才可以完成工作
(4)特点- 对应用全透明,无须特殊处理
- 数据自动均衡
- 动态扩容,无须下线
- 提供三种分片方式
(5)分片集群的3中方式- 基于 范围:片键范围查询性能好,优化读,数据分布可能不均匀,容易产生热点。
- 基于 Hash:数据分布均匀,写优化,范围查询效率低,适用于日志,物联网高并发场景。
- 基于 Zone:按照地域、时效等属性分为多个Zone。一个集合(collection)中的所有的对象都被存放到一个块(chunk)中,默认块的大小是 64MB。当数据容量超过64 MB,才有可能实施一个迁移
(6)合理的架构- 一个分片 不超过3TB,尽量保证在2TB。常用索引必须容纳进内存。
- 需要多少个分片?
分片数量=max(所需 存储容量/单节点挂载容量, 工作集大小/单服务器 内存容量 0.6, 并发总量/单节点并发量0.7)
- 如何选择片键?
- 基数,基数越大越好,比如百家姓要比年龄基数大。
- 写分布,数据分布不均匀,比如学生的年龄。
- 定向性,mongos可以直接将数据定位。
四、灾备与恢复 (1)备份 mongodump -h HostName:Port -d DatabaseName -c CollectionName |
使用
参数实现增量备份。复制从mongodump从开始执行到完成所有的oplog。会输出到
文件。
(2)恢复 mongostore -h HostName:port -d DatabaseName -c CollectionName Filename.bson |
使用
参数实现增量恢复。通过
参数和
参数实现特定时间点的恢复。
在分片集群的备份中,多个分片可能在发生数据迁移和均衡,导致备份的数据发生错乱,可以通过停止均衡器解决。
(3)备份方案 五、事务支持 (1)写事务 writeConcern参数:决定一个写操作落到多少个节点上才算成功。
- w参数:
- 1:默认,要求写操作已经传播到 独立的mongo实例或者 副本集的Primary成员。
- 0:不要求确认写操作。
- majority:要求写操作已经传播到 大多数具有 存储数据且具有投票权的成员。
- j参数:
- true:写操作落到 journal算成功。
- false:写操作落到 内存算成功。
(2)读事务- 从哪里读? 位置
- 由 readPreference参数控制,取值如下:
- Primary(默认):主节点,一般用户 下订单。
- PrimaryPrefered:主节点优先,一般用户 查订单。
- Secondary:从节点,一般用于 报表。
- SecondaryPrefered:优先从节点。
- nearest:就近原则,由 PingTime决定,一般用于 上传图片分发到全球。
- 结合使用 Tag定向某些节点:通过指定{"purpose": ""}实现。
- 什么样的数据可以读? 隔离性
- 由 readConcern参数控制,取值如下:
- avaliable:读取所有可用的数据。
- local(默认):读取所有可用并且属于当前分片的数据。
- majority:读取大多数节点上提交完成的数据,防止脏读。
- 实现机制:节点 使用MVCC机制维护多个版本,每个大多数节点确认过的版本数据作为一个快照,MongoDB通过维护多个快照实现链接不同的版本,快照维持在不再使用。
- linearizable:线性化读取文档,保证之前所有写入的,能够保证出现 网络分区的时候读取的安全,因为在读取的时候 会检查所有节点。
- snapshot:读取最近快照中的数据。
- 如何安全的读写?
- readConcern设置为majority
- writeConcern设置为majority
(3)多文档事务MongoDB的ACID- A= 4.0版本的复制集多表多行,4.2版本的分片集群多表多行,1.0版本的单表单文档。
- C=writeConcern和readConcern。
- I=readConcern。
- D=Journal和Replication。
(4)ChangeStream 用于 追踪变更,类似于触发器,基于 oplog实现,返回的_id可用于 断点恢复,有个cursor进行追踪, 推送majority条件的变更。
- 应用程序可以实时的了解数据的变化。
- 复制协议版本必须是1且使用WT存储引擎。
- 只有 副本集和Shard可用。
- 使用MongoDB Driver3.6,并且必须开启 3.6版本特性参数featureCompatibilityVersion。 writeConcern必须配置。
- ChangeStream与触发器的异同点:
- ChangeStream是 异步的,基于 事件回调机制。
- ChangeStream 每个客户端都会生效一次。
- ChangeStream支持 断点,触发器只能事务回滚。
- 应用场景:
- ChangeStream的中断事件不能超过oplog的回收时间。
六、面试题 MongoDB的优势?
- 面向Collection和Document,以JSON格式保存数据,支持二进制数据和大型对象。
- 高性能,支持Document嵌入,减少了数据库上的IO操作,基于具有完整的索引支持,支持快速查询。
- 高可用,复制集,提供自动故障转移。
- 高可扩展,分片集群。
- 支持聚合管道和全文索引。
- 支持插件式存储引擎,WiredTiger存储引擎和in-memory存储引擎。
- MongoDB支持的数据类型:
- 类似于Java中的:String(UTF-8编码才是合法的)、Integer、Double、Boolean、Arrays、Datetime。
- 特有的:ObjectId(用于存储文档ID,ObjectId基于分布式主键实现MongoDB分片也可用)、Min/Max Keys(将一个值与BSON元素最低值和最高值比较)、Code(JavaScript代码)、Regular Expression(正则表达式)、Binary Data(二进制数据)、Null(空值)、Object(内嵌的文档)。
什么是mongod,默认参数有哪些?
- mongod是处理MongoDB系统的主要进程,默认参数有--dbpath=/data/db,--port=27017
MySQL和MongoDB的区别:
- MongoDB是非关系型数据库
- MySQL采用 虚拟内存+持久化的方式
- MySQL使用传统的SQL语句方式
- MongoDB常见的架构有副本集和分片集群,MySQL有MS、MHA、MMM等架构
- MongoDB基于内存,将热数据存储在物理内存,从而实现数据告诉读写,MySQL每个存储引擎都有自己的特点。
更新操作会立刻fsync到磁盘?
- 不会,磁盘写操作默认延迟执行,写操作可能在 2~3s内落到磁盘,可以通过 syncPeriodSecs参数配置。
MongoDB支持的索引类型?
- 单字段索引
- 复合索引
- 多键索引
- 全文索引
- Hash索引
- 通配符索引
- 2d sphere索引
MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗?
由于MongoDB索引使用B-tree树原理,只会在A:{B,C}上使用索引。
如果块移动操作(moveChunk)失败了,我需要手动清除部分转移的文档吗?
不需要,移动操作是一致并且是确定的。一次失败后,移动操作会不断重试。当完成后,数据只会出现在新的分片里。
数据在什么时候才会扩展到多个分片里?
MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块(chunk)中,默认块的大小是 64Mb。当数据容量超过64 Mb,才有可能实施一个迁移,只有当存在不止一个块的时候,才会有多个分片获取数据的选项。
更新一个正在被迁移的块(Chunk)上的文档时会发生什么?
更新操作会立即发生在旧的块(Chunk)上,然后更改才会在所有权转移前复制到新的分片上。
如果一个分片(Shard)停止或很慢的时候,发起一个查询会怎样?
如果一个分片停止了,除非查询设置了 “Partial” 选项,否则查询会返回一个错误。如果一个分片响应很慢,MongoDB 会等待它的响应。
什么是Arbiter?
仲裁节点不维护数据集。 仲裁节点的目的是通过响应其他副本集节点的心跳和选举请求来维护副本集中的仲裁。
复制集节点类型有哪些?
- 优先级0型(Priority 0)节点
- 隐藏型(Hidden)节点
- 延迟型(Delayed)节点
- 投票型(Vote)节点以及不可投票节点
七、应用案例 (1)MongoDB典型的应用场景 MongoDB是OLTP数据库,原则上MySQL和Oracle能做的事情,MongoDB也都可以。MongoDB具有原生的横向扩展能力,灵活的模型支持,适合快速开发迭代,数据模型多变的场景,并且MongoDB使用了JSON数据结构,非常适合微服务领域。
基于功能的选择:
| | MongoDB | 传统关系型数据库 |
---|
亿级以上的数据量支持 | Easy | 分库分表 | 灵活的表结构 | Easy | 数据字典,关联查询 | 高并发读 | Easy | Hard | 高并发写 | Easy | Hard | 跨地区的集群 | Easy | Hard | 数据分片 | Easy | 中间件 | 地址位置查询 | 完整支持 | PostGreSQL还可以,其他的很麻烦 | 聚合计算 | Easy | GroupBY,复杂的SQL | 异构数据 | Easy | 数据字典,关联查询 | 大、宽表 | Easy | 性能局限 |
基于场景的选择:
- 移动端应用、小程序
场景特点:基于RESTful API,快速迭代,数据结构频繁变化,大部分功能基于地理信息,爆发式的增长,高可用
业界案例:Keep(说实在的, 健身还不如专门请个私教单独一对一),摩拜单车,ADP
- 电商的海量商品数据
场景特点:商品信息包罗万象,数据库模式设计困难
业界案例:京东商城,小红书,GAP
- 内容管理:
场景特点:内容数据多样,扩展困难
业界案例:Adobe AEM,SiteCore
- 物联网IoT
场景特点:传感器数据结构往往是半结构化数据,传感器实时采集的数据量巨大,容易增长到百亿级别
业界案例:华为、Bosch、MindSphere
- SaaS应用
场景特点:多租户模式,需求多变,数据增长快
业界案例:ADP、Teambition
- 主机分流
场景特点:高性能查询,实时同步机制
业界案例:金融行业
- 实时在线分析
场景特点:流数据计算,快速计算,秒级响应
业界案例:MongoDB缓存机制、MongoDB聚合框架、微分片架构
- 关系型迁移到MongoDB承载更多的数据和并发
场景特点:数据增长导致性能低,分库分表方案复杂
业界案例:头条、网易、百度、东航、中行
(2)MongoDB对接MySQL、Oracle 从传统的关系型数据库迁移到MongoDB需要综合考虑的几个问题:
- 总体架构
- 模式设计
表结构整合为JSON文档
- SQL语句/存储过程/ORM层
原始SQL
存储过程特性
ORM框架
- 数据迁移
数据迁移的几个方式:
(1)数据库导出导入,导出JSON或者CSV
(2)ETL批量迁移工具,Kettle、Talend
(3)实时同步工具,infomatica、Tapdata(会运行一个Agent),一般是解析日志模式
(4)应用主动迁移
(3)MongoDB与Spark MongoDB作为Spark的存储方案,MongoDB相比HDFS更加细粒度存储,并且支持结构化存储。MongoDB支持索引机制,使得Spark的读取更加快速,HDFS是一次写,多次读,但是MongoDB适合Spark的读写混合场景。MongoDB是在线式存储,毫秒级的SLA。
(4)可视化与ETL MongoDB可以通过BI Connector实现与SQL的结合。BI Connector会自动产生DRDL映射文件,然后我们根据映射文件来编写SQL语句实现数据展示。
BI Connector是企业版的,并且是一个独立的服务。
BI Connector暴露的是MySQL驱动构建的解释器,然后作为一个虚拟的MySQL服务。
(5)两地三中心高级集群设计 | 容灾级别 | 描述 | RPO | RTO |
---|
Level0 | 无灾备源,只有本地的数据备份 | 24小时 | 4小时 | Level1 | 本地备份+异地保存,将关键数据保存并送到异地 | 24小时 | 8小时 | Level2 | 双中心主备,通过网络建立热点备份 | 秒级 | 数分钟到半小时 | Level3 | 双中心双活,互相进行数据备份 | 秒级 | 秒级 | Level4 | 双中心双活+异地热备,当一个城市的两个中心不可用时切换 | 秒级 | 分钟级 |
网络层解决方案
GSLB实现MongoDB负载均衡器的健康检查,通过域名实现应用层的切换。
应用层解决方案
使用负载均衡技术,虚拟IP技术,使用同一个Session,使用同一套数据。
使用HAProxy或者Nginx作为本地的SLB本地负载均衡器。
数据库层解决方案
通过日志同步或者存储镜像实现数据拷贝。
复制集跨中心2+2+1解决方案
2+2+1保证了主中心的高可用,oplog同步实现了毫秒级的拷贝。
(6)全球多写 由于复制集只解决了读取的问题,写入还是要在Primary上进行所以不能够保证几个国家的用户体验。
全球多写本质上是一个特殊的分片集群。将集群中的分片节点分区域部署。要实现全球分片多写,那么要实现以下三点条件:
- 针对要分片的数据集合,模型中 增加一个区域字段。
- 给集群中的每个 分片添加区域标签。
sh.addShardTag("shard0", "Asia"); |
- 为每个区域指定属于这个区域的 分片块范围。
sh.addShardRange("tableName", {"location": "China"}, "Asia"); |
全球多写的事务性问题:
- 当海外用户访问读取数据时,希望是从海外本地读取,因此需要设置
。
- 当海外用户下单,那么需要写到本地大部分节点才算成功,在国内的海外数据等待oplog同步,因此需要设置
。 - 当需要读取所有区域的数据进行汇总时,只需要设置读取本地主从节点为:
就会保证从本地读取就近的数据。 - 加入海外用户在国内下单,那么就会导致需要写入远程海外节点,因为配置了
需要写入大部分节点。 当然,MongoDB也可以在国内和海外向Oracle那样同时部署两套集群,通过第三方工具实现同步,中间也需要处理数据冲突问题。常见的中间件有: Tapdata和MongoShake。 这两个第三方中间件也是基于oplog的。
八、连接与开发注意事项- 连接到复制集:mongodb://node1,node2/dbname?[option]
- 连接到分片集:mongdb://mongos1,mongos2/dbname?[option]
- 支持域名解析:mongodb+srv://mongos或者node地址
- mongos前不可以使用负载均衡器,因为mongos自带LB
- 事务支持:
- 使用 4.2兼容驱动。
- 事务在 60秒内完成,否则会被取消。
- 涉及事务的分片 不能使用仲裁节点。
- 事务会 影响Chunk迁移效率。
- 正在迁移的Chunk可能造成事务失败。
- 多文档事务必须在Primary节点进行。
- readConcern只应该在事务级别设置,不应该在每次读写上进行。
- 其他:
- 每一个查询尽量对应一个索引。
- 尽量使用覆盖索引。
- 使用projection来减少返回到Client的内容。
- 处理分页避免使用count,只是用limit。
- 尽量控制在1000个更新文档事务之内。
- 系统上线时的必要检查:
- 禁用NUMA,否则在某些情况下可能导致突发的大量的SWAP交换。
- 禁用Transparent Huge Page,否则会影响数据库效率。
设置为120秒,容忍网络问题。- 设置最大文件句柄打开数目。
- 关闭文件系统的atime,提高访问效率。
九、索引管理 MongoDB中的索引是特殊结构,索引存储在 易于遍历的数据集合中,而且使用 BTree结构。
(1)创建索引要考虑的问题- 每个索引至少需要8KB的空间
- 添加索引会对写操作性能产生影响,因为每个集合在插入时也必须更新索引
- 索引处于Action状态时,每个索引都会占用磁盘空间和内存
(2)索引的限制- 索引名称超度不可以超过128字段
- 复合索引不能超过32个属性
- 每个集合不能超过64个索引
(3)索引管理- 创建索引
db.collection.createIndex(<key>, <option>); |
| 参数 | 数据类型 | 描述 |
---|
background | Boolean | 创建索引会阻塞数据库操作,可以指定为后台操作。 | unique | Boolean | 是否建立唯一索引 | name | String | 索引的名称 | dropDups | Boolean | 3.0版本废弃,建立索引时是否删除重复记录 | sparse | Boolean | 对文档中不存在的字段数据不建立索引 | expireAfterSeconds | Integer | 秒,设定索引的TTL | v | Index version | 索引的版本号 | weight | Document | 索引权重值,数值在1-99999之间 | default_language | String | 对于文本类型的索引,决定了分词器规则,默认为英语 | language_override | String | 对于文本类型的索引,指定了包含在文档中的字段名 |
- 查看索引
db.collection.getIndexs(); |
- 删除索引
db.collection.dropIndexs();
db.collection.dropIndex(); |
- 查看创建过程和终止
db.currentOp();
db.killOp(); |
- 使用情况
// 获取索引访问信息
$indexStats
// 返回查询计划
explain()
// 控制索引, 强制MongoDB使用特定索引进行查询
hint() |
(4)单值索引 MongoDB可以在任何字段上创建索引,默认情况下会在
字段创建索引,
索引时为了防止客户端具有相同的值创建的索引,该索引无法删除。在 分片集群中使用
索引。
(5)复合索引 将多个键组合到一起,这样可以 加速匹配多个键的查询。
- 无法创建具有Hash索引的复合索引
- 复合字段的索引是 有顺序的
- 复合索引支持 前缀匹配查询
db.collection.createIndex( { <field1>: <type>, <field2>: <type2>, ... } ) |
(6)多键索引 MongoDB使用多键索引为数组的每个元素创建索引,多键索引可以建立在字符串、数字、内嵌文档类型的数组上。如果创建的字段包含数组的值,那么MongoDB将会自动确定是否创建索引。
db.coll.createIndex( { <field>: < 1 or -1 > } ) |
(7)全文索引 MongoDB机制提供了全文索引类型,支持在集合中搜索字符串。
db.collection.createIndex( { key: "text",key:"text" ..... } ) |
MongoDB提供权重以及通配符的创建方式。查询方式多个字符串空格隔开,排除查询使用“-”。 每个全文索引可以通过设置权重来分配不同的搜索程度,默认权重为1,对于文档中的每个索引字段,MongoDB将匹配数乘以权重并将结果相加。 使用此总和,MongoDB然后计算文档的分数
- 每个集合最多只有一个全文索引
- 如果查询使用
表达式就无法使用hint()函数
(8)Hash索引 散列索引使用散列函数来计算索引字段值的散列值。 散列函数会折叠嵌入的文档并计算整个值的散列值,但不支持多键(即数组)索引。
db.collection.createIndex( { _id: "hashed" } ) |
散列索引支持使用散列分片键进行分片。 基于散列的分片使用字段的散列索引作为分片键来分割整个分片群集中的数据。
十、安全架构 通过在命令行方式加入
参数或者在配置文件添加
开启安全选项。
使用命令行客户端操作:
mongo -uUsername -pPassword --authenticationDatabase DbName |
(1)MongoDB支持的安全策略- 用户名密码
- 证书
- LDAP,企业版
- Kerberos,企业版
(2)针对集群节点之间的认证- KeyFile,统一将Key拷贝到不同的节点, 随机的字符串
- X.509,基于证书的模式,通过内部或者外部的 CA服务器颁发,每个节点都有不同的证书
(3)MongoDB支持的用户权限 MongoDB的Role建立在Action和Resource上, Action定义了一种动作, Resource表示某个动作可以操作的资源。MongoDB内置权限角色继承关系图如下:
自定义角色和用户分别可以使用createRole()和createUser()。
(4)传输加密 MongoDB支持TLS/SSL来加密 所有的网络数据传输,不管是内部节点还是客户端到服务器。
(5)落盘加密(企业版)- 生成master key,这是一个用来加密数据库的key。每一个数据库都对应不同的key。
- 当落盘时, 基于不同数据库的key进行数据加密。
- key的管理通过使用 KMIP协议的秘钥管理服务器完成。MongoDB也支持文件的方式进行管理。
(6)字段加密- MongoDB支持 字段级别的加密。
- 当向加密的数据发送请求的时候,MongoDB的驱动直接联系秘钥管理器,获取秘钥,然后根据查询条件直接去数据库查询,将获取的加密数据拉取过来然后使用秘钥解密返回明文数据。数据的加密解密都发生在MongoDB的驱动程序。
(7)审计(企业版)- 记录格式为JSON
- 可以记录到本地文件或者syslog
- 记录的内容有:DDL、DML、用户认证
审计日志记录到syslog:
--auditDestination syslog |
审计日志记录到指定文件:
--auditDestination file --auditFormat JSON --auditPath /path/to/auditLog.json |
对删除进行审计:
--auditDestination file --auditFormat JSON --auditPath /path/to/auditLog.json --auditFilter '{atype: {$in: ["dropCollection"]}}' |
十一、性能优化 (1)mongostat 用于了解MongoDB运行状态的工具。
- insert、query、update、delete:最近一秒钟有多少个操作
- getmore:针对游标操作,最近一秒钟的操作
- command:创建索引等操作,最近一秒钟的执行个数
- dirty:超过20%的时候可能会阻塞新请求,因为这个参数表示还没有刷盘数据占比
- used:超过95%的时候可能会阻塞新请求,由于MongoDB基于内存缓存机制,当缓存超过80%时,就会执行LRU算法
- qrw、arw:表示排队的请求
- conn:表示当前连接数
(2)mongotop 用于了解集合压力的工具
- ns:集合
- total:总时间耗时
- read:读时间耗时
- write:写时间耗时
(3)mongod日志 MongoDB会记录超过100ms的查询,会将执行计划输出。
(4)mtools 常用指令:
- mplotqueries LogFile:将所有的慢查询通过图标展示。
- mloginfo —queries LogFile:总结所有慢查询模式和出现的次数,消耗时间等。
[url=github.com/rueckstiess/mtools]https://github.com/rueckstiess/mtools[/url]
十二、GridFS GridFS是MongoDB的一个子模块,主要用于在MongoDB中存储文件,相当于MongoDB内置的一个分布式文件系统。本质上还是讲文件的数据分块存储在集合中,默认的文件集合分为
和
。fs.files是存储文件的基本信息,比如文件名,大小,上传时间,MD5等。fs.chunks是存储文件真正数据的地方,一个文件会被分割成多个chunk块进行存储,一般为 256KB/个。
GridFS的好处是你不用单独去搭建一个文件系统,直接使用Mongodb自带的即可,备份,分片都依赖MongoDB,维护起来也方便。
----------------------------
原文链接:https://blog.51cto.com/xvjunjie/2473696
程序猿的技术大观园:www.javathinker.net
[这个贴子最后由 flybird 在 2020-03-12 13:21:34 重新编辑]
|
|