1、为什么使用redis?
主要有两个考虑角度:高性能、高并发。
(PS:Redis大多数情况下用在缓存上,或者共享Session上面。如果只是为了分布式锁这些其他功能,还有其他中间件 Zookpeer 等代替,并非一定要使用 Redis。)
我们在碰到需要执行耗时特别久,且结果不频繁变动的 SQL,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应。
例如:现在有一个商品秒杀系统,同一时间,有几万甚至几十万的人在刷新页面数据。执行的是同一操作——向数据库查数据,查询的是同一个商品的信息,一次查询的时间是600ms,这样对于我们的数据库而言对于几万几十万的并发是无法支撑的,因为我们数据库一般单机情况2000qps就差不多报警了,即使能支撑,也需要太多从机数据库,而且执行的都是重复的命令,这对于数据库未免太浪费性能了。这时我们把缓存(redis或memcached)加进来,先去缓存中查询,如果缓存中没有,再去数据库查,把数据库中查出来的结果放到redis中,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里查询,2ms 搞定。性能提升 300 倍,而且可以省去一大堆对数据库的请求,减少了数据库的压力。
所以,通过上面的例子我们可以看出来,使用缓存可以提高性能(600ms变成了2ms),可以支撑更高的并发(减少了数据库的压力)。
但是:既然memcached也能实现,为什么我们非要用redis呢?继续往下看。
2、redis和memcached有什么区别?
- redis 支持复杂的数据结构
redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择(这是redis最大的优点之一,也是大多企业选择redis的原因之一)。
- redis 原生支持集群模式
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
- 性能对比
由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。
- redis 的线程模型
redis 内部使用文件事件处理器
file event handler(具体:请看本博客最下方的补充)
,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将 socket 产生的事件压入内存队列中,事件分派器根据 socket 上的事件来选择对应的事件处理器进行处理。
总结:大多数企业之所以选择Redis而不选择memcached都是因为前两个原因,即便有第三个缺点的存在,但是对于大多数企业的需求,影响并不大,因此近些年很多企业都开始由memcached转到redis上面来。
3、为什么redis单线程模型效率这么高?
- 纯内存操作
- 核心是基于非阻塞的 IO 多路复用机制
- 单线程反而避免了多线程的频繁上下文切换问题
4、redis都有哪些数据类型?
- string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)
#string(字符串,使用方式为get和set,用做最基础key/value缓存。) set key1 value1 get key1 #hash(可以理解成map) hset myhash name mercury #往myhash中set一个键值对 hset myhash age 26 #往person中set一个键值对 hset myhash id 1 #往myhash中set一个键值对 hget myhash name #读取myhash中的某个字段的值 "mercury" #myhash的结构如下 #myhash = { # "name": "mercury", # "age": 26, # "id": 1 #} #list(有序列表) lpush mylist 1 #往mylist中push一个值 lpush mylist 2 #往mylist中push一个值 lpush mylist 3 4 5 #往mylist中push多个值 rpop mylist #以先进后出的方式,从mylist中取值。 #可以使用lrange实现分页功能,性能高。 #0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。 lrange mylist 0 -1 #set(无序集合,自动去重。) sadd mySet 1 #添加元素 smembers mySet #查看全部元素 sismember mySet 3 #判断是否包含某个值 srem mySet 1 #删除某个/些元素 srem mySet 2 4 #删除某个/些元素 scard mySet #查看元素个数 spop mySet #随机删除一个元素 smove testSet mySet 2 #将一个set的元素移动到另外一个set sinter testSet mySet #求两set的交集 sunion testSet mySet #求两set的并集 sdiff yourSet mySet #求在testSet中而不在mySet中的元素 #zset(zset是排序的set) #注意:不同的是每个元素都会关联一个double类型的分数。 #redis正是通过分数来为集合中的成员进行从小到大的排序。 zset的成员是唯一的,但分数(score)却可以重复。 zadd sun 80 venus #往sun中添加元素 zadd sun 75 earth #往sun中添加元素 zadd sun 90 mercury #往sun中添加元素 zadd sun 62 mars #往sun中添加元素 zrevrange sun 0 3 # 获取排名前三的用户(默认是升序,所以需要 rev 改为降序) zrank sun earth #获取某用户的排名
5、redis有哪些过期策略?
两种:定期删除和惰性删除。
- 定期删除:
Redis 默认每个 100ms 检查,有过期 Key 则删除。需要说明的是,Redis 不是每个 100ms 将所有的 Key 检查一次,而是随机抽取进行检查,因为如果redis中有数量很大的key(例如几十万key),那么每隔100ms检查一次,redis很可能就死掉了,因为数据太多,检查太频繁,cpu负载太高了。如果只采用定期删除策略,会导致很多 Key 到时间没有删除。于是,惰性删除派上用场。
- 过期删除:
当你在获取某个 key 的时候,redis 会检查这个 key是否设置了过期时间,如果设置了过期时间,检查key是否过期,如果过期了此时就会删除,返回空值。
注意:由于定期删除是随机抽取 key进行检查的,所以一定会漏掉一部分key,而这时候如果该key也没走惰性删除,那相当于沉积在内存中了,时间一久,必定大量堆积在内存中,导致内存耗尽。这时候要依靠redis的内存淘汰机制。继续往下看。
6、redis有哪些内存淘汰机制?
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错(使用较少)。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key(使用较少)。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(使用较少)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key(使用较少)。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除(使用较少)。
配置内存淘汰机制方式:
在 redis.conf 中有一行配置:
# maxmemory-policy volatile-lru
7、redis主从架构?
- 主从架构:
一般一台redis平均能支撑的QPS为几万。对于缓存而言,大多数情况下是用来支撑读高并发的。所以会把架构做成主从(master-slave)架构,一主多从,主(master)负责写请求,并将数据复制到其它的从(slave)节点,从节点负责读请求。这样可以很轻松实现水平扩容,以支撑读高并发。
- 主从机制:
redis采用异步方式把数据复制到从(slave)节点,自redis2.8 开始,slave会周期性地确认自己每次复制的数据量;
一个主节点(master node)是可以配置多个从节点(slave node)的;
从节点可以连接其他的从节点;
从节点做复制的时候,不会阻塞主节点的正常工作;
从节点在做复制的时候,也不会阻塞对自己的查询操作,它会用旧的数据集来为外部请求提供服务;但是复制完成的时候,会先删除旧数据集,加载新数据集,这个时候会暂停对外服务;
从节点主要用来进行横向扩容,做读写分离,扩容的从节点可以提高读的吞吐量。
注意:在采用主从架构时,建议开启主节点的持久化(主节点的各种备份方案也建议开启),不建议用子节点作为主节点的数据热备份,因为,如果把主节点的持久化功能关掉,可能在(主节点)master宕机重启的时候数据是空的,这时从节点可能会来主节点复制数据,这样从节点(slave node)的数据也丢了。
补充:文件事件处理器(File Event Handler
)
IO多路复用:I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。
例子:模拟一个tcp服务器处理30个客户socket。
假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
1. 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D......这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。 (单线程模型)
2. 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。 这种类似于为每一个用户创建一个进程或者线程处理连接。 (多线程模型)
3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A.... 。(IO多路复用模型)
文件事件处理器:
如果是客户端要连接redis,那么会为socket关联连接应答处理器
如果是客户端要写数据到redis,那么会为socket关联命令请求处理器
如果是客户端要从redis读数据,那么会为socket关联命令回复处理器
客户端与redis通信的一次流程:
在redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socket的AE_READABLE事件跟命令请求处理器关联起来。
当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。接着redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。
命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系。