Redis从入门到实战(入门篇)
前言
之前只是在项目简单使用了Redis(只是充当缓存层实现),对Redis的体系技术没深入了解,最近时间比较充裕,所以再次复习巩固Redis,然后打算写几篇博客记录以及分享所复习的Redis知识。
- Redis从入门到实战:入门篇
- Redis从入门到实战:实战篇
- Redis从入门到实战:进阶篇
- Redis从入门到实战:完结篇
- Redis简介
- 为什么要用Redis?
- Redis的优点
- Redis的应用场景
- Linux Centos7安装Redis与Window10安装Redis
- Redis客户端介绍与使用
- Redis核心配置文件详解
- Redis Key命名规范
- Redis最常用的5种数据类型及其对应的命令
- Redis的性能测试
- Redis是一个由C语言编写的,开源的key-value存储系统,它支持存储的value类型相对更多,包括String、List、Set、Sorted Set和Hash,而且这些操作都是原子性的,又被其作者戏称为数据结构服务器。
- 它属于NoSQL(Not Only SQL)数据库中的键值(Key-Value)存储数据库,即它属于与MySQL和Oracle等关系型数据库不同的非关系型数据库。它与memcached类似,但是优于memcached。
- 为了保证效率,数据都是缓存在内存中。Redis可以周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
- 假如我们不使用Redis,那么用户每次发送请求的时候,服务器都会前往关系型数据库(MySQL,Oracle...)查询数据(不考虑其他缓存技术),每次往返数据库会消耗大量系统资源。如果用户并发量高的话,可能服务器就承受不了,就会宕机崩溃。
文章图片
在这里插入图片描述 - 如果我们使用了Redis,那么用户每次发送请求的时候,先去查询Redis缓存,如果缓存中有想要的数据,就直接响应客户端。如果没有,再去查询关系型数据库,这样就能节省系统开销,如果再搭建Redis集群,那么就能应对大量的高并发场景。
文章图片
在这里插入图片描述
- Redis是一款性能极高的非关系型数据库,Redis读的速度是110000次/s,写的速度是81000次/s,非常适合做缓存。
- 丰富的数据类型,Redis支持String、List、Set、Sorted Set和Hash数据类型操作,从而支持大量的应用场景。
- 原子性,Redis所有操作都是原子性的,不用担心并发安全问题,同时Redis还支持对几个操作全并后的原子性执行。
- Redis还支持publish/subscribe,通知,key过期等特性。
- 缓存
并发量高的网站都使用了Redis作为缓存层的实现,缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。
Redis提供了键过期功能(适用存储手机验证码),也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。 - 计数器
什么是计数器?如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令(自增命令)来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。 - 分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 - 分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。 - 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。 - 最新列表
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。 - 消息系统
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,Redis不能和专业的消息中间件相比。
Redis客户端介绍与使用 https://blog.csdn.net/weixin_44176169/article/details/105025327
Redis核心配置文件(redis.conf) 下面列举了一些常见配置,当然还有其他配置信息,可以查看安装目录下的redis.conf。
配置 | 作用 | 默认 |
---|---|---|
port | Redis访问端口,由于Redis是单线程模型,因此单机开多个Redis进程的时候会修改端口 | 6379 |
bind | 如果bind选项为空或0.0.0.0的话,那会接受所有来自于可用网络接口的连接。 | 127.0.0.1 |
timeout | 指定在一个client空闲多少秒之后就关闭它,0表示不管 | 0 |
daemonize | 指定Redis是否以守护进程的方式启动 | no |
logfile | 配置log文件地址,默认打印在命令行终端的窗口上 | "" |
databases | 设置Redis数据库的数量,默认使用0号DB | 16 |
save | 把Redis数据保存到磁盘上,这个是在RDB的时候用的 | save 900 1,save 300 10,save 60 10000 |
dbfilename | dump的文件位置 | dump.rdb |
dir | Redis工作目录 | ./ |
slaveof | 主从复制,使用slaveof让一个节点称为某个节点的副本,这个只需要在副本上配置 | 关闭 |
masterauth | 如果主机使用了requirepass配置进行密码保护,使用这个配置告诉副本连接的时候需要鉴权 | 关闭 |
slave-read-only | 配置Redis的Slave实例是否接受写操作,即Slave是否为只读Redis | yes |
requirepass | 设置客户端认证密码 | 关闭 |
maxclients | 设置同时连接的最大客户端数量,一旦达到了限制,Redis会关闭所有的新连接并发送一个"max number of clients reached"的错误 | 关闭,默认10000 |
maxmemory | 不要使用超过指定数量的内存,一旦达到了,Redis会尝试使用驱逐策略来移除键 | 关闭 |
appendonly | 是否开启AOF | no |
appendfilename | AOF文件名称 | appendonly.aof |
requirepass | 设置客户端认证密码 | 关闭 |
- Redis key值是二进制安全的,这意味着可以用任何二进制序列作为key值,比如"me"的简单字符串到一个JPEG文件的内容都可以。空字符串也是有效key值。
- key不要太长,最好不要操作1024个字节,这不仅会消耗内存还会降低查找效率。
- key不要太短,如果太短会降低key的可读性。
- 在项目中,key最好有一个统一的命名规范(根据企业的需求),我们可以使用
分段设计法
来设计Key名称。
- 把表名(实体类名称)转化为Key前缀
- 主键名(或其他常用于搜索的字段)
- 主键值
- 存储的字段名(属性名称)
id | name | |
---|---|---|
10010 | zhangsan | zhangsan@qq.com |
10086 | lisi | lisi@qq.com |
set user:id:10010:email zhangsan@qq.com;
set user:id:10086:email lisi@qq.com;
127.0.0.1:6379> mset user:id:10010:email zhangsan@qq.com user:id:10086:email lisi@qq.com
OK
127.0.0.1:6379> mget user:id:10010:email user:id:10086:email
1) "zhangsan@qq.com"
2) "lisi@qq.com"
127.0.0.1:6379>
Redis最常用的5种数据类型及其对应的命令 上面说了Redis是Key-Value结构的非关系型数据库,其中Key只能是字符串(String),Value有多种数据结构:
- String
- Hash
- List
- Set
- Sorted Set
文章图片
在这里插入图片描述
- Redis命令用于在Redis服务上执行操作,要在Redis服务上执行命令需要一个Redis客户端。
- 可以选择Redis安装自带的客户端(redis-cli),也可以按照上面教程安装
Redis Web版本的客户端
。
- keys pattern:查找所有符合给定模式pattern的key,匹配模式有如下几种:
*:匹配所有key
?:匹配单个key - exists key:检查给定key是否存在
存在返回1
不存在返回0 - randomkey:从当前数据库中随机返回(不删除)一个
key
当数据库不为空时,返回一个key
当数据库为空时,返回nil
- del key [key...]:删除给定的一个或多个Key,不存在的key会被忽略
- expire key seconds:为给定key设置有效时间,以秒为单位,当key过期时(有效时间为0),它会被自动删除
设置成功返回 1
当key不存在或者不能为key设置生存时间时返回0 - ttl key:以秒为单位,返回给定key的剩余生存时间(TTL, time to live)
当key不存在时,返回-2
当key存在但没有设置剩余生存时间时,返回-1
否则,以秒为单位,返回key的剩余生存时间 - persist key:移除给定key的有效时间
当生存时间移除成功时,返回1
如果key不存在或key没有设置生存时间,返回0 - rename key newkey:将
key
改名为newkey
当key和newkey相同,或者key不存在时,返回一个错误
当newkey已经存在时, rename命令将覆盖旧值 - type key:返回key的类型
- select 数据库编号:(默认有16个数据库,0-15)
- move key 数据库编号:将当前数据库key移动到某个数据库,目标库有,则不能移动
- String类型是Redis中最为基础的数据存储类型,它在Redis中是二进制安全的,这 便意味着该类型可以接受任何格式的数据,如JPEG图像数据或Json对象描述信息等。 在Redis中字符串类型的Value最多可以容纳的数据长度是512M。
- Redis String命令是操作value为String类型的命令。
文章图片
在这里插入图片描述 - set key value:设定key持有指定的字符串value,如果该key存在则进行覆盖 操作。总是返回”OK”
- get key:获取key的value。如果与该key关联的value不是String类型,redis将返回错误信息,因为get命令只能用于获取String 类型的值;如果该key不存在,返 回null。
127.0.0.1:6379[1]> set user:id:1:name xq OK 127.0.0.1:6379[1]> get user:id:1:name "xq" 127.0.0.1:6379[1]> set user:id:1:name xxxq OK 127.0.0.1:6379[1]> get user:id:1:name "xxxq"
文章图片
在这里插入图片描述 - getset key value:先获取该key的值,然后在设置该key的值。
127.0.0.1:6379[1]> getset user:id:1:name zwq "xxxq" 127.0.0.1:6379[1]> get user:id:1:name "zwq"
- incr key:将指定的key的value原子性的递增1.如果该key不存在,默认初始值为0,在incr之后其值为1。如果value的值不能转成整型,如"xq",该操作将执行失败并返回相应的错误信息。
- decr key:将指定的key的value原子性的递减1.如果该key不存在,默认初始值为0,在incr之后其值为-1。如果value的值不能转成整型,如"xq",该操作将执行失败并返回相应的错误信息。
127.0.0.1:6379[1]> set user:id:1:age 22 OK 127.0.0.1:6379[1]> incr user:id:1:age (integer) 23 127.0.0.1:6379[1]> decr user:id:1:age (integer) 22
- incrby key increment:将指定的key的value原子性增加increment。如果该key不存在,默认初始值为0,在incrby之后,该值为increment。如果该值不能转成整型,如"xq"则失败并返回错误信息。
- decrby key decrement:将指定的key的value原子性减少decrement,如果该key不存在。默认初始值为0,在decrby之后,该值为decrement。如果该值不能转成整型,如"xq"则失败并返回错误信息。
127.0.0.1:6379[1]> incrby user:id:1:height 5 (integer) 5 127.0.0.1:6379[1]> decrby user:id:1:height 3 (integer) 2
- append key value:如果该key存在,则在原有的value后追加该值;如果该key不存在,则重新创建一个key/value。
127.0.0.1:6379[1]> append user:id:1:address guangdong (integer) 9 127.0.0.1:6379[1]> append user:id:1:address guangzhou (integer) 18 127.0.0.1:6379[1]> get user:id:1:address "guangdongguangzhou"
- Redis中的Hashes类型可以看成具有String Key和String Value的map容器。
- 所以该类型非常适合于存储值对象的信息。如Username、Password和Age等。
- 如果Hash中包含很少的字段,那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash可以存储4294967295个键值对。
文章图片
在这里插入图片描述 - hset key field value:为指定的key设定field/value对(键值对)。
- hget key field:返回指定的key中的field的值。
- hgetall key:获取key中的所有filed-vaule。
127.0.0.1:6379[1]> hset user:id:2 name zwq (integer) 1 127.0.0.1:6379[1]> hset user:id:2 age 22 (integer) 1 127.0.0.1:6379[1]> hget user:id:2 name "zwq" 127.0.0.1:6379[1]> hgetall user:id:2 1) "name" 2) "zwq" 3) "age" 4) "22"
- hmset key fields:设置key中的多个filed/value。
- hmget key fileds:获取key中的多个filed的值。
- hexists key field:判断指定的key中的filed是否存在。
- hlen key:获取key所包含的field的数量。
- hincrby key field increment:设置key中filed的值增加increment,如:age增加20。
127.0.0.1:6379[1]> hmset user:id:3 name zs age 18 OK 127.0.0.1:6379[1]> hmget user:id:3 name age 1) "zs" 2) "18" 127.0.0.1:6379[1]> hlen user:id:3 (integer) 2 127.0.0.1:6379[1]> hexists user:id:3 age (integer) 1 127.0.0.1:6379[1]> hincrby user:id:3 age 10 (integer) 28
Redis可视化客户端可以清晰看到Hash结构。
文章图片
在这里插入图片描述
- 在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不 存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是 4294967295。
- 从元素插入和删除的效率来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在毫秒级内完成。然而需要说明的是,如果元素插入或删除操作是作用于链表中间,那将会是非常低效的。
文章图片
在这里插入图片描述 - lpush key value1 value2...:在指定的key所关联的list的头部插入所有的values,如果该key不存在,该命令在插入的之前创建一个与该key关联的空链表,之后再向该链表的头部插入数据。插入成功,返回元素的个数。
- rpush key value1、value2…:在该list的尾部添加元素。
- lrange key start end:获取链表中从start到end的元素的值,start、end可为负数,若为-1则表示链表尾部的元素,-2则表示倒数第二个,依次类推…
127.0.0.1:6379[1]> lpush grade:7 class1 class2 class3 (integer) 3 127.0.0.1:6379[1]> rpush grade:7 class6 class5 class4 (integer) 6 127.0.0.1:6379[1]> lrange grade:7 0 5 1) "class3" 2) "class2" 3) "class1" 4) "class6" 5) "class5" 6) "class4"
文章图片
在这里插入图片描述 - lpushx key value:将一个值插入到已存在的列表头部。如果列表不在,操作无效。
- rpushx key value:一个值插入已存在的列表尾部(最右边)。如果列表不在,操作无效。
key不存在,插入失败 127.0.0.1:6379[1]> lpushx grade:8 class7 (integer) 0 127.0.0.1:6379[1]> rpushx grade:8 class8 (integer) 0
命令 | 作用 | |
---|---|---|
lpush key value [value ...] | 将一个或者多个值插入到列表key的表头 | |
rpush key value [value ...] | 将一个或者多个值插入到列表key的表尾 | |
lpushx key value | 当且仅当key存在时将值value插入到key的表头 | |
rpushx key value | 当且仅当key存在时将值value插入到key的表尾 | |
lpop key | 移除并返回列表key的头元素 | |
rpop key | 移除并返回列表的尾元素 | |
blpop key [key ...] timeout | lpop的阻塞式指令 | |
brpop key [key ...] timeout | rpop的阻塞式指令 | |
rpoplpush source destination | 将列表Source的尾元素弹出以及返回客户端,并且将该元素插入到destination列表中 | |
brpoplpush source destination timeout | rpoplpush的阻塞版 | |
lindex key index | 返回key中下表为index的元素 | |
linsert key before | after pivot value | 将值value插入到key中,位为pivot之前或者之后 |
llen key | 返回列表key的长度 | |
lrange key start stop | 返回列表key中指定区间内的元素 | |
lrem key count value | 根据count的值,移除列表中与参数value相等的元素 | |
lset key index value | 将列表key下表为index的元素值设置为value,0代表链表的头元素,-1代表链表的尾元素 | |
ltrim key start stop | 对一个列表进行trim |
- 在Redis中,我们可以将Set类型看作为没有排序的字符集合,和List类型一样,我们也可以在该类型的数据值上执行添加、删除或判断某一元素是否存在等操作。Set可包含的最大元素数是4294967295。
- 和List类型不同的是,Set集合中不允许出现重复的元素。和List类型相比,Set类型在功能上还存在着一个非常重要的特性,即在服务器端完成多个Set之间的聚合计算操作,如unions、intersections和differences。由于这些操作均在服务端完成, 因此效率极高,而且也节省了大量的网络IO开销。
文章图片
在这里插入图片描述
命令 | 作用 |
---|---|
sadd key member [member ...] | 将元素加入到集合key中,已经有的忽略 |
scard key | 返回集合key的元素个数 |
sdiff key [key ...] | 返回一个集合的全部成员,该集合是所有给定集合之间的差集 |
sdiffstore destination key [key ...] | 返回集合之间的差集,并将它保存在destination集合中 |
sinter key [key ...] | 返回集合中给定集合的交集 |
sinterstore destination key [key ...] | 返回给定集合之间的差集,并将它保存在destination集合中 |
sismember key member | 判断member元素是否为集合key成员 |
smembers key | 返回集合中的所有成员 |
smove source destination member | 将member元素从source集合移动到destination集合 |
spop key | 移除并返回集合中的一个随机元素 |
srandmember key [count] | 返回指定count个数的集合,count为正数表示不能重复,负数可以重复 |
srem key member [member ...] | 移除集合key中的多个元素 |
sunion key [key ...] | 返回所有指定key的并集 |
sunionstore destination key [key ...] |
127.0.0.1:6379> sadd names zs ls ww zl
(integer) 4
127.0.0.1:6379> sadd namess zs zl
(integer) 2
127.0.0.1:6379> sinter names namess
1) "zl"
2) "zs"
文章图片
在这里插入图片描述 Redis Sorted Set命令
- Sorted Set和Set类型极为相似,它们都是字符串的集合,都不允许重复的成员出现在一个Set中。它们之间的主要差别是Sorted-Sets中的每一个成员都会有一个分数(score)与之关联,Redis正是通过分数来为集合中的成员进行从小到大的排序。然而需要额外指出的是,尽管Sorted-Sets中的成员必须是唯一的,但是分数(score) 却是可以重复的。
- 在Sorted Set中添加、删除或更新一个成员都是非常快速的操作,其时间复杂度为集合中成员数量的对数。由于Sorted Set中的成员在集合中的位置是有序的,因此, 即便是访问位于集合中部的成员也仍然是非常高效的。事实上,Redis所具有的这一特征在很多其它类型的数据库中是很难实现的,换句话说,在该点上要想达到和Redis同样的高效,在其它数据库中进行建模是非常困难的。
文章图片
在这里插入图片描述
命令 | 作用 |
---|---|
zadd key source member [[source member] [...]] | 将一个或者多个member元素及其score值加入到有序集合key中 |
zcard key | 返回有序集合key的元素个数 |
zcount key min max | 返回有序集合key中,score值在min和max之间的元素个数 |
zincrby key increment member | 为有序集合key的成员member的score值加上增量increment |
zrange key start stop | 返回有序集key中,指定下标区间内的成员 |
zrevrange key start stop [withscores] | 返回指定区间内的成员递减顺序 |
zrevrangebyrank key max min [withscores] [limit offset count] | |
zrangebyscore key min max [withscopes] [limit offset count] | 返回score值介于min和max之间的集合 |
zrank key member | 返回有序集key中成员member的排名 |
zrevrank key member | 返回有序集key中成员member的递减排名 |
zrem key member [member ...] | 移除有序集key中的多个成员 |
zremrangebyrank key start stop | 移除有序集key中,指定排名区间内的所有成员 |
zremrangebyscore key min max | 移除有序集key中,指定score范围内的成员 |
zscore key member | 返回成员member的score值 |
127.0.0.1:6379> zadd mathScore 98 zs 97 ls 76 ww 100 zl
(integer) 4
127.0.0.1:6379> zcard mathScore
(integer) 4
127.0.0.1:6379> zscore mathScore ww
"76"
文章图片
在这里插入图片描述 Redis的性能测试 Redis在安装之后有一个redis-benchmark,这个就是Redis提供用于做性能测试的,它可以用来模拟N个客户端同时发出M个请求。首先看一下redis-benchmark自带的一些参数:
命令 | 作用 | 默认值 |
---|---|---|
-h | 服务器名称 | 127.0.0.1 |
-p | 服务器端口 | 6379 |
-s | 服务器Socket | 无 |
-c | 并行连接数 | 50 |
-n | 请求数 | 10000 |
-d | SET/GET值的字节大小 | 2 |
-k | 1表示keep alive,0表示重连 | 1 |
-r | SET/GET/INC使用随机Key而不是常量,在形式上key样子为mykey_ran:000000012456,-r的值决定了value的最大值 | 无 |
-p | 使用管道请求 | 1,即不使用管道 |
-q | 安静模式,只显示query/sec值 | 无 |
--csv \使用csv格式输出 | 无 | |
-l | 循环,无限运行测试 | 无 |
-t | 只运行使用逗号分割的命令的测试 | 无 |
-I | 空闲模式,只打开N个空闲线程并且等待 | 无 |
- redis-benchmark -q,运行该命令,测试QPS(每秒查询率),从测试结果来看,值为40000-50000之间,离官方说的每秒100000QPS还是有一定差距,当然这也和电脑配置有关。
D:\Redis>redis-benchmark.exe -q PING_INLINE: 44883.30 requests per second PING_BULK: 45641.26 requests per second SET: 37105.75 requests per second GET: 41237.11 requests per second INCR: 44208.66 requests per second LPUSH: 42625.75 requests per second LPOP: 44702.73 requests per second SADD: 44863.16 requests per second SPOP: 44822.95 requests per second LPUSH (needed to benchmark LRANGE): 44622.94 requests per second LRANGE_100 (first 100 elements): 23126.73 requests per second LRANGE_300 (first 300 elements): 12220.46 requests per second LRANGE_500 (first 450 elements): 9390.55 requests per second LRANGE_600 (first 600 elements): 7499.63 requests per second MSET (10 keys): 36941.26 requests per second
- 在实际项目中,Redis存储的Key和Value应该是数量丰富的,上面命令只是测试单一的key和单一的Value,我们测试使用-r模拟value到100000且将运行次数提高到1000000次,具体命令为
redis-benchmark -q -r 100000 -n 1000000
,从测试结果来看,和上面测试的差不多,说明Redis读写能力在Key和Value是否丰富上没多大关系,还是和电脑配置和网络带宽有关些。D:\Redis>redis-benchmark -q -r 100000 -n 1000000 PING_INLINE: 45968.56 requests per second PING_BULK: 47279.09 requests per second SET: 40817.99 requests per second GET: 44265.42 requests per second INCR: 44064.51 requests per second LPUSH: 44724.72 requests per second LPOP: 44974.14 requests per second SADD: 41211.62 requests per second SPOP: 45091.76 requests per second LPUSH (needed to benchmark LRANGE): 47034.48 requests per second LRANGE_100 (first 100 elements): 24550.12 requests per second LRANGE_300 (first 300 elements): 12818.54 requests per second LRANGE_500 (first 450 elements): 9410.61 requests per second LRANGE_600 (first 600 elements): 7476.92 requests per second MSET (10 keys): 31666.61 requests per second
- 这样就把Redis入门技术介绍完了,大家觉得OK的话,不妨来个或者关注也行。
- 参考资料:https://www.cnblogs.com/xrq730/p/8890896.html
推荐阅读
- Docker应用:容器间通信与Mariadb数据库主从复制
- 一个人的碎碎念
- 我从来不做坏事
- 从蓦然回首到花开在眼前,都是为了更好的明天。
- 西湖游
- 改变自己,先从自我反思开始
- leetcode|leetcode 92. 反转链表 II
- 从我的第一张健身卡谈传统健身房
- 自媒体形势分析
- 操作系统|[译]从内部了解现代浏览器(1)