目录
一.NoSQL概述
1. 数据库类型
二. 部署Redis服务
1.安装Redis
2.验证服务
三. Redis数据类型及操作
1.字符串的概述
2.字符串操作命令
3.字符串实践
4.散列类型的概述
5. 散列类型操作命令
6.散列类型实践
7. 列表类型的概述
8.列表类型操作命令
9. 列表类型实践
10.集合类型
11. 集合实践
12.有序集合类型的概述
13.有序集合类型操作
14.有序集合类型实践
四.Redis其他操作命令
1.数据操作命令
2.数据库操作命令
3.修改Redis服务运行参数
RDBMS
- 关系数据库管理系统:Relational Database Management System
- 按照预先设置的组织结构,将数据存储在物理介质上
- 数据之间可以做关联操作
- 主流的RDBMS软件
- MySQL
- MariaDB
- Oracle
- DB2
- SQL Server
- PostgreSQL
NoSQL:Not Only SQL
- 意为"不仅仅是SQL"
- 泛指非关系型数据库
- 不需要预先定义数据存储结构
- 每条记录可以有不同的数据类型和字段个数
- 在一些数据库结构经常变化,数据结构不定的系统中,就非常适合使用NoSQL来存储。比如监控系统中的监控信息的存储,可能每种类型的监控信息都不太一样。这样可以避免经常对MySQL进行表结构调整,增加字段带来的性能问题。
- 这种架构的缺点就是数据直接存储在NoSQL中,不能做关系数据库的复杂查询,如果由于需求变更,需要进行某些查询,可能无法满足,所以采用这种架构的时候需要确认未来是否会进行复杂关系查询以及如何应对。
- 主流软件
- Redis
- Memcached
- MongoDB
- CouchDB
- Neo4j
- FlockDB
官网: Redis
从源代码
下载、解压和编译Redis:安装编译器
[root@redis1 ~]# yum install -y gcc [root@redis1 ~]# wget https://download.redis.io/releases/redis-4.0.8.tar.gz [root@redis1 ~]# tar xf redis-4.0.8.tar.gz [root@redis1 ~]# cd redis-4.0.8 # 修改安装目录为/usr/local/redis [root@redis1 redis-4.0.8]# vim +27 src/Makefile PREFIX?=/usr/local/redis # 编译安装 [root@redis1 redis-4.0.8]# make && make install # 将redis命令目录添加至PATH环境变量 [root@redis1 redis-4.0.8]# vim /etc/bashrc # 尾部追加 export PATH=$PATH:/usr/local/redis/bin [root@redis1 redis-4.0.8]# source /etc/bashrc # 初始化redis服务 [root@redis1 redis-4.0.8]# ./utils/install_server.sh # 全部问题直接回车采用默认值 Welcome to the redis service installer This script will help you easily set up a running redis server Please select the redis port for this instance: [6379] Selecting default: 6379 Please select the redis config file name [/etc/redis/6379.conf] Selected default - /etc/redis/6379.conf Please select the redis log file name [/var/log/redis_6379.log] Selected default - /var/log/redis_6379.log Please select the data directory for this instance [/var/lib/redis/6379] Selected default - /var/lib/redis/6379 Please select the redis executable path [/usr/local/redis/bin/redis-server] Selected config: Port : 6379 Config file : /etc/redis/6379.conf Log file : /var/log/redis_6379.log Data dir : /var/lib/redis/6379 Executable : /usr/local/redis/bin/redis-server Cli Executable : /usr/local/redis/bin/redis-cli Is this ok? Then press ENTER to go on or Ctrl-C to abort. Copied /tmp/6379.conf => /etc/init.d/redis_6379 Installing service... Successfully added to chkconfig! Successfully added to runlevels 345! Starting Redis server... Installation successful!
# 查看服务状态 [root@redis1 ~]# /etc/init.d/redis_6379 status Redis is running (10023) # 也可以使用以下方式查看服务状态 [root@redis1 ~]# service redis_6379 status Redis is running (10023) [root@redis1 ~]# ss -tlnp | grep :6379 LISTEN 0 128 127.0.0.1:6379 *:* users:(("redis-server",pid=10023,fd=6)) # 关闭服务 [root@redis1 ~]# service redis_6379 stop Stopping ... Redis stopped # 也可以使用以下方式关闭服务 [root@redis1 ~]# /etc/init.d/redis_6379 stop # 启动服务 [root@redis1 ~]# service redis_6379 start Starting Redis server... # 也可以使用以下方式启动服务 [root@redis1 ~]# /etc/init.d/redis_6379 start # 重启服务 [root@redis1 ~]# service redis_6379 restart Stopping ... Redis stopped Starting Redis server... # 也可以使用以下方式重启服务 [root@redis1 ~]# /etc/init.d/redis_6379 restart # 连接redis [root@redis1 ~]# redis-cli 127.0.0.1:6379> ping # 测试服务,正常返回PONG PONG 127.0.0.1:6379> exit
- 字符串类型是 Redis 中最基本的数据类型,它能存储任何形式的字符串,包括二进制数据
- 可以用其存储用户的邮箱、JSON 化的对象甚至是一张图片
- 一个字符串类型键允许存储的数据的最大容量是512 MB
- 字符串类型是其他4种数据类型的基础,其他数据类型和字符串类型的差别从某种角度来说只是组织字符串的形式不同
1)赋值命令
# 赋值 SET key value# 取值 GET key# 查看所有 keys *2)赋值命令示例
127.0.0.1:6379> SET username tom OK 127.0.0.1:6379> GET username "tom" 127.0.0.1:6379> GET password # 当键不存在时会返回空结果。 (nil)3) 递增数字:字符串类型可以存储任何形式的字符串,当存储的字符串是整数形式时,Redis 提供了一个实用的命令 INCR,其作用是让当前键值递增,并返回递增后的值。
# 当要操作的键不存在时会默认键值为0,所以第一次递增后的结果是1。 127.0.0.1:6379> INCR num (integer) 1 127.0.0.1:6379> INCR num (integer) 2 127.0.0.1:6379> GET num "2"4)当键值不是整数时Redis会提示错误
127.0.0.1:6379> SET foo bar OK 127.0.0.1:6379> INCR foo (error) ERR value is not an integer or out of range5) 增加指定的整数
127.0.0.1:6379> INCRBY num 2 (integer) 4 127.0.0.1:6379> INCRBY num 2 (integer) 66)递减数字
127.0.0.1:6379> DECR num (integer) 5 127.0.0.1:6379> DECRBY num 2 (integer) 37)向尾部追加值
127.0.0.1:6379> SET hi Hello OK 127.0.0.1:6379> APPEND hi " World" # 因为字符串包含空格,需要使用引号 (integer) 11 # 返回值为hi的总长度 127.0.0.1:6379> GET hi "Hello World"8)获取字符串长度
127.0.0.1:6379> STRLEN hi (integer) 119)中文字符返回字节数
127.0.0.1:6379> SET name 张三 OK 127.0.0.1:6379> STRLEN name (integer) 6 # UTF-8编码的中文,由于“张”和“三”两个字的UTF-8编码的长度都是3,所以此例中会返回6。 ```10) 同时获得/设置多个键值
127.0.0.1:6379> MSET username tom password tedu.cn OK 127.0.0.1:6379> MGET username password 1) "tom" 2) "tedu.cn"11)判断字段是否存在
127.0.0.1:6379> EXISTS num (integer) 1 127.0.0.1:6379> EXISTS number (integer) 012)字段不存在时赋值
127.0.0.1:6379> SETNX number 100 (integer) 1 127.0.0.1:6379> GET number "100"13) 查看所有的key
127.0.0.1:6379> KEYS *14)删除字段
127.0.0.1:6379> DEL number (integer) 1 127.0.0.1:6379> DEL number (integer) 015)查看键的类型
127.0.0.1:6379> TYPE name string
- Redis 对于键的命名并没有强制的要求,但比较好的实践是用“对象类型:对象ID:对象属性”来命名一个键,如使用键【`user:1:friends`】来存储ID为1的用户的好友列表。
- 例:如果你正在编写一个博客网站,博客的一个常见的功能是统计文章的访问量,我们可以为每篇文章使用一个名为【`post:文章ID:page.view`】的键来记录文章的访问量,每次访问文章的时候使用INCR命令使相应的键值递增。# 有用户访问文章ID号为42的博文,则将其访问计数加1 127.0.0.1:6379> INCR post:42:page.view (integer) 1 127.0.0.1:6379> GET post:42:page.view "1" 127.0.0.1:6379> INCR post:42:page.view (integer) 2 127.0.0.1:6379> GET post:42:page.view "2"
- 散列类型(hash)的键值也是一种字典结构,其存储了字段(field)和字段值的映射
- 字段值只能是字符串
- 散列类型适合存储对象。使用对象类别和 ID 构成键名,使用字段表示对象的属性,而字段值则存储属性值
1) 赋值与取值
# 设置单个字段 127.0.0.1:6379> HSET user1 name bob (integer) 1 127.0.0.1:6379> HSET user1 gender male (integer) 1 127.0.0.1:6379> HGET user1 name "bob" 127.0.0.1:6379> HGET user1 gender "male" # 设置多个字段 127.0.0.1:6379> HMSET user1 email bob@tedu.cn phone 13412345678 OK 127.0.0.1:6379> HMGET user1 email phone 1) "bob@tedu.cn" 2) "13412345678" # 获取所有字段 127.0.0.1:6379> HGETALL user1 1) "name" 2) "bob" 3) "gender" 4) "male" 5) "email" 6) "bob@tedu.cn" 7) "phone" 8) "13412345678"2) 判断
# 判断字段是否存在 127.0.0.1:6379> HEXISTS user1 address (integer) 0 127.0.0.1:6379> HEXISTS user1 name (integer) 1 # 当字段不存在时赋值 127.0.0.1:6379> HSETNX user1 address beijing (integer) 1 127.0.0.1:6379> HSETNX user1 address beijing (integer) 03) 数字递增
127.0.0.1:6379> HINCRBY user1 age 20 (integer) 20 127.0.0.1:6379> HINCRBY user1 age 1 (integer) 214) 删除字段
127.0.0.1:6379> HDEL user1 age (integer) 15) 只获取字段名
127.0.0.1:6379> HKEYS user1 1) "name" 2) "gender" 3) "email" 4) "phone" 5) "address"6) 只获取值
127.0.0.1:6379> HVALS user1 1) "bob" 2) "male" 3) "bob@tedu.cn" 4) "13412345678" 5) "beijing"7) 获得字段数量
127.0.0.1:6379> HLEN user1 (integer) 5
- 例:将文章ID号为10的文章以散列类型存储在Redis中
127.0.0.1:6379> HSET post:10 title 例解Python (integer) 1 127.0.0.1:6379> HGETALL post:10 1) "title" 2) "\xe4\xbe\x8b\xe8\xa7\xa3Python" 127.0.0.1:6379> HSET post:10 author ZhangZhiGang (integer) 1 127.0.0.1:6379> HMSET post:10 date 2021-05-01 summary 'Python Programming' OK 127.0.0.1:6379> HGETALL post:10 1) "title" 2) "\xe4\xbe\x8b\xe8\xa7\xa3Python" 3) "author" 4) "ZhangZhiGang" 5) "date" 6) "2021-05-01" 7) "summary" 8) "Python Programming"
- 列表类型(list)可以存储一个有序的字符串列表
- 常用的操作是向列表两端添加元素,或者获得列表的某一个片段- 列表类型内部是使用双向链表(double linked list)实现的,获取越接近两端的元素速度就越快
- 使用链表的代价是通过索引访问元素比较慢
- 这种特性使列表类型能非常快速地完成关系数据库难以应付的场景:如社交网站的新鲜事,我们关心的只是最新的内容,使用列表类型存储,即使新鲜事的总数达到几千万个,获取其中最新的100条数据也是极快的
1) LPUSH命令用来向列表左边增加元素,返回值表示增加元素后列表的长度
127.0.0.1:6379> LPUSH numbers 1 (integer) 1 127.0.0.1:6379> LPUSH numbers 2 3 (integer) 32) 取出列表所有元素
127.0.0.1:6379> LRANGE numbers 0 -1 # 起始下标为0,结束下标为-1 1) "3" 2) "2" 3) "1"3) RPUSH命令用来向列表右边增加元素,返回值表示增加元素后列表的长度
127.0.0.1:6379> RPUSH numbers 0 -1 (integer) 5 127.0.0.1:6379> LRANGE numbers 0 -1 1) "3" 2) "2" 3) "1" 4) "0" 5) "-1"4) 从列表两端弹出元素
127.0.0.1:6379> LPOP numbers "3" 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "1" 3) "0" 4) "-1" 127.0.0.1:6379> RPOP numbers "-1" 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "1" 3) "0"5) 获取列表中元素的个数
127.0.0.1:6379> LLEN numbers (integer) 36) 删除列表中指定的值
# 语法LREM key count vfile:///usr/share/doc/HTML/zh-CN/index.htmlalue#LREM命令会删除列表中前count个值为value的元素,返回值是实际删除的元素个数。根据count值的不同,LREM命令的执行方式会略有差异。
#(1)当 count > 0时 LREM 命令会从列表左边开始删除前 count 个值为 value的元素。
#(2)当 count < 0时 LREM 命令会从列表右边开始删除前 count 个值为 value 的元素。
#(3)当 count = 0是 LREM命令会删除所有值为 value的元素。127.0.0.1:6379> LPUSH numbers 0 1 2 0 1 2 0 1 2 (integer) 12 127.0.0.1:6379> RPUSH numbers 2 1 0 2 1 0 2 1 0 (integer) 21 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "1" 3) "0" 4) "2" 5) "1" 6) "0" 7) "2" 8) "1" 9) "0" 10) "2" 11) "1" 12) "0" 13) "2" 14) "1" 15) "0" 16) "2" 17) "1" 18) "0" 19) "2" 20) "1" 21) "0" # 127.0.0.1:6379> LREM numbers 2 1 # 从左侧开始删除2个1 (integer) 2 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "0" 3) "2" 4) "0" 5) "2" 6) "1" 7) "0" 8) "2" 9) "1" 10) "0" 11) "2" 12) "1" 13) "0" 14) "2" 15) "1" 16) "0" 17) "2" 18) "1" 19) "0" # 从右侧开始删除2个0 127.0.0.1:6379> LREM numbers -2 0 (integer) 2 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "0" 3) "2" 4) "0" 5) "2" 6) "1" 7) "0" 8) "2" 9) "1" 10) "0" 11) "2" 12) "1" 13) "0" 14) "2" 15) "1" 16) "2" 17) "1" # 删除所有的0 127.0.0.1:6379> LREM numbers 0 0 (integer) 5 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "2" 3) "2" 4) "1" 5) "2" 6) "1" 7) "2" 8) "1" 9) "2" 10) "1" 11) "2" 12) "1"7) 获得/设置指定索引的元素
# 获取numbers列表中下标为0的值 127.0.0.1:6379> LINDEX numbers 0 "2" # 设置下标为1的值为10 127.0.0.1:6379> LSET numbers 1 10 OK 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "10" 3) "2" 4) "1" 5) "2" 6) "1" 7) "2" 8) "1" 9) "2" 10) "1" 11) "2" 12) "1"8) 删除指定范围之外的所有元素
127.0.0.1:6379> LTRIM numbers 0 2 OK 127.0.0.1:6379> LRANGE numbers 0 -1 1) "2" 2) "10" 3) "2"9) 插入元素
# 在2的前面插入20 127.0.0.1:6379> LINSERT numbers BEFORE 2 20 (integer) 4 127.0.0.1:6379> LRANGE numbers 0 -1 1) "20" 2) "2" 3) "10" 4) "2" # 在2的后面插入30 127.0.0.1:6379> LINSERT numbers AFTER 2 30 (integer) 5 127.0.0.1:6379> LRANGE numbers 0 -1 1) "20" 2) "2" 3) "30" 4) "10" 5) "2"
- 例:记录最新的10篇博文
573470543127.0.0.1:6379> LPUSH posts:list 11 12 13 (integer) 3 127.0.0.1:6379> LRANGE posts:list 0 -1 1) "13" 2) "12" 3) "11"
- 集合中的每个元素都是不同的,且没有顺序
1)增加/删除元素
127.0.0.1:6379> SADD letters a b c (integer) 3 127.0.0.1:6379> SADD letters b c d (integer) 1 127.0.0.1:6379> SMEMBERS letters 1) "d" 2) "b" 3) "a" 4) "c" 127.0.0.1:6379> SREM letters a c (integer) 2 127.0.0.1:6379> SMEMBERS letters 1) "d" 2) "b"2) 判断元素是否在集合中
127.0.0.1:6379> SISMEMBER letters a (integer) 0 127.0.0.1:6379> SISMEMBER letters b (integer) 13) 集合运算
127.0.0.1:6379> SADD s1 a b c (integer) 3 127.0.0 573470543.1:6379> SADD s2 b c d (integer) 3 127.0.0.1:6379> SINTER s1 s2 1) "b" 2) "c" 127.0.0.1:6379> SUNION s1 s2 1) "a" 2) "c" 3) "b" 4) "d" 127.0.0.1:6379> SDIFF s1 s2 1) "a"4)获得集合中元素个数
127.0.0.1:6379> SCARD letters (integer) 25) 随机获得集合中的元素
# 在集合s1中随机取出两个不同元素。 127.0.0.1:6379> SRANDMEMBER s1 2 1) "b" 2) "c" # 在集合s1中随机取出两个有可能相同元素。 127.0.0.1:6379> SRANDMEMBER s1 -2 1) "c" 2) "c" 127.0.0.1:6379> SRANDMEMBER s1 -2 1) "a" 2) "b"6) 集合中随机弹出一个元素
127.0.0.1:6379> SPOP s1 "a" 127.0.0.1:6379> SMEMBERS s1 1) "b" 2) "c"
- 例:为文章号为10的博客文章添加标签
573470543127.0.0.1:6379> SADD post:10:tags python redis nginx (integer) 3 127.0.0.1:6379> SMEMBERS post:10:tags 1) "python" 2) "nginx" 3) "redis"
- 在集合类型的基础上有序集合类型为集合中的每个元素都关联了一个分数
- 这使得我们不仅可以完成插入、删除和判断元素是否存在等集合类型支持的操作,还能够获得分数最高(或最低)的前N个元素、获得指定分数范围内的元素等与分数有关的操作
- 虽然集合中每个元素都是不同的,但是它们的分数却可以相同。
- 有序集合类型在某些方面和列表类型有些相似
- 二者都是有序的
- 二者都可以获得某一范围的元素- 有序集合类型和列表也有着很大的区别,这使得它们的应用场景也是不同的
- 列表类型是通过链表实现的,获取靠近两端的数据速度极快,而当元素增多后,访问中间数据的速度会较慢,所以它更加适合实现如“新鲜事”或“日志”这样很少访问中间元素的应用
- 有序集合类型是使用散列表和跳跃表(Skip list)实现的,所以即使读取位于中间部分的数据速度也很快
- 列表中不能简单地调整某个元素的位置,但是有序集合可以(通过更改这个元素的分数)
- 有序集合要比列表类型更耗费内存
1) ZADD 命令用来向有序集合中加入一个元素和该元素的分数,如果该元素已经存在则会用新的分数替换原有的分数。ZADD命令的返回值是新加入到集合中的元素个数
127.0.0.1:6379> ZADD scores 88 tom 90 jerry 75 bob 92 alice (integer) 4 127.0.0.1:6379> ZRANGE scores 0 -1 1) "bob" 2) "tom" 3) "jerry" 4) "alice" 127.0.0.1:6379> ZRANGE scores 0 -1 WITHSCORES 1) "bob" 2) "75" 3) "tom" 4) "88" 5) "jerry" 6) "90" 7) "alice" 8) "92" 127.0.0.1:6379> ZADD scores 85 jerry (integer) 0 127.0.0.1:6379> ZRANGE scores 0 -1 WITHSCORES 1) "bob" 2) "75" 3) "jerry" 4) "85" 5) "tom" 6) "88" 7) "alice" 8) "92"2) 获得元素的分数
127.0.0.1:6379> ZSCORE scores tom "88"3) 获得指定分数范围的元素
127.0.0.1:6379> ZRANGEBYSCORE scores 80 90 WITHSCORES 1) "jerry" 2) "85" 3) "tom" 4) "88"4) 增加某个元素的分数
127.0.0.1:6379> ZINCRBY scores 3 bob "78" 127.0.0.1:6379> ZSCORE scores bob "78"5) 获得集合中元素的数量
127.0.0.1:6379> ZCARD scores (integer) 46) 获得指定分数范围内的元素个数
127.0.0.1:6379> ZCOUNT scores 80 90 (integer) 27) 删除元素
127.0.0.1:6379> ZREM scores bob (integer) 18) 获得元素的排名
127.0.0.1:6379> ZRANK scores tom # 获取tom的排名 (integer) 1 # 升序排列,从0开始计数 127.0.0.1:6379> ZREVRANK scores alice # 获取alice的排名 (integer) 0 # 降序排列,从0开始计数
- 例:将博客文章按照点击量排序
127.0.0.1:6379> ZADD posts:page.view 0 post:10:page.view (integer) 1 127.0.0.1:6379> ZINCRBY posts:page.view 1 post:10:page.view "1" 127.0.0.1:6379> ZRANGE posts:page.view 0 -1 WITHSCORES 1) "post:10:page.view" 2) "1"
1) 查看key过期时间
127.0.0.1:6379> TTL name (integer) -1 # -1 表示永不过期 # 创建key的同时,设置它的过期时间。key是user1:login,value是235124,超时时间60秒 127.0.0.1:6379> SET user1:login 235124 ex 60 OK
2)
设置key的过期时间(秒)127.0.0.1:6379> EXPIRE password 20 (integer) 1 127.0.0.1:6379> TTL password (integer) 15 127.0.0.1:6379> TTL password (integer) -2 # -2表示已过期3)查看key是否存在
127.0.0.1:6379> EXISTS password (integer) 04) 存盘
127.0.0.1:6379> SAVE OK
1) 默认Redis有16个库,将name移动到1号库
127.0.0.1:6379> MOVE name 1 (integer) 12) 选择库
127.0.0.1:6379> SELECT 1 OK 127.0.0.1:6379[1]> KEYS * 1) "name"3) 清除库
# 清除当前库 127.0.0.1:6379[1]> FLUSHDB OK 127.0.0.1:6379[1]> KEYS * (empty list or set) # 清除所有库 127.0.0.1:6379[1]> SELECT 0 OK 127.0.0.1:6379> KEYS * 1) "numbers" 2) "2" 3) "posts:list" 4) "letters" 5) "posts:page.view" 6) "scores" 7) "post:10:tags" 8) "foo" 9) "s1" 10) "s2" 11) "num" 12) "post:10" 13) "user1" 14) "username" 15) "hi" 16) "post:42:page.view" 127.0.0.1:6379> SELECT 1 OK 127.0.0.1:6379[1]> FLUSHALL OK 127.0.0.1:6379[1]> SELECT 1 OK 127.0.0.1:6379[1]> KEYS * (empty list or set)4)关闭Redis
127.0.0.1:6379> SHUTDOWN not connected> exit
1) 常用参数
- 监听地址:默认运行在127.0.0.1上
- 监听端口:默认监听6379端口(不用修改)
- 密码[root@redis1 ~]# vim /etc/redis/6379.conf protected-mode no # 关闭保护模式,以允许不指定服务绑定在哪个地址 # bind 127.0.0.1 # 注释此行,运行在0.0.0.0上 port 6379 # 端口号 requirepass tedu.cn # 密码2) 修改启动脚本
[root@redis1 ~]# vim +43 /etc/init.d/redis_6379 ... ... $CLIEXEC -p $REDISPORT -a tedu.cn shutdown ... ...3) 启动服务并测试
[root@redis1 ~]# service redis_6379 start Starting Redis server... [root@redis1 ~]# redis-cli -h 192.168.1.11 -p 6379 -a tedu.cn 192.168.1.11:6379> PING PONG
> 附:python操作redis > > ```shell > # 安装redis模块 > [root@zzgrhel8 ~]# pip3 install redis > > # 操作 > [root@zzgrhel8 ~]# python3 > >>> import redis > >>> help(redis.Redis) > >>> r = redis.Redis(host='192.168.1.11', db=0, password='tedu.cn') > # 操作字符串类型数据 > >>> r.set('name', 'tom')更多Python操作参见:https://www.cnblogs.com/melonjiang/p/5342505.html