你简历中项目经验是不是有用到redis,但是面试官随便问你几个问题,你都是打不出来,或者答非所问,本篇文章针对redis面试中经常问到的问题出发,根据自身的理解,参考相关的文章,get到下面几点。
两个角度回答:高性能和高并发。
举一个项目的例子:一个http请求获取天气系统的天气数据,然后该天气系统会访问其他http服务获得数据。
不添加缓存:每次请求都要再进行一次http请求,耗费时间,性能差,同时,天气系统访问那个http服务的次数多了,可能会被人家限制不让访问。
添加缓存:第一次请求获取数据可能较慢,但是获取天气数据添加到缓存中,第二次获取数据,直接从天气系统的内存中获取,非常快,同时不再访问那个http服务,这样之后每次请求到缓存获取数据,允许的请求数量自然大了。
缓存分为本地缓存和分布式缓存:
以java为例,使用map实现的是本地缓存,好处是轻量,快速,生命周期随着jvm的销毁而结束。并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用redis或者memcached之类的是分布式缓存,多实例的情况下,共用一份缓存数据,缓存具有一致性,缺点就是要实现redis的高可用,整个程序搭建比较复杂。
应用场景:如果只有少量数据作为缓存,并且没有持久化的需求,就可以直接使用map作为缓存。
详细区别:
redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:
多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。
假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。
这样便完成了一次通信。
对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!
常用命令:set,get,decr,incr,mget等
结构:String数据结构是简单的key-value结构,value可以是字符串,也可以是数字
常用key-value缓存应用:常规计数,微博数,粉丝数。
常用命令:hget,hset,hgetall
hash是一个String类型的field和value的映射表,hash特别适用于存储对象,
应用场景:比如存储用户的信息,商品的信息。比如下面存放了我本人的信息
key=JavaUser599
value={
“id”: 1,
“name”: “xiaodeng”,
“age”: 22,
“location”: “Wuan, HeBei”
}
常用命令:lpush,rpush,lpop,rpop,lrange等
结构:List就是链表,redis的实现是一个双向链表,即可以支持反向查找和遍历,不过带来额外的内存开销。
应用场景:比如微博的关注列表,粉丝列表,消息列表等。
另外,可以通过lrange命令,就是从某个元素开始查找多少个元素,可以基于List实现分页操作,这是一个很棒的功能,基于redis实现简单的高性能分页,可以做微博那种不断下拉分页的的东西(一页一页走),性能高。
常用命令:sadd,spop,smembers,sunion等
结构,set对外提供的功能与List类似,都是列表的功能,特殊在于,Set可以自动排重。
适用场景:当你需要存储一个列表数据,同时不希望出现重复的数据,set是一个好选择,并且set提供了判断某个成员是否在set集合中的接口,list没有,可以基于set实现交集,并集,差集的操作。
比如在微博应用中,把一个用户所有关注的人存在一个集合中,把它的粉丝都存在一个集合中,redis可以很方便地实现共同关注,共同粉丝,共同喜欢等功能,这个过程就是求交集的过程。
SINTERSTORE myset myset1 myset2 //将myset1和myset2集合的交集数据放到myset集合中,返回集合中的元素个数。
常用命令:zadd,zrange,zrem,zcard等
和set相比,Sorted Set增加了一个权重参数score,使得集合中的参数可以根据score排序。
举例,在直播系统中,根据直播间用户刷礼物作为权重参数,来排序用户。
一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的。如果自己项目中判断是否过期,影响性能。刚好redis中有个设置时间过期的功能,即对存储在redis数据库中的数据设置一个过期时间。作为一个缓存数据库,这是非常实用的。
我们set key的时候,都可以设置一个expire time,就是过期时间。通过过期时间可以指定这个数据的存活时间。
如果我们对一批数据设置存活时间为一个小时,那么接下来的一个小时,redis是如何删除的呢?
定期删除+惰性删除
在第8个问题中,redis删除过期数据是通过定期删除+惰性删除,如果定期删除存在大量过期时间没有删除,同时也没有再次访问,那么这些过期数据大量堆积在内存中,导致redis内存耗尽怎么办?
同样的还有一个问题:mysql中2000w条数据,redis中存在20w条数据,如果保证redis中都是热点数据?
以上redis提供了6种数据淘汰策略:
redis支持两种不同的持久化操作,一种是快照,另外一种是只追加文件。
快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
appendonly yes
开启AOF持久化后每执行一条会更改redis中数据的命令,redis就会将该命令写入硬盘中的AOF文件中,AOP文件的保存位置和RDB保存的位置相同,都是通过dir参数来设置的,默认的文件名称是appendonly.aof。
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和性能,用户最好选择everysec,每秒钟同步一次AOF文件,Redis性能几乎没有影响,而且即使出现系统崩溃,最多损失一秒的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
事务提供了一种将多个命令请求打包,然后一次性按顺序执行多个命令的机制,并且在执行期间,服务器不会中断该事务而去执行其他的客户端命令。它会将事务中的所有命令都执行完毕,才会处理其他客户端的请求。
注意:redis同一个事务中,如果有一条命令执行失败,其后的命令仍会执行,没有回滚。
1. 什么是缓存穿透?
就是大量请求的key根本不存在缓存中,然后导致直接请求在数据库中,根本就么有经过缓存这一层。
举个例子:某个黑客故意制造缓存中不存在的key发起大量请求,导致大量请求直接作用在数据库上。一般mysql默认的最大连接数是150左右,其次服务器的cpu,内存,网络等都会限制并发能力,一般3000并发请求就能大死大部分数据库了。
2. 有哪些解决办法?
SET key value EX 100 //时间单位为:秒
这种方案可以解决key变化不频繁的情况,如果黑客恶意攻击每次构建不一样的key,会导致redis中存在大量无效的key。
这种方案不能从根本上解决问题,如果非要使用这种方案,尽量将无效key的过期时间设置短一些比如1分钟。
下面用java代码展示这种方案:
public Object getObjectInclNullById(Integer id) {
// 从缓存中获取数据
Object cacheValue = cache.get(id);
// 缓存为空
if (cacheValue == null) {
// 从数据库中获取
Object storageValue = storage.get(key);
// 缓存空对象
cache.set(key, storageValue);
// 如果存储数据为空,需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);// 必须设置过期时间,否则有被攻击的风险
}
return storageValue;
}
return cacheValue;
}
如果想了解布隆过滤器可以查看这篇文章《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐!
所谓的redis并发竞争key问题也就是多个系统同时对key进行操作,但是最后执行的顺序和我们期望的执行顺序不同,这样就导致了结果的不一样!
解决方案:分布式锁(zookeeper和redis都可以实现分布式锁),如果不存在Redis并发竞争key问题,就不要使用分布式锁,影响性能。
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
一般来说分两种情况:
此时会衍生出三个问题?
1.为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候缓存中的数据并不是单纯从数据库中拿出来的,可能需要通过计算,才能放入缓存。
另外更新缓存的代价很高,同时如果数据库频繁地修改数据,同时消耗很大的代价更新完缓存,但是,这个缓存到底会不会频繁地访问到?
所以直接删除缓存,等读的时候在从数据库中获取,添加到缓存中,利用懒加载的思想,不去做浪费资源的事情。
2.先更新数据库,再删除缓存,有什么问题?怎么解决?
会导致数据不一致的情况:比如先更新数据库,再删除缓存,如果缓存删除失败,就会导致数据不一致。
解决思路:先删除缓存,然后再更新数据库,如果数据库更新失败,那么数据库也是旧的数据,缓存也是空的,再次查询也是查询数据库数据到缓存中,不会出现不一致的情况。
3.在大量并发读的情况,同时同时有更新数据的情况,先删除缓存,再更新数据库有可能也会出现数据不一致的情况!
即,数据发生变更,先删除缓存,此时还没来得及更新数据库的数据,此时读请求进来,看到缓存中没有,访问数据库,查到了数据库旧的数据,然后添加到缓存中,随后数据变更完成对数据库数据的修改,此时就会出现缓存和数据库不一致。
解决方案:自己想吧
参考:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md
本文参考:JavaGuide
redis线程模型参考:https://www.javazhiyin.com/22943.html
redis.conf注释参考:http://download.redis.io/redis-stable/redis.conf
更多:邓新