文章目录
- 1. Redis为什么这么快?
- 2. Redis的持久化机制是怎样的?
- 3. Redis 的过期策略是怎么样的?
- 4. Redis的内存淘汰策略是怎么样的?
- 5. 什么是热Key问题,如何解决热key问题?
- 6. 什么是大Key问题,如何解决?
- 7. 什么是缓存击穿、缓存穿透、缓存雪崩?
- 8. 什么情况下会出现数据库和缓存不一致的问题?
- 9. 如何解决Redis和数据库的一致性问题?
- 10. 为什么需要延迟双删,两次删除的原因是什么?
- 11. 如何用SETNX实现分布式锁?
- 12. 如何用Redisson实现分布式锁?
- 13. 公平锁和非公平锁的区别?
- 14. Redisson看门狗(watch dog)机制了解吗?
- 15. 什么是RedLock,他解决了什么问题?
- 16. Redis的哨兵机制?
- 17. 介绍一下Redis的集群模式?
- 18. 介绍下Redis集群的脑裂问题?
- 19. Redis为什么被设计成是单线程的?
- 20. 为什么Redis 6.0引入了多线程?
- 21. 为什么Lua脚本可以保证原子性?
- 22. Redis 的事务机制是怎样的?
- 23. Redis中key过期了一定会立即删除吗?
- 24. Redisson的lock和tryLock有什么区别?
- 25. 什么是Redis的Pipeline,和事务有什么区别?
- 26. 为什么Redis不支持回滚?
- 27. 如何用Redis实现乐观锁?
- 28. Redisson解锁失败,watchdog会不会一直续期下去?
- 29. Redis中的setnx和setex有啥区别?
- 30. Redis 与 Memcached 有什么区别?
- 31. Redis实现分布锁的时候,哪些问题需要考虑?
- 32. 如何用setnx实现一个可重入锁?
- 33. Redis 支持哪几种数据类型?
- 34. Redis中跳表的实现原理?(实现ZSet的主要数据结构)
- 35. Redis为什么要自己定义SDS?
- 36. Redis除了做缓存,Redis还能用来干什么?
- 37. Redis如何实现延迟消息?
参考:
https://www.yuque.com/hollis666
、https://www.mianshiya.com
、https://javaguide.cn
1. Redis为什么这么快?
基于内存
:Redis 是一种基于内存的数据库,数据存储在内存中,数据的读写速度非常快,因为内存访问速度比硬盘访问速度快得多。单线程模型
:Redis 使用单线程模型,这意味着它的所有操作都是在一个线程内完成的,不需要进行线程切换和上下文切换。这大大提高了 Redis 的运行效率和响应速度。多路复用 I/O 模型
:Redis 在单线程的基础上,采用了I/O 多路复用技术,实现了单个线程同时处理多个客户端连接的能力,从而提高了 Redis 的并发性能。高效的数据结构
:Redis 提供了多种高效的数据结构,如哈希表、有序集合、列表等,这些数据结构都被实现得非常高效,能够在 O(1) 的时间复杂度内完成数据读写操作,这也是 Redis 能够快速处理数据请求的重要因素之一。多线程的引入
:在Redis 6.0中,为了进一步提升IO的性能,引入了多线程的机制。采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。
2. Redis的持久化机制是怎样的?
Redis 提供了两种主要的持久化机制:RDB(Redis Database Backup) 和 AOF(Append Only File),以及两者结合的混合持久化模式。它们的核心目标是确保 Redis 在崩溃或重启后能恢复数据。
-
RDB(快照持久化)
RDB 会在指定的时间间隔将 Redis 内存中的数据生成一个快照(snapshot),并保存为一个二进制文件。
RDB的优点是:快照文件小、恢复速度快,适合做备份和灾难恢复。
RDB的缺点是:RDB 不是实时持久化,如果 Redis 崩溃,最后一次 RDB 之后的变更会丢失。 -
AOF(追加文件持久化)
AOF 会以日志的形式,将 Redis 执行的每一个写命令追加到一个文件中。当 Redis 重启时,通过重放 AOF 文件中的命令来恢复数据。
AOF的优点是:可以实现更高的数据可靠性、支持更细粒度的数据恢复,适合做数据存档和数据备份。
AOF的缺点是:文件大占用空间更多,每次写操作都需要写磁盘导致负载较高。
3. Redis 的过期策略是怎么样的?
Redis 的过期策略采用的是定期删除
和惰性删除
相结合的方式。
定期删除
:Redis 默认每隔 100ms 就随机抽取一些设置了过期时间的 key,并检查其是否过期,如果过期才删除。定期删除是 Redis 的主动删除策略,它可以确保过期的 key 能够及时被删除,但是会占用 CPU 资源去扫描 key,可能会影响 Redis 的性能。惰性删除
:当一个 key 过期时,不会立即从内存中删除,而是在访问这个 key 的时候才会触发删除操作。惰性删除是 Redis 的被动删除策略,它可以节省 CPU 资源,但是会导致过期的 key 始终保存在内存中,占用内存空间。
Redis默认同时开启定期删除和惰性删除两种过期策略。
定期删除其实并不会立即释放内存,而是把这些键标记为“已过期”,并放入一个专门的链表中。然后,在Redis的内存使用率达到一定阈值时,Redis会对这些“已过期”的键进行一次内存回收操作,释放被这些键占用的内存空间。
而惰性删除则是在键被访问时进行过期检查,如果过期了则删除键并释放内存。
需要注意的是,即使Redis进行了内存回收操作,也不能完全保证被删除的内存空间会立即被系统回收。
因为把内存返回给操作系统的开销很大,会导致频繁的系统调用和内存碎片化。当 Redis 释放对象时,jemalloc(Redis 默认使用 jemalloc 作为内存分配器) 只是把这些内存块标记为空闲,以供 Redis 进程内部再次使用,但并不把内存归还给操作系统。即使 jemalloc 把大块内存释放了,Linux 或其他操作系统也未必马上回收。操作系统会缓存内存以优化性能,并在其他进程需要时再回收。
4. Redis的内存淘汰策略是怎么样的?
不淘汰数据(默认)
:
- noeviction:当运行内存超过最大设置内存的时候,不会淘汰数据,而是直接返回报错禁止写入
设置了过期时间的数据淘汰
:
- volatile-random:随机淘汰掉设置了过期时间的key
- volatile-ttl:优先淘汰掉较早过期的key
- volatile-lru(redis3.0之前默认策略):淘汰掉所有设置了过期时间的,然后最久未使用的key
- volatile-Ifu(redis4.0后新增):与上面类似,不过是淘汰掉最少使用的key
所有数据的数据淘汰
:
- allkeys-random:随机淘汰掉任意的key
- allkeys-lru:淘汰掉缓存中最久没有使用的key
- allkeys-Ifu(redis4.0后新增):淘汰掉缓存中最少使用的key
5. 什么是热Key问题,如何解决热key问题?
热Key问题指在同一个时间点上,Redis中的同一个key被大量访问,就会导致流量过于集中,使得很多物理资源无法支撑,如网络带宽、物理存储空间、数据库连接等。
解决方案:
热点key拆分
:将热点数据分散到多个Key中,例如通过引l入随机前缀,使不同用户请求分散到多个Key,多个key分布在多实例中,避免集中访问单一Key。多级缓存
:在Redis前增加其他缓存层(如CDN、本地缓存),以分担Redis的访问压力。限流和降级
:在热点Key访问过高时,应用限流策略,减少对Redis的请求,或者在必要时返回降级的数据或空值。
6. 什么是大Key问题,如何解决?
Big Key是Redis中存储了大量数据的Key,包括value过大或者元素数量过多的情况,Big Key可能造成一些问题,包括:
- 内存分布不均。在集群模式下,不同slot分配到不同实例中,如果大key都映射到一个实例,则分布不均,查询效率也会受到影响。
- 由于Redis单线程执行命令,操作大Key时耗时较长,从而导致Redis出现其它命令阻塞的问题。
- 大Key对资源的占用巨大,在你进行网络I/O传输的时候,导致你获取过程中产生的网络流量较大,从而产生网络传输时间延长甚至网络传输发现阻塞的现象,例如一个key2MB,请求个1000次2000MB。
- 客户端超时。因为操作大Key时耗时较长,可能导致客户端等待超时。
7. 什么是缓存击穿、缓存穿透、缓存雪崩?
缓存击穿
:是指当某一key的缓存过期时大并发量的请求同时访问此key,瞬间击穿缓存服务器直接访问数据库,让数据库处于负载的情况。缓存穿透
:是指缓存服务器中没有缓存数据,数据库中也没有符合条件的数据,导致业务系统每次都绕过缓存服务器查询下游的数据库,缓存服务器完全失去了其应有的作用。缓存雪崩
:是指当大量缓存同时过期或缓存服务宕机,所有请求的都直接访问数据库,造成数据库高负载,影响性能,甚至数据库宕机。
8. 什么情况下会出现数据库和缓存不一致的问题?
在非并发的场景中
:缓存的操作和数据库的操作没办法保证原子性,有可能一个操作成功,一个操作失败的。所以存在不一致的情况。在并发场景中
:如果两个线程,同时进行先写数据库,后更新缓存的操作,就可能会出现不一致:
W | W |
---|---|
写数据库,更新成20 | |
- | 写数据库,更新成10 |
- | 写缓存,更新成10 |
写缓存,更新成20(数据不一致) |
如果在并发场景中,如果两个线程,同时进行先更新缓存,后写数据库的操作,同理,也可能会出现不一致:
W | W |
---|---|
写缓存,更新成20 | - |
- | 写缓存,更新成10 |
- | 写数据库,更新成10 |
写数据库,更新成20(数据不一致) | - |
还有一种低概率的场景,读写并发:
W | R |
---|---|
- | 读缓存,缓存中没有值 |
- | 读数据库,数据库中得到结果为10 |
写数据库和缓存,更新成20 | |
- | 写缓存,更新成10(数据不一致) |
9. 如何解决Redis和数据库的一致性问题?
1、先更新数据库, 再删除缓存。(并发量不高可以选择)
2、延迟双删:先删除缓存,再更新数据库,再删除一次缓存(并发量高可以选择)
先操作数据库,后操作缓存,是一种比较典型的设计模式——
Cache Aside Pattern
探讨一下第一个方案,先写数据库还是先删缓存?
都会存在问题,解决办法:延迟双删
- 先删缓存
如果我们是先删除缓存,再更新数据库,有一个好处,那就是:如果是先删除缓存成功了,但是第二步更新数据库失败了,这种情况是可以接受的,因为这样只是把缓存给清空了而已,但是不会有脏数据,也没什么影响,只需要重试就好了。
但是会存在读写并发导致数据不一致
的情况:
W | R |
---|---|
删除缓存 | - |
- | 读缓存,缓存中没有值 |
- | 读数据库,数据库中得到结果为10 |
更新数据库,更新成20 | |
- | 写缓存,更新成10(数据不一致) |
假如一个读线程,在读缓存的时候没查到值,他就会去数据库中查询,但是如果自查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就导致最终缓存会被重新用一个"旧值"覆盖掉。这也就导致了缓存和数据库的不一致的现象。
- 先写数据库
如果我们先更新数据库,再删除缓存,有一个好处,那就是缓存删除失败的概率还是比较低的
,除非是网络问题或者缓存服务器宕机的问题,否则大部分情况都是可以成功的。
并且这个方案还有一个好处,那就是数据库是作为持久层存储的,先更新数据库就能确保数据先写入持久层可以保证数据的可靠性和一致性,即使在删除缓存失败的情况下,数据库中已有最新数据。
但是这个方案也存在一个问题,那就是先写数据库,后删除缓存,如果第二步失败了,会导致数据库中的数据已经更新,但是缓存还是旧数据,导致数据不一致。
10. 为什么需要延迟双删,两次删除的原因是什么?
第一次删除缓存的原因:
第一次之所以要选择先删除缓存,而不是直接更新数据库,主要是因为先写数据库会存在一个比较关键的问题,那就是缓存的更新和数据库的更新不是一个原子操作,那么就存在失败的可能性。
如果写数据库成功了,但是删缓存失败了!那么就会导致数据不一致。
而如果先删缓存成功了,后更新数据库失败了,没关系,因为缓存删除了就删除了,又不是更新,不会有错误数据,也没有不一致问题。
所以,为了避免这个因为两个操作无法作为一个原子操作而导致的不一致问题,我们选择先删除缓存,再更新数据库。这是第一次删除缓存的原因。
第二次删除缓存的原因:
一般来说,一些并发量不大的业务,这么做就已经可以了,先删缓存,后更新数据(如果业务量不大,其实先更新数据库,再删除缓存其实也可以),基本上就能满足业务上的需求了。
但是如果是并发量比较高的话,那么就可能存在读写并发导致的不一致
的情况
"读写并发"的问题会导致并发发生后,缓存中的数被读线程写进去脏数据,那么就只需要在写线程在删缓存、写数据库之后,延迟一段时间,再执行一把删除动作就行了。
所以,为了避免因为先删除缓存而导致的读写并发问题,所以引入了第二次缓存删除。
有了第二次删除,第一次还有意义吗?
如果不要第一次删除,只保留第二次删除那么就这个流程就变成了:先更新数据库, 再删除缓存。
那么这个方案的缺点前面讲过了,一旦删除缓存失败,就会导致数据不一致的问题。
那么延迟双删的第二次删除不也一样可能失败吗?
确实第二次删除也还是有概率失败,但是因为我们在延迟双删的方案中先做了一次删除,而延迟双删的第二次删除只为了尝试解决
因为读写并发导致的不一致问题,或者说尽可能降低这种情况发生的概率。
11. 如何用SETNX实现分布式锁?
Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。
(1) 获取锁
SETNX lock_key unique_value
lock_key
:锁的名称(全局唯一标识这把锁)。
unique_value
:通常用一个随机值或UUID,标识这个锁是由哪个客户端持有的,用于解锁时确认锁的归属。
(2) 设置锁的过期时间
因为 SETNX 本身不会设置过期时间,如果客户端崩溃而未主动释放锁,锁会被永久占用。
所以通常需要在 SETNX
成功后,立即设置一个过期时间:
EXPIRE lock_key 10 # 设置10秒过期
问题:SETNX 和 EXPIRE 是两条命令,不是原子操作。可能在 SETNX 成功后、EXPIRE 执行前客户端崩溃,导致锁无过期时间。
解决方案: Redis 2.6.12+ 支持
SET lock_key unique_value NX EX 10
NX:等价于 SETNX,只在键不存在时设置。
EX 10:设置过期时间为 10 秒。
这是一个原子操作,可以避免上面的问题。
(3) 释放锁
释放锁时,必须 确保自己加的锁自己才能解,否则可能会误删别人的锁。
实现方法:
-
先 GET lock_key,判断 value 是否等于自己持有的 unique_value。
-
如果相等,则执行 DEL lock_key。
但这不是原子操作,可能在 GET 后,其他客户端已经获得锁。
为保证原子性,需要使用 Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
优点
(1)实现简单:SETNX命令实现简单,易于理解和使用。
(2)性能较高:由于SETNX命令的执行原子性,保证了分布式锁的正确性,SETNX命令是单线程执行的,所以性能较高。
缺点
(1)锁无法续期:如果加锁方在加锁后的执行时间较长,而锁的超时时间设置的较短,可能导致锁被误释放。
(2)无法避免死锁:如果加锁方在加锁后未能及时解锁(也未设置超时时间),且该客户端崩溃,可能导致死锁。
(3)存在竞争:由于SETNX命令是对Key的操作,所以在高并发情况下,多个客户端之间仍可能存在竞争,从而影响性能。
(4)setnx不支持可重入,可以借助redission封装的能力实现可重入锁。
12. 如何用Redisson实现分布式锁?
在使用SETNX实现的分布式锁中,因为存在锁无法续期导致并发冲突的问题,所以在真实的生产环境中用的并不是很多,其实,真正在使用Redis时,用的比较多的是基于Redisson实现分布式锁。
为了避免锁超时,Redisson中引入了看门狗的机制,他可以帮助我们在Redisson实例被关闭前,不断的延长锁的有效期。
可重入锁
基于Redisson可以非常简单的就获取一个可重入的分布式锁。基本步骤如下:
引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>最新版</version>
</dependency>
定义一个Redisson客户端:
@Configuration
public class RedissonConfig {@Bean(destroyMethod="shutdown")public RedissonClient redisson() throws IOException {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);return redisson;}
}
接下来,在想要使用分布式锁的地方做如下调用即可:
@Service
public class LockTestService{@AutowiredRedissonClient redisson;public void testLock(){RLock lock = redisson.getLock("myLock");try {lock.lock();// 执行需要加锁的代码} finally {lock.unlock();}}
}
也可以设置超时时间:
// 设置锁的超时时间为30秒
lock.lock(30, TimeUnit.SECONDS);
try {// 执行需要保护的代码
} finally {lock.unlock();
}
且这个锁也只能被这个线程解锁。Redisson 的 unlock 方法在解锁时,会去判断当前线程 ID 是否存在于redis 的加锁的 hash 结构中,如果有则认为可以解锁,如果没有,则无法解锁。
除了可重入锁以外,Redisson还支持公平锁(FairLock)以及联锁(MultiLock)的使用。
公平锁(FairLock)
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lock();
联锁(MultiLock)
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
13. 公平锁和非公平锁的区别?
非公平锁
:多个线程不按照申请锁的顺序去获得锁,而是直接去尝试获取锁,获取不到,再进入队列等待,如果能获取到,就直接获取到锁。
公平锁
:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁。
两种锁分别适合不同的场景中,存在着各自的优缺点,对于公平锁来说,他的优点是所有的线程都能得到资源,不会饿死在队列中。但是他存在着吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大的缺点。
而对于非公平锁来说,他可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必去唤醒所有线程,会减少唤起线程的数量。但是他可能会导致队列中排队的线程一直获取不到锁或者长时间获取不到锁,活活饿死的情况。
默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多
14. Redisson看门狗(watch dog)机制了解吗?
Redisson的看门狗(watch dog)主要用来避免Redis中的锁在超时后业务逻辑还未执行完毕,锁却被自动释放的情况。它通过定期刷新锁的过期时间来实现自动续期。
主要原理:
定时刷新
:如果当前分布式锁未设置过期时间,Redisson基于Netty时间轮启动一个定时任务,定期向Redis发送命令更新锁的过期时间,默认每10s发送一次请求,每次续期30s。释放锁
:当客户端主动释放锁时,Redisson会取消看门狗刷新操作。一旦客户端宕机,看门狗线程自然消失,锁也会在 30 秒后自动过期。
15. 什么是RedLock,他解决了什么问题?
RedLock是Redis的作者提出的一个多节点分布式锁算法,旨在解决使用单节点Redis分布式锁可能存在的单点故障问题。
Redis的单点故障问题:
1、在使用单节点Redis实现分布式锁时,如果这个Redis实例挂掉,那么所有使用这个实例的客户端都会出现无法获取锁的情况。
2、当使用集群模式部署的时候,如果master一个客户端在master节点加锁成功了,然后没来得及同步数据到其他节点上,他就挂了, 那么这时候如果选出一个新的节点,再有客户端来加锁的时候,就也能加锁成功,因为数据没来得及同步,新的master会认为这个key是不存在的。
RedLock通过使用多个Redis节点,来提供一个更加健壮的分布式锁解决方案,能够在某些Redis节点故障的情况下,仍然能够保证分布式锁的可用性。
RedLock是通过引入多个Redis节点来解决单点故障的问题。
在进行加锁操作时,RedLock会向每个Redis节点发送相同的命令请求,每个节点都会去竞争锁,如果至少在大多数节点上成功获取了锁,那么就认为加锁成功。反之,如果大多数节点上没有成功获取锁,则加锁失败。这样就可以避免因为某个Redis节点故障导致加锁失败的情况发生。
这样,当超过半数以上的节点都写入成功之后,即使master挂了,新选出来的master也能保证刚刚的那个key一定存在(否则这个节点就不会被选为master)。
需要注意的是,RedLock并不能完全解决分布式锁的问题。例如,在脑裂的情况下,RedLock可能会产生两个客户端同时持有锁的情况。
16. Redis的哨兵机制?
主从架构中,如果采用读写分离的模式,即主节点负责写请求,从节点负责读请求。假设这个时候主节点宕机了,没有新的
主节点顶替上来的话,就会出现很长一段时间写请求没响应的情况。
针对这个情况,便出现了哨兵这个机制。它主要进行监控作用,如果主节点挂了,将从节点切换成主节点,从而最大限度地
减少停机时间和数据丢失。
哨兵节点(Sentinel)
:主要作用是对Redis的主从服务节点进行监控,当主节点发生故障的时候,哨兵节点会选择一个
合适的从节点升级为主节点,并通知其他从节点和客户端进行更新操作。Redis节点
:主要包括master以及slave节点,就是Redis提供服务的实例。
主观下线和客观下线
主观下线
Sentinel每隔1s会发送ping命令给所有的节点。如果Sentinel超过一段时间还未收到对应节点的pong回复,就会认为
这个节点主观下线。
客观下线
假设目前有个主节点被一个sentinel的判断主观下线了,但可能主节点并没问题,只是因为网络抖动导致了一台哨兵的误判。因此,它会向其他哨兵发起投票,其他哨兵会判断主节点的状态进行投票,可以投赞成或反对,以此来确定这个主节点是不是真的出了问题!
如果认为下线的总投票数大于quorum(一般为集群总数/2+1,假设哨兵集群有3台实例,那么3/2+1=2),则判定
该主节点客观下线,此时就需要进行主从切换,而只有哨兵的leader才能操作主从切换。
17. 介绍一下Redis的集群模式?
Redis有三种主要的集群模式,用于在分布式环境中实现高可用性和数据复制。这些集群模式分别是:主从复制(Master-Slave Replication)
、哨兵模式(Sentinel
)和Redis Cluster模式
主从复制
主从模式中,包括一个主节点(Master)和一个或多个从节点(Slave)。主节点负责处理所有写操作和读操作,而从节点则复制主节点的数据,并且只能处理读操作。当主节点发生故障时,可以将一个从节点升级为主节点,实现故障转移(需要手动实现)。
主从复制的优势在于简单易用,适用于读多写少的场景。它提供了数据备份功能,并且可以有很好的扩展性,只要增加更多的从节点,就能让整个集群的读的能力不断提升。
但是主从模式最大的缺点,就是不具备故障自动转移的能力,没有办法做容错和恢复。
哨兵模式
为了解决主从模式的无法自动容错及恢复的问题,Redis引入了一种哨兵模式的集群架构。
哨兵模式是在主从复制的基础上加入了哨兵节点。哨兵节点是一种特殊的Redis节点,用于监控主节点和从节点的状态。当主节点发生故障时,哨兵节点可以自动进行故障转移,选择一个合适的从节点升级为主节点,并通知其他从节点和应用程序进行更新。
在原来的主从架构中,引入哨兵节点,其作用是监控Redis主节点和从节点的状态。通常需要部署多个哨兵节点,以确保故障转移的可靠性。
哨兵节点定期向所有主节点和从节点发送PING命令,如果在指定的时间内未收到PONG响应,哨兵节点会将该节点标记为主观下线。如果一个主节点被多数哨兵节点标记为主观下线,那么它将被标记为客观下线。
当主节点被标记为客观下线时,哨兵节点会触发故障转移过程。它会从所有健康的从节点中选举一个新的主节点,并将所有从节点切换到新的主节点,实现自动故障转移。同时,哨兵节点会更新所有客户端的配置,指向新的主节点。
哨兵节点通过发布订阅功能来通知客户端有关主节点状态变化的消息。客户端收到消息后,会更新配置,将新的主节点信息应用于连接池,从而使客户端可以继续与新的主节点进行交互。
这个哨兵模式的优点就是为整个集群提供了一种故障转移和恢复的能力。
Cluster模式
Redis Cluster是Redis中推荐的分布式集群解决方案。它将数据自动分片到多个节点上,每个节点负责一部分数据。
在Redis的Cluster 集群模式中,使用哈希槽(hash slot)的方式来进行数据分片,将整个数据集划分为多个槽,每个槽分配给一个节点。客户端访问数据时,先计算出数据对应的槽,然后直接连接到该槽所在的节点进行操作。
Redis Cluster将整个数据集划分为16384个槽,每个槽都有一个编号(0~16383),集群的每个节点可以负责多个hash槽,客户端访问数据时,先根据key计算出对应的槽编号,然后根据槽编号找到负责该槽的节点,向该节点发送请求。
18. 介绍下Redis集群的脑裂问题?
脑裂是指在分布式系统中,由于网络分区或其他问题导致系统中的多个节点(特别是主节点)
误以为自己是唯一的主节点
。
这种情况会导致多个主节点同时提供写入服务,从而引起数据不一致。
为什么会产生脑裂?
Redis的脑裂问题可能发生在网络分区或者主节点出现问题的时候:
-
网络分区
:网络故障或分区导致了不同子集之间的通信中断。
Master节点,哨兵和Slave节点被分割为了两个网络,Master处在一个网络中,Slave库和哨兵在另外一个网络中,此时哨兵发现和Master连不上了,就会发起主从切换,选一个新的Master,这时候就会出现两个主节点的情况。 -
主节点问题
:集群中的主节点之间出现问题,导致不同的子集认为它们是正常的主节点。
Master节点有问题,哨兵就会开始选举新的主节点,但是在这个过程中,原来的那个Master节点又恢复了,这时候就可能会导致一部分Slave节点认为他是Master节点,而另一部分Slave新选出了一个Master
如何避免脑裂?
配置参数:
min-replicas-to-write 1
min-replicas-max-lag 10
含义:
-
至少有 1 个从节点且同步延迟不超过 10 秒时,主节点才接受写操作。
-
如果主节点与从节点断开(或者延迟太大),主节点将拒绝写请求,防止脑裂数据不一致。
19. Redis为什么被设计成是单线程的?
Redis并没有在网络请求模块和数据操作模块中使用多线程模型,主要是基于以下四个原因:
- Redis 操作基于内存,绝大多数操作的性能瓶颈不在 CPU
- 单线程模型,避免了线程间切换带来的性能开销
- 在单线程中使用多路复用 I/O技术也能提升Redis的I/O利用率
20. 为什么Redis 6.0引入了多线程?
虽然之前采用了多路复用技术,但是多路复用的IO模型本质上仍然是同步阻塞型IO模型。
从上图我们可以看到,在多路复用的IO模型中,在处理网络请求时,调用 select (其他函数同理)的过程是阻塞的,也就是说这个过程会阻塞线程,如果并发量很高,此处可能会成为瓶颈。
如果能采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可以减少由于网络 I/O 等待造成的影响,还可以充分利用 CPU 的多核优势。
所以,Redis 6.0采用多个IO线程来处理网络请求,网络请求的解析可以由其他线程完成,然后把解析后的请求交由主线程进行实际的内存读写。提升网络请求处理的并行度,进而提升整体性能。
但是,Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。
21. 为什么Lua脚本可以保证原子性?
Lua脚本可以保证原子性,因为Redis会将Lua脚本封装成一个单独的事务,而这个单独的事务会在Redis客户端运行时,由Redis服务器自行处理并完成整个事务,如果在这个进程中有其他客户端请求的时候,Redis将会把它暂存起来,等到 Lua 脚本处理完毕后,才会再把被暂存的请求恢复。
这样就可以保证整个脚本是作为一个整体执行的,中间不会被其他命令插入。但是,如果命令执行过程中命令产生错误,事务是不会回滚的,将会影响后续命令的执行。
也就是说,Redis保证以原子方式执行Lua脚本,但是不保证脚本中所有操作要么都执行或者都回滚。
22. Redis 的事务机制是怎样的?
Redis中是支持事务的,他的事务主要目的是保证多个命令执行的原子性,即要在一个原子操作中执行,不会被打断。
需要注意的是,Redis的事务是不支持回滚的
从 Redis 2.6.5 开始,服务器会在累积命令的过程中检测到错误。然后,在执行 EXEC 期间会拒绝执行事务,并返回一个错误,同时丢弃该事务。
如果事务执行过程中发生错误,Redis会继续执行剩余的命令而不是回滚整个事务。
Redis错误有两种情况,一种是在命令排队的过程就就检测到的错误,比如语法错误,比如内存不够了,等等。在这种错误,会在调用 EXEC 后,命令可能会直接失败。
还有一种错误,是在调用 EXEC 后,命令执行过程中出现的错误,最常见的就是操作类型不一致,比如对字符串进行列表相关的操作。这种就是在执行过程中才会出现的。
23. Redis中key过期了一定会立即删除吗?
Redis的键有两种过期方式:一种是被动过期,另一种是主动过期。
被动过期指的是当某个客户端尝试访问一个键,发现该键已经超时,那么它会被从Redis中删除。
当然,仅仅依靠被动过期还不够,因为有些过期的键可能永远不会再被访问。这些键应该被及时删除,因此Redis会定期随机检查一些带有过期时间的键。所有已经过期的键都会从键空间中删除。
具体来说,Redis每秒会执行以下操作10次:
- 从带有过期时间的键集合中随机选择20个键。
- 删除所有已经过期的键。
- 如果已经过期的键占比超过25%,则重新从步骤1开始。
直到过期Key的比例下降到 25% 或者这次任务的执行耗时超过了25毫秒,才会退出循环
所以,Redis其实是并不保证Key在过期的时候就能被立即删除的。因为一方面惰性删除中需要下次访问才会删除,即使是主动删除,也是通过轮询的方式来实现的。如果要过期的key很多的话,就会带来延迟的情况。
24. Redisson的lock和tryLock有什么区别?
tryLock是尝试获取锁,如果能获取到直接返回true,如果无法获取到锁,他会按照我们指定的waitTime进行阻塞,在这个时间段内他还会再尝试获取锁。如果超过这个时间还没获取到则返回false。如果我们没有指定waitTime,那么他就在未获取到锁的时候,就直接返回false了。
lock的原理是以阻塞的方式去获取锁,如果获取锁失败会一直等待,直到获取成功。
25. 什么是Redis的Pipeline,和事务有什么区别?
Redis 的 Pipeline 机制是一种用于优化网络延迟的技术,主要用于在单个请求/响应周期内执行多个命令。在没有 Pipeline 的情况下,每执行一个 Redis 命令,客户端都需要等待服务器响应之后才能发送下一个命令。这种往返通信尤其在网络延迟较高的环境中会显著影响性能。
在 Pipeline 模式下,客户端可以一次性发送多个命令到 Redis 服务器,而无需等待每个命令的响应。Redis 服务器接收到这批命令后,会依次执行它们并返回响应。
所以,Pipeline通过减少客户端与服务器之间的往返通信次数,可以显著提高性能,特别是在执行大量命令的场景中。
但是,需要注意的是,Pipeline是不保证原子性的,他的多个命令都是独立执行的,Redis并不保证这些命令可以以不可分割的原子操作进行执行。这是Pipeline和Redis的事务的最大的区别。
虽然都是执行一些相关命令,但是Redis的事务提供了原子性保障,保证命令执行以不可分割、不可中断的原子性操作进行,而Pipeline则没有原子性保证。
但是他们在命令执行上有一个相同点,那就是如果执行多个命令过程中,有一个命令失败了,其他命令还是会被执行,而不会回滚的。
26. 为什么Redis不支持回滚?
Redis是不支持回滚的,即使是Redis的事务和Lua脚本,在执行的过程中,如果出现了错误,也是无法回滚的
因为
-
Redis的设计就是简单、高效等,所以引入事务的回滚机制会让系统更加的复杂,并且影响性能。
-
从使用场景上来说,Redis一般都是被用作缓存的,不太需要很复杂的事务支持,当人们需要复杂的事务时会考虑持久化的关系型数据库。
-
相比于关系型数据库,Redis是通过单线程执行的,在执行过程中,出现错误的概率比较低,并且这些问题一般在编译阶段都应该被发现,所以就不太需要引入回滚机制。
27. 如何用Redis实现乐观锁?
在Redis中,想要实现这个功能,我们可以依赖 WATCH 命令。这个命令一旦运行,他会确保只有在 WATCH 监视的键在调用 EXEC 之前没有改变时,后续的事务才会执行。
WATCH counter
GET counter
MULTI
SET counter <从 GET 获得的值 + 任何增量>
EXEC
EXEC:使用 EXEC 命令执行事务。如果自从事务开始以来监视的键被修改过,EXEC 将返回 nil,这表示事务中的命令没有被执行。
通过这种方式,Redis 保证了只有在监视的数据自事务开始以来没有改变的情况下,事务才会执行,从而实现了乐观锁定。
28. Redisson解锁失败,watchdog会不会一直续期下去?
不会的,因为在解锁过程中,不管是解锁失败了,还是解锁时抛了异常,都还是会把本地的续期任务停止,避免下次续期。
29. Redis中的setnx和setex有啥区别?
SETNX ,SET if Not eXists , 只有键不存在时才设置值,不能设置过期时间
SETEX , SET with EXpiration, 设置值并指定过期时间,无条件进行设置,并带有过期时间
30. Redis 与 Memcached 有什么区别?
Redis 和 Memcached 都是常见的缓存服务器,它们的主要区别包括以下几个方面:
数据结构不同
:Redis 提供了多种数据结构,如字符串、哈希表、列表、集合、有序集合等,而 Memcached 只支持简单的键值对存储。持久化方式不同
:Redis 支持多种持久化方式,如 RDB 和 AOF,可以将数据持久化到磁盘上;而 Memcached 不支持持久化。处理数据的方式不同
:Redis 使用单线程处理数据请求,支持事务、Lua 脚本等高级功能;而 Memcached 使用多线程处理数据请求,只支持基本的 GET、SET 操作。内存管理方式不同
:Redis 的内存管理比 Memcached 更加复杂,支持更多的内存优化策略。
Redis 和 Memcached 有着不同的设计理念和应用场景。Redis 适用于数据结构复杂、需要高级功能和数据持久化的场景;而 Memcached 则适用于简单的键值存储场景。
31. Redis实现分布锁的时候,哪些问题需要考虑?
锁的互斥性
对于锁的互斥性,可以借助setnx来保证,因为这个操作本身就是一个原子性操作,并且结合Redis的单线程的机制,就可以保证互斥性。
锁的可重入性
至于可重入性,其实就是说一个线程,在锁没有释放的情况下,他是可以反复的拿到同一把锁的。并且需要在锁中记录加锁次数,用来保证重入几次就需要解锁几次。用setnx也是可以实现的。
如果我们直接使用Redisson的话,他是支持可重入锁的实现的。可以直接用。
锁的性能
因为Redis是基于内存的,所以他的性能也是很高的。
误解锁问题
要确保只有锁的持有者能释放锁,避免其他客户端误解锁。
锁的有效时间
为了避免死锁,我们一般会给一个分布式锁设置一个超时时间,如上面我们用的setnx的方案,其实就是设置了一个超时时间的。
但是有的是,代码如果执行的比较慢的话,比如设置的超时时间是3秒,但是代码执行了5秒,那么就会导致在第三秒的时候,key超时了就自动解锁了,那么其他的线程就可以拿到锁了,这时候就会发生并发的问题了。
可以像redisson一样,实现一个watch dog的机制,给锁自动做续期,让锁不会提前释放。
32. 如何用setnx实现一个可重入锁?
可重入锁是一种多线程同步机制,允许同一线程多次获取同一个锁而不会导致死锁。
加锁的逻辑:
- 当线程尝试获取锁时,它首先检查锁是否已经存在。
- 如果锁不存在(即 SETNX 返回成功),线程设置锁,存储自己的标识符和计数器(初始化为1)。
- 如果锁已存在,线程检查锁中的标识符是否与自己的相同。
- 如果是,线程已经持有锁,只需增加计数器的值。
- 如果不是,获取锁失败,因为锁已被其他线程持有。
解锁的逻辑:
- 当线程释放锁时,它会减少计数器的值。
- 如果计数器降至0,这意味着线程已完成对锁的所有获取请求,可以完全释放锁。
- 如果计数器大于0,锁仍被视为被该线程持有。
33. Redis 支持哪几种数据类型?
Redis 中支持了多种数据类型,其中比较常用的有五种:
- 字符串(String)
- 哈希(Hash)
- 列表(List)
- 集合(Set)
- 有序集合(Sorted Set)也称为ZSet
另外,Redis中还支持一些高级的数据类型,如:Streams、Bitmap、Geospatial以及HyperLogLog
34. Redis中跳表的实现原理?(实现ZSet的主要数据结构)
跳表主要是通过多层链表来实现,底层链表保存所有元素,而每一层链表都是下一层的子集。
- 插入时,首先从最高层开始查找插入位置,然后随机决定新节点的层数,最后在相应的层中插入节点并更新指针。
- 删除时,同样从最高层开始查找要删除的节点,并在各层中更新指针,以保持跳表的结构。
- 查找时,从最高层开始,逐层向下,直到找到目标元素或确定元素不存在。查找效率高,时间复杂度为O(logn)
35. Redis为什么要自己定义SDS?
Redis自己本身是通过C语言实现的,但是他并没有直接使用C语言中的字符数组的方式来实现字符串,而是自己实现了一个SDS(Simple Dynamic Strings),即简单动态字符串,这是为什么呢?
C语言字符串的问题:
- 在C语言中,当识别到字符数组中的\0字符的时候,就认为字符串结束了,这样实现的字符串中就不能保存任意内容了。
- C中的字符串以\0作为识别字符串结束的方式,所以他的字符串长度判断、字符串追加等操作,都需要从头开始遍历,一直遍历到\0的时候再返回长度或者做追加。这就使得字符串相关的操作效率都很低。
解决办法:
-
在这个字符串中增加一个表示分配给该字符数组的总长度的alloc字段,和一个表示字符串现有长度的len字段。这样在获取长度的时候就不依赖\0了,直接返回len的值就行了。
-
在做追加操作的时候,只需要判断新追加的部分的len加上已有的len是否大于alloc,如果超过就重新再申请新空间,如果没超过,就直接进行追加就行了。
36. Redis除了做缓存,Redis还能用来干什么?
消息队列(不建议)
:Redis 支持发布/订阅模式和Stream,可以作为轻量级消息队列使用,用于异步处理任务或处理高并发请求。延迟消息(不建议)
:Redis的ZSET可以用来实现延迟消息,也可以基于Key的过期消息实现延迟消息,还可以借助Redisson的RDelayQueue来实现延迟消息,都是可以的。排行榜(建议)
:利用Redis 的有序集合和列表结构,可以成为设计实时排行榜的绝佳选择,例如各类热门排行榜、热门商品列表等。计数器(建议)
:基于Redis可以实现一些计数器的功能,比如网站的访问量、朋友圈点赞等。通过 incr 命令就能实现原子性的自增操作,从而实现一个全局计数器。·分布式ID(可以)
:因为他有全局自增计数的功能,所以在分布式场景,我们也可以利用Redis来实现一个分布式ID来保障全局的唯一且自增。分布式锁(建议)
:Redis 的单线程特性可以保证多个客户端之间对同一把锁的操作是原子性的,可以轻松实现分布式锁,用于控制多个进程对共享资源的访问。地理位置应用(建议)
:Redis 支持GEO,支持地理位置定位和查询,可以存储地理位置信息并通过 Redis 的查询功能获取附近的位置信息。比如"附近的人"用它来实现就非常方便。分布式限流(可以)
:Redis提供了令牌桶和漏桶算法的实现,可以用于实现分布式限流。分布式Session(建议)
:可以使用Redis实现分布式Session管理,保证多台服务器之间用户的会话状态同步。布隆过滤器(建议)
:Redis提供了布隆过滤器(Bloom Filter)数据结构的实现,可以高效地检测一个元素是否存在于一个集合中状态统计(数据量大建议用)
:Redis中支持BitMap这种数据结构,它不仅查询和存储高效,更能节省很多空间,所以我们可以借助他做状态统计,比如记录亿级用户的登录状态,或者是拿他来做签到统计也比较常见。共同关注(建议)
:Redis中支持Set集合类型,这个类型非常适合我们做一些取并集、交集、差集等,基于这个特性,我们就能取交集的方式非常方便的实现共同好友、或者共同关注的功能。推荐关注(可以)
:和上面的共同关注类似,交集实现共同好友,那么并集或者差集就能实现推荐关注的功能。
37. Redis如何实现延迟消息?
Redis的zset实现延迟消息
我们可以借助Redis中的有序集合——zset来实现这个功能。
zset是一个有序集合,每一个元素(member)都关联了一个 score,可以通过 score 排序来取集合中的值。
我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为 score 和 member。这样redis会对zset按照score延时时间进行排序。然后我们再开启redis扫描任务,获取”当前时间 > score”的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。
使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是在高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用加分布式锁解决,但是这样做也会降低吞吐型。
Redission实现延迟消息
Redission中定义了分布式延迟队列RDelayedQueue,这是一种基于zset结构实现的延时队列,它允许以指定的延迟时长将元素放到目标队列中。
调用 RDelayedQueue.offer(message, delay, TimeUnit.SECONDS)
,该消息被序列化并存入一个 ZSet,score = 当前时间 + delay,Redisson 的后台线程会不断扫描 ZSet,找到 score <= 当前时间 的消息,然后将它们投递到消费者队列,消费者通过 RQueue.take()
获取到期消息。
例子:
// 1. 定义一个真正的消费者队列 RQueue
RQueue<String> queue = redisson.getQueue("myQueue");// 2. 获取一个延迟队列 RDelayedQueue,并绑定到 RQueue
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(queue);// 3. 向延迟队列投递消息
delayedQueue.offer("task1", 5, TimeUnit.SECONDS);
执行过程:
-
task1 会先存入 ZSet(score = 当前时间 + 5 秒)。
-
当 5 秒后,Redisson 的后台线程会把 task1 移动到 myQueue(RQueue)。
消费者可以:
String msg = queue.take(); // 阻塞等待获取消息