来源:素文宅博客
转自:https://blog.yoodb.com/yoodb/article/detail/1564
目前Redis在互联网行业技术存储方面使用广泛,几乎所有后端技术面试官都会在Redis的使用和原理方面对求职者提一些问题。本篇通俗易懂的整理总结了近年来针对Redis缓存的相关面试题并附有相关的答案,希望大家在以后面试中能够顺利的拿到OFFER。
什么是Redis?
Redis全称Remote Dictionary Server,是一个完全开源免费的,遵守BSD协议,使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库。它是当前最热门的NoSql非关系型数据库之一,也被人们称为数据结构服务器。
Redis的数据类型有哪些?
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
1、String(字符串)
string是redis最基本的类型,可以理解成与Memcached一样的类型,一个key对应一个value。常用命令包括set、get、decr、incr、mget等,一个键最大存储512B。
应用场景:string类型是二进制安全的,通俗的说就是redis的string可以包含任何数据。如jpg图片或者序列化对象。
2、Hash(哈希)
hash是一个键值(key=>value)对集合;是string类型的field和value的映射表,hash适合用于存储对象。常用命令包括hget、hset、hgetall等,每个hash可以存储232 -1键值对(40多亿)。
应用场景:存储一些结构化的数据,比如用户的昵称、年龄、性别、积分等,存储一个用户信息对象数据。
3、List(列表)
list是按照插入顺序排序,可以添加一个元素到列表的头部(左边)或者尾部(右边)。常用命令包括lpush、rpush、lpop、rpop、lrange等,最多可存储232 - 1元素(4294967295,每个列表可存储40多亿)。
应用场景:list类型常被用于消息队列的服务,以完成多程序之间的消息交换。
4、Set(集合)
set是string类型的无序集合,类似list列表一样,在执行插入、删除和判断是否存在某元素时,效率很高。常用命令包括sadd、spop、smembers、sunion等,set包含最大元素数量是4294967295。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
集合最大的优势在于可以进行交集并集和差集操作,集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
应用场景:利用交集求共同数据;利用唯一性,可以统计访问网站的所有独立IP;好友推荐时可根据tag求交集,大于某个threshold(临界值的)就可以推荐。
5、zset(sorted set:有序集合)
zset和set一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数(score),zset的成员是唯一的,但分数却可以重复。redis正是通过分数来为集合中的成员进行从小到大的排序。
sorted set是插入有序的,即自动排序。常用命令包括zadd、zrange、zrem、zcard等,当需要一个有序的并且不重复的集合列表时,可以选择sorted set数据结构。
应用场景:如存储全班同学的成绩,其集合value可以是同学的学号,而score就可以是成绩;排行榜应用,根据得分列出topN的用户等。
Redis有什么优点?
1)速度快:数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)。
2)支持丰富数据类型:包括string、list、set、Zset、hash等数据类型。
3)持久化存储:提供RDB和AOF两种数据的持久化存储方案,解决内存数据库最担心的万一Redis挂掉,数据会消失掉。
4)丰富的特性:可用于订阅发布Pub/Sub功能,事务,缓存,消息,Key过期策略,支持多个DB,计数等。
Redis有什么缺点?
1)由于Redis是内存数据库,单台机器的存储数据量跟机器本身的内存大小有关,虽然Redis本身有Key过期策略,但是还是需要提前预估和节约内存。若内存增长过快,需要定期删除数据。可以使用Redis Cluster、Codis等方案,对Redis进行分区,从单机Redis变成集群Redis。
2)如果进行完整重同步时由于需要生成RDB文件,并进行传输,会占用主机的CPU,并会消耗网络的带宽。不过Redis2.8版本,已经有部分重同步的功能,但是还是有可能有完整重同步的。如新上线的备机。
3)修改配置文件需要重启时Redis会将硬盘中的数据加载进内存,比较耗时,在这个过程中Redis是不能提供服务的。
Redis相比memcached有哪些优势?
1)memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型;
2)redis的速度比memcached快很多;
3)redis可以持久化其数据;
4)Redis支持数据的备份,即master-slave模式的数据备份;
5)使用底层模型不同,它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
6)value大小:redis最大可以达到1GB,而memcache只有1MB。
Redis和memcached有哪些区别?
1)memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。
2)redis和memcache都是将数据存放在内存中,都是内存数据库。
3)redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
4)虚拟内存–redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘,这样能保证数据的持久性。
5)过期策略–memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10
6)分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从。
7)存储数据安全–memcache挂掉后,数据若超过内存大小,会丢失数据;redis可以定期保存到磁盘(持久化)。
8)灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复。
9)Redis支持数据的备份,即master-slave模式的数据备份。
*注:“Redis相比memcached有哪些优势?”和“Redis和memcached有哪些区别?”两者可相互借鉴。
Redis是否是单进程单线程方式,为什么?
Redis是单进程单线程的,Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库,由C语言编写。官方提供的数据是可以达到100000+的qps。
Redis处理效率快的原因有几方面:完全基于内存;数据结构简单,对数据操作也简单;使用多路I/O复用模型。
单进程单线程好处:代码更清晰,处理逻辑更简单;不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;不存在多进程或者多线程导致的切换而消耗CPU。
单进程单线程弊端:无法发挥多核CPU性能,不过可以通过在单机开多个Redis实例来完善。
Redis持久化有哪几种方式?都有什么优缺点?
redis提供两种持久化机制RDB和AOF机制,RDB持久化机制是对redis中的数据执行周期性的持久化,APF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。
RDB(Redis DataBase)持久化方式:是指用数据集快照的方式半持久化模式,记录redis数据库的所有键值对,在某个时间点将数据写入一个临时文件。持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
优点:
1)只有一个文件dump.rdb,方便持久化。
2)容灾性好,一个文件可以保存到安全的磁盘。
3)性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能。
4)相对于数据集大时,比AOF的启动效率更高。
缺点:
1)数据安全性低。RDB是间隔一段时间进行持久化,如果持久化之间redis发生 故障,会发生数据丢失。
AOF(Append-only file)持久化方式:是指所有的命令行记录以redis命令请 求协议的格式完全持久化存储)保存为aof文件。
优点:
1)数据安全,aof持久化可以配置appendfsync属性,有always,每进行一次 命令操作就记录到aof文件中一次。
2)通过append模式写文件,即使中途服务器宕机,可以通过redis-check-aof工具解决数据一致性问题。
3)AOF机制的rewrite模式。AOF文件没被rewrite之前(文件过大时会对命令 进行合并重写),可以删除其中的某些命令(比如误操作的flushall)。
缺点:
1)AOF文件比RDB文件大,且恢复速度慢。
2)数据量大的时候,比RDB启动效率低。
Redis常见的性能问题和解决方法
1)Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
2)如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3)为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
4)尽量避免在压力较大的主库上增加从库。
5)为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<--Slave1<--Slave2<--Slave3.......,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
Redis内存回收策略(淘汰策略)?
Redis也会因为内存不足而出错,也可能因为回收过久而导致系统长期的停顿,因此需要执行回收策略。Redis允许配置6种回收策略,在内存达到最大时,将会淘汰键值,并进行回收。内存回收策略如下:
volatile-lru :采用LRU算法(最近最少使用算法),但是只淘汰超时的键值对。
allkeys-lru :采用LRU算法,淘汰键值对。
volatile-random :采用随机淘汰策略,但是只淘汰超时的键值对。
allkeys-random :采用随机淘汰策略,淘汰键值对。
volatile-ttl :采用TTL算法(生存周期),淘汰存活时间最短的键值对。
noeviction :不淘汰任何键值对,内存已满时,进行写操作返回错误。
volatile和allkeys规定是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。
使用策略规则:
1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru;
2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random。
Redis的过期键删除策略有哪几种?
redis数据库键的过期时间都保存在过期字典中,根据系统时间和存活时间判断是否过期。redis有三种不同的删除策略:
定时删除:在设置键的过期时间的同时,创建一个定时器( timer ),让定时器在键的过期时间来临时,立即执行对键的删除操作。
惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库, 则由算法决定。
在这三种策略中,第一种和第三种为主动删除策略, 而第二种则为被动删除策略。
优缺点
1)定时删除,对内存友好,但是对cpu很不友好
2)惰性删除,对cpu友好,对内存很不友好
3)定期删除,是两种折中,但是,如果删除太频繁,将退化为定时删除,如果删除次数太少,将退化为惰性删除。
Redis中Pipeline有什么好处,为什么要用pipeline?
pipeline是redis批量提交的一种方式,也就是把多个命令操作建立一次连接发给redis去执行,在性能方面会比循环的单次提交会好很多。
pipeline的好处是可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。
Redis是如何实现的同步机制?
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
什么是缓存穿透?如何避免?什么是缓存雪崩?何如避免?
缓存穿透
一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
如何避免?
1)对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
2)对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。
如何避免?
1)在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2)做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
3)不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
为什么Redis要把所有数据放到内存中?
Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以Redis具有快速和数据持久化的特性。如果不将数据放到内存中,磁盘的I/O速度会严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后将不能继续插入新值。
Redis集群的原理是什么?
1)Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
2)Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
Redis集群在什么情况下会导致整个集群不可用?
假设有A,B,C三个节点的集群,在没有复制模型的情况下,如果节点B失败了,那么整个集群就会以为缺少5501-11000这个范围的槽而不可用。
Redis集群的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品。
Redis key的过期时间和永久有效分别怎么设置?
Redis key的过期时间是EXPIRE命令;Redis key的永久有效是PERSIST命令。
Redis支持的Java客户端都有哪些?官方推荐用哪个?
Redisson、Jedis、lettuce等等,官方推荐使用Redisson。
说说Redis哈希槽的概念?
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
怎么理解Redis事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行,事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis集群如何复制?最大节点数?是否支持选择数据库?
Redis集群是采用异步复制的;其最大节点数是16384个;目前无法做到选择性数据库,默认从零数据库开始。
Redis和Redisson有什么关系?
Redisson是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些 Java 的对象(Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet, Map, ConcurrentMap,List, ListMultimap, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock,ReadWriteLock, AtomicLong, CountDownLatch, Publish / Subscribe, HyperLogLog)。
Jedis与Redisson对比有什么优缺点?
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
怎么理解Redis事务?相关命令有哪些?
1)事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。
2)事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
3)事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis事务相关的命令有MULTI(开始)、EXEC(结束)、DISCARD(撤销)、WATCH(乐观锁)。
如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。关注微信公众号“Java精选”,每天推送优质文章。
Redis适合的场景有哪些?
1、会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台Magento 也提供 Redis 的插件。
2、全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。再次以 Magento 为例,Magento提供一个插件来使用 Redis 作为全页缓存后端。此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
3、队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。如果你快速的在 Google中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。
4、排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可:当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。
5、发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统。
一个Redis实例最多能存放多少的keys?List、Set、Sorted Set他们最多能存放多少元素?
理论上Redis可以处理多达232的keys,并且在实际中进行了测试,每个实例至少存放了2亿5千万的keys。任何list、set和sorted set都可以存放232个元素。换句话说,Redis的存储极限是系统中的可用内存值。
Redis持久化数据和缓存怎么做扩容?
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化,否则(即Redis节点需要动态变化的情况)必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
Redis如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以应该尽可能的将数据模型抽象到一个散列表里面。如web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。
Redis回收进程如何工作的?
Redis内存回收机制主要体现在以下两个方面:
1)删除到达时间的键对象。
2)内存使用达到maxmemory上限时触发内存溢出控制策略。
1、删除过期键对象
Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存了大量的键,维护每个键精准的过期删除机制会导致消耗大量的CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除和定时任务删除机制实现过期键的内存回收。关注微信公众号“Java精选”,每天推送优质文章。
惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
定时任务删除:Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例,使用快慢两种速率模式回收键。
比如:
定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25ms。
如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1ms且2s内只能运行1次。
快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
2、内存溢出控制策略
当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:
noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。应该没人用吧。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 Key。推荐使用,目前项目在用这种。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 Key。应该也没人用吧,你不删最少使用 Key,去随机删。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 Key。这种情况一般是把 Redis 既当缓存,又做持久化存储的时候才用。不推荐。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 Key。依然不推荐。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 Key 优先移除。不推荐。如果没有对应的键,则回退到noeviction策略。
其他文章推荐,关注微信公众号“Java精选”:
IntelliJ IDEA使用技巧——IDEA常用快捷键01期
IntelliJ IDEA使用技巧——IDEA增加jar包02期
IntelliJ IDEA使用技巧——IDEA工具Debug模式断点调试详解03期
IntelliJ IDEA使用技巧—IDEA设置默认Maven版本04期
IntelliJ IDEA使用技巧—使用EasyCode插件一键生成代码05期
IntelliJ IDEA使用技巧—在IDEA中Project和Module的概念、区别与关系06期
IntelliJ IDEA使用技巧—在IDEA使用中常用设置及性能优化图解07期
关注下方微信公众号“Java精选”(w_z90110),回复关键词领取资料:如Mysql、Hadoop、Dubbo、Spring Boot等,免费领取视频教程、资料文档和项目源码。
Java精选专注程序员推送一些Java开发知识,包括基础知识、各大流行框架(Mybatis、Spring、Spring Boot等)、大数据技术(Storm、Hadoop、MapReduce、Spark等)、数据库(Mysql、Oracle、NoSQL等)、算法与数据结构、面试专题、面试技巧经验、职业规划以及优质开源项目等。其中一部分由小编总结整理,另一部分来源于网络上优质资源,希望对大家的学习和工作有所帮助。