为什么效率比较高?
答:
是纯内存数据库,一般都是简单的存取操作,读取速度快
使用非阻塞IO,IO多路复用
采用单线程模型,保证每个操作的原子性,减少了线程的上下文切换和竞争
全程使用hash结构,读取速度快,再比如有序集合使用的跳表,不同场景下采用不同的底层数据结构来实现。
简述下Redis的过期策略?
答:我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用。
惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
定期删除:由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。
补充:1. 定期删除:1.redis每过100ms,从设置了过期时间的key中随机取出20个缓存key2.清除其中的过期key3.若过期key占比超过1/4,则重复步骤1。为什么定期删除采用的是随机策略,而不是对全部数据做清理呢?因为每100ms过滤所有缓存数据,对CPU压力较大。虽然定期删除解决了一部分过期数据的问题,但是还有不少数据并没有被清除出缓存,所以此时就有了惰性删除。2. 惰性删除:这种方式是被动触发的,部分过期缓存key未被清理出缓存,长久占用缓存资源(随机策略存在的缺陷性),随着缓存数据的不断增加,无法通过删除过期key的方式腾出空间,来储存新的热点数据。所以这里就需要我们的内存淘汰策略
简述下Redis复制?
老版实现(2.8版本以前)
1. 过程:
- 同步(一开始启动的时候)
- 从服务器向主服务器发送SYNC命令
- 收到命令的服务器执行bgsave命令,生成一个RDB文件,并且在这个过程中,缓存所有的写命令,因为在后台线程生成RDB文件的时候,这些新产生的写命令是不会记录的
- 当主服务器的bgsave执行完后,将RDB文件发送给从服务器,然后从服务器载入这个RDB文件,
到达执行bgsave的主服务器状态 - 主服务器再将缓存的写命令发送给从服务器,从服务器就可以恢复到主服务器当前的状态
- 传播命令(启动之后的方式)
- 将命令传播给从服务器,从服务器执行即可
缺点:初次复制的时候完全没问题,但是断线后重连时效率很低,重复的内容很多,降低了效率
新版实现(2.8版本开始)
-
完整重同步(用于处理初次复制的情况)
- 同上
-
部分重同步(用于断线后重复制的情况)
- 当从服务器断线后,主服务去记录那些断线期间的写命令,当重连的时候只将这部分发送给从服务器
- 实现方式,是有一个复制偏移量,每当主服务器向从服务器发送N字节数据的时候,就将自己的复制偏移量增加N,然后从服务器收到N字节的数据时,将自己的复制偏移量加N。然后还有一个复制积压缓冲区,是一个队列,这里会存最近一些发送给从服务器的数据,每次同步的时候,从服务器会将自己的复制偏移量发送给主服务器,然后主服务器检查是否同步,如果不同步就计算偏移量的差值,并将缓冲区中相应的数据发送给服务器
简述下Redis持久化的方式?
答:
-
RDB
- 简述: RDB即将当前数据生成快照,并保存于硬盘中。可以通过手动命令,也可以设置自动触发。
- 方式: save 和 bgsave
- 文件保存的东西:TYPE KEY VALUE TTL(time to live 如果有的话,也会有过期时间)
-
AOF
- 简述: AOF通过日志,对数据的写入修改操作进行记录。这种持久化方式实时性更好。通过配置文件打开-**AOF。
-
保存的方式:比如
RPUSH LIST "A" "B" [A,B]
RPUSH LIST "C" [A,B,C]
RPUSH LIST "D" "E" [ A,B,C,D,E]
LPOP LIST[B,C,D,E]
LPOP LIST[C,D,E]
这里是5条指令,但是不是保存这五条,我们只需要保存RPUSH "C" "D" "E"
AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
redis优化从哪些方面入手?
答:
- 在没有必要开启持久化的时候,关闭持久化
- 缩短键值对的存储长度,从以上数据可以看出,在 key 不变的情况下,value 值越大操作效率越慢,因为 Redis 对于同一种数据类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 的作者是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。
- 禁用长耗时的查询命令;
- 使用 Pipeline(管道技术)批量操作数据,在平常都是一个命令,一个返回结果,使用管道技术我们可以一次性发送多个命令,然后等得到总的结果后再返回。
Redis 内存淘汰机制了解么?
为什么会有内存淘汰机制:其实是因为Redis的删除机制并不是一次性删除所有过期的数据,而是每次随机选一部分删除。所以最终还是有可能内存溢出,另外内存淘汰机制的合理性,最理想的就是没有过期的数据不会删除,但是还是有可能没过期的数据被删除了,但redis的定位是缓存,所以你那些强业务性的东西,本来就不应该防在redis里,他是一个有误差的东西
答:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
Redis如何实现分布式锁?
- 加锁: 1、加锁本质上就是在redis上面设置一个key,这样当其他服务想获取这个锁的时候,都会执行set一个key的操作,这里这里有两个参数需要注意,nx是当key不存在的时候才进行设置,如果存在的话返回失败,然后还有一个ex参数,这里是设置key的过期时间,主要是为了防止如果某个服务获取锁后宕机了,不会导致死锁。2、解锁的时候也要注意,这里要防止一个误解锁问题,因为咋们前面说了锁是有过期时间的,那么如果锁过期了,业务代码还没有执行完,但是此时锁是已经释放了,也就是说其他人可能会去获取这把锁,此时当我们之前没执行完的业务代码执行完之后,并解锁的话,就会导致锁误解开。3、误解锁解决方案,为每个加锁生成一个uid,让解锁的时候,加锁的人可以知道这把锁是否是自己加的,这里注意,解锁此时也分为两步走了,第一步是判断是否是自己加的,第二步是进行删除。这里的两步操作同样需要保证原子性,为什么要保证原子性呢?假如A请求在获取锁对应的value值验证requestId相等后,下达删除指令。但是由于网络等原因,删除的指令阻塞住了。而此时锁因为超时自动解锁了,并且B请求获取到了锁,重新加锁。这时候A请求到删除指令执行了,结果把B请求好不容易获取到的锁给删了。
出现的问题
- 加锁没有保证原子性:将加锁和设置过期时间分为两步,如果加完锁后出现错误,导致过期时间没有设置,这样就会导致锁永远不会过期,解决方案:1、 最新的redis命令已经提供API同步。 2 、在redis中,lua脚本是当作命令来执行的,所以是原子的操作,所以我们可以利用lua命令来进行加锁
- 锁误解除:如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
// 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
}
如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
注意加锁和解锁的操作原子性
简述 Redis 中跳表的应用以及优缺点?
答:
跳表是一个可以快速查找的有序链表, 搜索、插入、删除操作的时间均为O(logn), 跳表虽然是非常有用的数据结构,但是很多书里都没有写这个,我在大学的数据结构课本里也没有写跳表,就导致很多人对跳表不熟悉。
跳表本质上是一个链表,因为链表的随机查找性能太差,是O(N),查找元素只能从头结点或者尾结点遍历。
跳表的优缺点:
作为快速查找的数据结构,跳表常用来和红黑树 做比较,列一下跳表和红黑树的优缺点吧
跳表的优点:
跳表实现起来相对简单。红黑树的定义和左旋右旋操作,确实复杂,我资质愚钝,理解起来还是有单困难。后面我会解释跳表实现简单的原因的.
区间查找方便。在跳表中找到一个节点后,就可以通过前后指针找到相邻的元素。红黑树则需要通过父节点,子节点去寻找,相对麻烦。
红黑树的优点:
内存占用小,只需要3个指针就可以(左子树,右子树,父节点) 而跳表有一个向后的指针,每一层都有一个向前的指针
红黑树的查找稳定,红黑树有着严格的定义,每次插入和删除数据都会通过左旋右旋来平衡树的结构,通过红黑树查找有着稳定的查找时间O(logn) ,为啥跳表是不稳定的,看到跳表是怎样确定层数的就明白了
跳表和普通链表相比,除了费内存,好像没啥缺点了
如何解决缓存与数据库不一致的问题?
Redis事务?
Redis三种高级数据结构?
- HyperLogLog:
- bitMap:
- GEO: