redis 5.0.3 讲解、集群搭建

REDIS

 

一 、redis 介绍

不管你是从事Python、Java、Go、PHP、Ruby等等... Redis都应该是一个比较熟悉的中间件。而大部分经常写业务代码的程序员,实际工作中或许只用到了set value、get value两个操作。

1.redis 概念:

Redis是一个开源的底层使用C语言编写的key-value存储数据库。可用于缓存、事件发布订阅、高速队列等场景。而且支持丰富的数据类型:string(字符串)、hash(哈希)、list(列表)、set(无序集合)、zset(sorted set:有序集合)

Redis 在3.0版本前只支持单实例模式,虽然支持主从模式、哨兵模式部署来解决单点故障,但是现在互联网企业动辄大几百G的数据,可完全是没法满足业务的需求,所以,Redis 在 3.0 版本以后就推出了集群模式。

2.Redis在项目中的应用场景

2.1、缓存数据 最常用,对经常需要查询且变动不是很频繁的数据 常称作热点数据。

2.2、消息队列 相当于消息订阅系统,比如ActiveMQ、RocketMQ。如果对数据有较高一致性要求时,还是建议使用MQ)

2.3、计数器 比如统计点击率、点赞率,redis具有原子性,可以避免并发问题

2.4、电商网站信息 大型电商平台初始化页面数据的缓存。比如去哪儿网购买机票的时候首页的价格和你点进去的价格会有差异。

2.5、热点数据 比如新闻网站实时热点、微博热搜等,需要频繁更新。总数据量比较大的时候直接从数据库查询会影响性能

3.数据类型

概念说明
String(字符串) string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value。string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。 string类型是Redis最基本的数据类型,一个键最大能存储512MB。
Hash(哈希) Redis hash 是一个键值对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。每个 hash 可以存储 2 *32 - 1键值对(40多亿)。
List(列表) Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾部(右边)。列表最多可存储 2*32 - 1元素 (4294967295, 每个列表可存储40多亿)。
Set(无序集合) Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。集合中最大的成员数为 2* 32 - 1(4294967295, 每个集合可存储40多亿个成员)。

| zset(sorted set:有序集合) | Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。 zset的成员是唯一的,但分数(score)却可以重复。 |

 

4.Redis数据类型应用场景

前面提到了Redis支持五种丰富的数据类型,那么在不同场景下的选择

String

字符串是最常用的数据类型,他能够存储任何类型的字符串,当然也包括二进制、JSON化的对象、甚至是base64编码之后的图片。在Redis中一个字符串最大的容量为512MB,可以说是无所不能了。

Hash

常用作存储结构化数据、比如论坛系统中可以用来存储用户的Id、昵称、头像、积分等信息。如果需要修改其中的信息,只需要通过Key取出Value进行反序列化修改某一项的值,再序列化存储到Redis中,Hash结构存储,由于Hash结构会在单个Hash元素在不足一定数量时进行压缩存储,所以可以大量节约内存。这一点在String结构里是不存在的。

List

List的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。另外,可以利用 lrange 命令,做基于 Redis 的分页功能,性能极佳,用户体验好。

Set

set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,这个时候就可以选择使用set。

Sorted Set

可以按照某个条件的权重进行排序,比如可以通过点击数做出排行榜的数据应用。

5 优势和缺点

5.1优势:

\1. 性能极高– Redis能读的速度是110000次/s,写的速度是81000次/s 。

\2. 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。

\3. 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。

\4. 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

 

5.2 缺点:

\1、由于是内存数据库,所以单台机器存储的数据量是有限的,需要开发者提前预估,需要及时删除不需要的数据。 \2、当修改Redis的数据之后需要将持久化到硬盘的数据重新加入到内容中,时间比较久,这个时候Redis是无法正常运行的。

5.3对比:Redis与其他key-value存储有什么不同

\1. Redis有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库的进化路径。Redis的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外的抽象。

\2. Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis可以做很多内部复杂性很强的事情。 同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需要进行随机访问。

二、安装redis

1.源码安装redis

#yum -y install gcc
# wget http://download.redis.io/releases/redis-5.0.3.tar.gz
# tar xzf redis-5.0.3.tar.gz
# cd redis-5.0.3
# make
 //默认端口 6379

 

1.1修改redis的配置文件

# pwd
/usr/local/redis/redis-5.0.3
​
# vim redis.conf
daemonize yes //把no 修改为yes,如果不修改,无法在后台继续运行

 

1.2启动redis

# pwd
/usr/local/redis/redis-5.0.3/src
#./redis-server ../redis.conf

通过客户端测试
# ./redis-cli
127.0.0.1:6379> set • value [expiration EX seconds|PX milliseconds] [NX|XX]
[root@LWJ01 src]# ./redis-cli
127.0.0.1:6379> ping   //查看服务是否运行
PONG
127.0.0.1:6379>

1.3设置开机自动启动

#vim /usr/lib/systemd/system/redis.service  
[Unit]
Description=Redis Server
After=network.target

[Service]
ExecStart=/usr/local/redis/redis-5.0.3/src/redis-server /usr/local/redis/redis-5.0.3/redis.conf  --daemonize no
ExecStop=/usr/local/redis/redis-5.0.3/src/redis-cli -p 6379 shutdown
Restart=always

[Install]
WantedBy=multi-user.target

//redis-cli、redis-server和redis.conf文件的位置根据你实际放置的位置设定,--daemonize no这个参数一定要带上,即使你配置文件里设置daemonize的值是yes,不然设置的服务不生效。

# systemctl daemon-reload
# systemctl start redis.service
# systemctl enable redis.service

2.通过yum 安装redis

# yum install redis

下载fedora的epel仓库
# yum install epel-release

2.1 文件路径

#pwd
/usr/bin //redis-server redis-cli 都在这个路径下面
#pwd
/etc   //redis.conf 配置文件的路径

2.2 启动服务

# systemctl start redis

使用配置文件启动 redis
# redis-server /etc/redis.conf &

查看进程
# ps -ef | grep redis
redis    13782     1  0 10:30 ?        00:00:00 /usr/bin/redis-server 127.0.0.1:6379
root     13813  8097  0 10:38 pts/0    00:00:00 grep --color=auto redis

查看端口
# ss -anpt | grep 6379
LISTEN     0      128    127.0.0.1:6379                     *:*                   users:(("redis-server",pid=13782,fd=4))

2.3 客户端登录

# redis-cli -h 127.0.0.1 -p 6379
## redis-cli -h 127.0.0.1 -p 6379 -a "123456" //设置后密码的登录
127.0.0.1:6379> ping
PONG
127.0.0.1:6379>

2.4 修改配置文件

# vim /etc/redis.conf
bind 127.0.0.1 //注释掉此行,不然只能本地访问 #bind 127.0.0.1
protected-mode yes   保护模式修改为no #product-mode no
requirepass 123456 修改默认密码,查找 requirepass foobared 将 foobared 修改为你的密码

2.5 客户端进行验证

# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379>

然后输入info,提示必须验证

输入用户名密码 (auth  123456)
#127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379> info

通过key * 查看所有的键(因为还没有使用,所以是空)
#127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>

2.6通过桌面客户端连接redis

redis desktop manager

https://redisdesktop.com/download (官网 )

github地址:https://github.com/uglide/RedisDesktopManager/releases

百度网盘:http://pan.baidu.com/s/1kU8sY3P

 

 

2.7 redis 备份和恢复

redis 数据的路径(其实备份和恢复就是cp)

导出:Redis写数据时先写到一个temp文件中,然后再把temp文件重命名为预定义的文件,所以即使Redis在运行,也可以直接用cp命令拷贝这个文件。

恢复:关闭Redis后直接覆盖掉demo.rdb,然后重启即可。

通过配置文件查看
#vim /etc/redis.conf
dbfilename dump.rdb
dir var/lib/redis


进行备份:
# redis-cli -h 192.168.25.65 -p 6379 -a "123456"
192.168.25.65:6379> ping
PONG
192.168.25.65:6379> SAVE //该命令将在 redis 安装目录中创建dump.rdb文件。
OK

进行恢复:
192.168.25.65:6379> CONFIG GET dir   //命令 CONFIG GET dir 输出的 redis 备份目录为 /var/lib/redis。
1) "dir"
2) "/var/lib/redis"
192.168.25.65:6379>

接下来的操作:
1.)停止redis服务
2.)拷贝备份文件到 /usr/local/redis/bin目录下
3.)重新启动redis服务

 

三 redis 集群搭建

简要说明:

2018年十月 Redis 发布了稳定版本的 5.0 版本,推出了各种新特性,其中一点是放弃 Ruby的集群方式,改为 使用 C语言编写的 redis-cli的方式,是集群的构建方式复杂度大大降低。

https://redis.io/topics/cluster-tutorial Redis官网查看集群搭建方式

以下步骤是在一台 Linux 服务器上搭建有6个节点的 Redis集群。(因为redis 集群最低6个节点,不然无法创建)

1 源码安装

#yum -y install gcc
# wget http://download.redis.io/releases/redis-5.0.3.tar.gz
# tar xzf redis-5.0.3.tar.gz
# cd redis-5.0.3
# make
//默认端口 6379

2.创建6个Redis配置文件,并配置

# pwd
/usr/local/redis/redis-5.0.3
#mkdir 6380 6381 6382 6383 6384 6385

然后拷贝原来的配置文件到目录下,然就在到相应目录下进行重命名
#cp redis.conf 6380   mv redis.conf redis-6380.conf
#cp redis.conf 6381   mv redis.conf redis-6381.conf
#cp redis.conf 6382   mv redis.conf redis-6382.conf
#cp redis.conf 6383   mv redis.conf redis-6383.conf
#cp redis.conf 6384   mv redis.conf redis-6384.conf
#cp redis.conf 6385   mv redis.conf redis-6385.conf

修改所有的配置文件 (加端口号的以此类推)
具体修改:
(1)port 6380  #绑定端口

(2)bind 192.168.25.64  #定IP也(可以注释掉,这样任何桌面客户端都可以连接了)

(3)dir /usr/local/redis-cluster/3680 #指定文件存放路径 ( .rdb .aof nodes-xxxx.conf 这样的文件都会在此路径下)

(4)cluster-enabled yes   #启动集群模式

(5)cluster-config-file #集群节点配置文件

(6)daemonize yes   #后台启动

(7)cluster-node-timeout 5000  #指定集群节点超时时间

(8)appendonly yes #指定持久化方式
 
(9)protected-mode no #非保护模式

3. 启动节点

# ./src/redis-server 6380/redis-6380.conf 
# ./src/redis-server 6381/redis-6381.conf
# ./src/redis-server 6382/redis-6382.conf
# ./src/redis-server 6383/redis-6383.conf
# ./src/redis-server 6384/redis-6384.conf
# ./src/redis-server 6385/redis-6385.conf

查看启动的节点进程

4. 启动集群

#./src/redis-cli --cluster create 192.168.25.64:6380 192.168.25.64:6381 192.168.25.64:6382 192.168.25.64:6383 192.168.25.64:6384 192.168.25.64:6385 --cluster-replicas 1    
// --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。(--cluster-replicas 1 命令的意思: 一主一从配置,六个节点就是 三主三从)



5. 客户端连接集群

# ./redis-cli -c -h 192.168.25.64 -p 6380
192.168.25.64:6380>

查看集群信息
192.168.25.64:6380> cluster info
cluster_state:ok         //集群状态
cluster_slots_assigned:16384   槽分配
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:7977
cluster_stats_messages_pong_sent:8091
cluster_stats_messages_sent:16068
cluster_stats_messages_ping_received:8086
cluster_stats_messages_pong_received:7977
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:16068
192.168.25.64:6380>

查看节点信息
192.168.25.64:6380> cluster nodes
0853d9773fdcdad1bc0174d64990c40c39b4a2c7 192.168.25.64:6384@16384 slave d9da56a8f068d5529a7771addf586d14e14ca888 0 1547453355000 5 connected
d9da56a8f068d5529a7771addf586d14e14ca888 192.168.25.64:6381@16381 master - 0 1547453356838 2 connected 5461-10922
4b8e4fa926486c8ab675b7bf2fe36320657f1eae 192.168.25.64:6380@16380 myself,master - 0 1547453354000 1 connected 0-5460
0e585e6a0f45e293680e7aec052d942d85dd90c3 192.168.25.64:6382@16382 master - 0 1547453356000 3 connected 10923-16383
8d9e75800b6a4cc1d2815fbb8dd0036ecd0221ce 192.168.25.64:6385@16385 slave 0e585e6a0f45e293680e7aec052d942d85dd90c3 0 1547453355837 6 connected
53d3770f2ee72b3548b8a4cb26a23e617f7558bb 192.168.25.64:6383@16383 slave 4b8e4fa926486c8ab675b7bf2fe36320657f1eae 0 1547453356000 4 connected
192.168.25.64:6380>

查看节点信息

6 集群测试

[root@LWJ01 src]# ./redis-cli -c -h 192.168.25.64 -p 6380
192.168.25.64:6380> set name ww
-> Redirected to slot [5798] located at 192.168.25.64:6381
OK
192.168.25.64:6381> set age 20
-> Redirected to slot [741] located at 192.168.25.64:6380
OK
192.168.25.64:6380> get name
-> Redirected to slot [5798] located at 192.168.25.64:6381
"ww"
192.168.25.64:6381> get age
-> Redirected to slot [741] located at 192.168.25.64:6380
"20"
192.168.25.64:6380>


# ./redis-cli -h 192.168.25.64 -p 6380
192.168.25.64:6380> set name ww
(error) MOVED 5798 192.168.25.64:6381
192.168.25.64:6380> set age 20
OK
192.168.25.64:6380> get name
(error) MOVED 5798 192.168.25.64:6381
192.168.25.64:6380> get age
"20"
192.168.25.64:6380>

可以看到,客户端连接加-c选项的时候,存储和提取key的时候不断在7031和7032之间跳转,这个称为客户端重定向。之所以发生客户端重定向,是因为Redis Cluster中的每个Master节点都会负责一部分的槽(slot),存取的时候都会进行键值空间计算定位key映射在哪个槽(slot)上,如果映射的槽(slot)正好是当前Master节点负责则直接存取,否则就跳转到其他Master节点负的槽(slot)中存取,这个过程对客户端是透明的。

7 redis 集群分区原理

7.1 槽(slot)的基本概念

从上面集群的简单操作中,我们已经知道redis存取key的时候,都要定位相应的槽(slot)。

Redis 集群键分布算法使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现: 一个 Redis 集群包含 16384 个哈希槽(hash slot), 它们的编号为0、1、2、3……16382、16383,这个槽是一个逻辑意义上的槽,实际上并不存在。redis中的每个key都属于这 16384 个哈希槽的其中一个,存取key时都要进行key->slot的映射计算。

 

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.25.64:6383 to 192.168.25.64:6380
Adding replica 192.168.25.64:6384 to 192.168.25.64:6381
Adding replica 192.168.25.64:6385 to 192.168.25.64:6382

# 从上面信息可以看出,创建集群的时候,哈希槽被分配到了三个主节点上,从节点是没有哈希槽的,6380 负责编号为 0 - 5460,共 5461个slots 、6381 负责编号为 5461 - 10922 ,共5462个slots、6382负责编号为 10923 - 16383 共5461个slots

7.2 集群分区好处

redis的集群分区,最主要的目的都是在移除、添加一个节点时对已经存在的缓存数据的定位影响尽可能的降到最小。redis将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点, 比如说:

  • 如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。

  • 与此类似, 如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。

因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在 节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线,从而保证集群的可用性。下面我们就来学习下集群中节点的增加和删除。

7.3 新增节点

## pwd
/usr/local/redis/redis-5.0.3
# mkdir 6386 6388
#cp redis.conf 6386     mv redis.conf redis-6386.conf
#cp redis.conf 6388     mv redis.conf redis-6388.conf
//进入到相应的目录下重命名,并进行配置

7.4 启动新增加的节点

# ./src/redis-server 6386/redis-6386.conf

进行查看
# ps -ef | grep redis

 

7.5 添加节点到集群

#./src/redis-cli --cluster add-node 192.168.25.64:6386 192.168.25.64:6386
#./src/redis-cli --cluster add-node 192.168.25.64:6388 192.168.25.64:6388
格式:
redis-cli --cluster add-node {新节点IP}:{新节点端口} {任意集群节点IP}:{对应端口} #如果添加集群中的主节点,则新添加的就是主节点,如果是从节点则是从节点

查看集群信息
# ./src/redis-cli -c -h 192.168.25.64 -p 6388
192.168.25.64:6386> cluster nodes

或者用这样可以查看集群检查集群
#./src/redis-cli --cluster check 192.168.25.64:6380

 

7.5.1 自定义分配槽

从添加主节点输出信息和查看集群信息中可以看出,我们已经成功的向集群中添加了一个主节点,但是这个主节还没有成为真正的主节点,因为还没有分配槽(slot),也没有从节点,现在要给它分配槽(slot)

 自定义分配槽(slot)
#./redis-cli --cluster reshard 192.168.25.64:6380

#系统提示要移动多少个配槽(slot),并且配槽(slot)要移动到哪个节点,任意输入一个数,如1024,再输入新增节点的ID1ee802b8051761fd26ab688ff9dca95df35ef3c4

#然后提示要从哪几个节点中移除1024个槽(slot),这里输入‘all’表示从所有的主节点中随机转移,凑够1024个哈希槽,然后就开始从新分配槽(slot)了。从新分配完后再次查看集群节点信息

 

7.5.2 查看分配的结果

#./src/redis-cli --cluster check 192.168.25.64:6380

也可以直接查看集群key、slot、slave分布信息
#./src/redis-cli --cluster info 192.168.25.64:6380

 

7.5.3 平衡各节点槽数量
现在想要各个节点槽的数量相当,所以要进行平衡
# ./src/redis-cli --cluster rebalance --cluster-threshold 1 192.168.25.64:6380

 

查看结果
#./src/redis-cli --cluster info 192.168.25.64:6380

7.6 指定从节点

查看节点状态
#./src/redis-cli -c -h 192.168.25.64 -p 6380

由下图可以看出 从节点 6383 的主节点是 6380 ,现在要把它变成 主节点6381的从节点,需要登录 6383客户端

# ./src/redis-cli -c -h  192.168.25.64 -p 6383
192.168.25.64:6383> cluster replicate d9da56a8f068d5529a7771addf586d14e14ca888
OK
192.168.25.64:6383>

再次查看集群节点信息
192.168.25.64:6383> cluster nodes

 

7.7 删除节点

删除从节点  6386
# ./src/redis-cli --cluster del-node 192.168.25.64:6386 9ba935475b55b9792e6ab0de7f974b8df68db11d
>>> Removing node 9ba935475b55b9792e6ab0de7f974b8df68db11d from cluster 192.168.25.64:6386
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.
[root@LWJ01 redis-5.0.3]#

# ./src/redis-cli -p 6386 cluster nodes
Could not connect to Redis at 127.0.0.1:6386: Connection refused
# ./src/redis-cli -p 6385 cluster nodes
通过下图可以看出从节点6386 已经被删除

 

删除主节点 3681(因为刚才删除的从节点就是6381的从节点,所以此步骤略)
1)如果主节点有从节点,将从节点转移到其他主节点
2)如果主节点有slot,去掉分配的slot,然后在删除主节点

#./redis-cli --cluster reshard 192.168.25.64:6380 //取消分配的slot
How many slots do you want to move (from 1 to 16384)? 4096 //被删除master的所有slot数量  
What is the receiving node ID? 5d8ef5a7fbd72ac586bef04fa6de8a88c0671052 //接收6378节点slot的master  
Please enter all the source node IDs.  
Type 'all' to use all the nodes as source nodes for the hash slots.  
Type 'done' once you entered all the source nodes IDs.  
Source node #1:03ccad2ba5dd1e062464bc7590400441fafb63f2 //被删除master的node-id  
Source node #2:done  
 
Do you want to proceed with the proposed reshard plan (yes/no)? yes //取消slot



查看结果: 6381 的槽已经被取消,分配给了6380
# ./src/redis-cli --cluster info 192.168.25.64:6380
192.168.25.64:6380 (4b8e4fa9...) -> 0 keys | 8192 slots | 1 slaves.
192.168.25.64:6381 (d9da56a8...) -> 0 keys | 0 slots | 0 slaves.
192.168.25.64:6382 (0e585e6a...) -> 0 keys | 4096 slots | 1 slaves.
192.168.25.64:6388 (1ee802b8...) -> 3 keys | 4096 slots | 1 slaves.
[OK] 3 keys in 4 masters.
0.00 keys per slot on average.

 

删除 6381
# ./src/redis-cli --cluster del-node 192.168.25.64:6381 d9da56a8f068d5529a7771addf586d14e14ca888
>>> Removing node d9da56a8f068d5529a7771addf586d14e14ca888 from cluster 192.168.25.64:6381
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.
[root@LWJ01 redis-5.0.3]#

查看结果:
# ./src/redis-cli --cluster info 192.168.25.64:6380
192.168.25.64:6380 (4b8e4fa9...) -> 0 keys | 8192 slots | 1 slaves.
192.168.25.64:6382 (0e585e6a...) -> 0 keys | 4096 slots | 1 slaves.
192.168.25.64:6388 (1ee802b8...) -> 3 keys | 4096 slots | 1 slaves.
[OK] 3 keys in 3 masters.
0.00 keys per slot on average.
[root@LWJ01 redis-5.0.3]#

posted @ 2019-01-15 17:30  LWJ303  阅读(9920)  评论(2编辑  收藏  举报
levels of contents