Redis(Remote Dictionary Server)是一个基于 C 语言开发的开源 NoSQL 数据库(BSD 许可)。与传统数据库不同的是,Redis 的数据是保存在内存中的(内存数据库,支持持久化),因此读写速度非常快,被广泛应用于分布式缓存方向。并且,Redis 存储的是 KV 键值对数据。
为了满足不同的业务场景,Redis 内置了多种数据类型实现(比如 String、Hash、Sorted Set、Bitmap、HyperLogLog、GEO)。并且,Redis 还支持事务、持久化、Lua 脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。
Redis中常见的基本数据类型有:String、List、Set、ZSet, Hash。
Redis中的String不同java中的String,Java中的String是final类型的,不可以更改。而Redis中的String,是可以更改的字符串,内部结构类似Java中的ArrayList,是预先分配数组的大小(内存中以字节数组的形式存在的),当容量不够的时候,再进行扩容。
扩容机制是:当字符串的大小小于1M时,每次扩容都是容量加倍。当大小达到1兆时,每次扩容都是增加1M。容量最大不超过512M。)
C 语言里面的字符串标准形式是以 NULL 作为结束符,但是在 Redis 里面字符串不是这么表示的。因为要获取 NULL 结尾的字符串的长度使用的是 strlen 标准库函数,这个函数的算法复杂度是 O(n),它需要对字节数组进行遍历扫描,作为单线程的 Redis 表示承受不起。Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
struct SDS<T> {
T capacity; //容量大小
T len; //数组长度
byte flags; //标志位
byte[] content; //具体内容
}
Redis中的list结构,相当于Java中的LinkedList,是一个双向链表数据结构。支持前后顺序遍历,链表结构的插入和删除操作较快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。
基本操作指令有:
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> lpush names tom
(integer) 1
127.0.0.1:6379> lpush names jerry
(integer) 2
127.0.0.1:6379> lrange names 0 -1
1) "jerry"
2) "tom"
127.0.0.1:6379> rpush names Amy
(integer) 3
127.0.0.1:6379> lrange names 0 -1
1) "jerry"
2) "tom"
3) "Amy"
127.0.0.1:6379> lindex names 3
(nil)
127.0.0.1:6379> lindex names 2
"Amy"
127.0.0.1:6379>
127.0.0.1:6379> lset names 2 Amy-to-Alice
OK
127.0.0.1:6379> lrange names 0 -1
1) "jerry"
2) "tom"
3) "Amy-to-Alice"
127.0.0.1:6379>
Redis 中的字典相当于 Java 中的 HashMap,内部实现也差不多类似,都是通过 “数组 + 链表” 的链地址法来解决部分 哈希冲突,同时这样的结构也吸收了两种不同数据结构的优点。
127.0.0.1:6379> hset books java "thinking in java"
(integer) 1
127.0.0.1:6379> hset books python "python in action"
(integer) 1
127.0.0.1:6379> type books
hash
127.0.0.1:6379> hgetall books
1) "java"
2) "thinking in java"
3) "python"
4) "python in action"
127.0.0.1:6379> hget books java
"thinking in java"
127.0.0.1:6379> hlen books
(integer) 2
127.0.0.1:6379>
扩展和收缩的条件是什么呢?
正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是原数组大小的 2 倍。不过如果 Redis 正在做 bgsave(持久化命令),为了减少内存过多分离,Redis 尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。
Redis 中的 hash 存储的value只能是字符串值,此外扩容与 Java 中的 HashMap 也不同。Java 中的HashMap在扩容的时候是一次性完成的,而 Redis 考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式 rehash 策略。
渐进式 rehash 指的是并非一次性完成,它是多次完成的,渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,所以 Redis 中的 hash(字典) 会存在新旧两个 hash 结构,在 rehash 结束后也就是旧 hash 的值全部搬迁到新 hash 之后,就会使用新的 hash 结构取而代之。
Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL,集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。
127.0.0.1:6379> sadd language Chinese
(integer) 1
127.0.0.1:6379> sadd language English
(integer) 1
127.0.0.1:6379> smembers language
1) "English"
2) "Chinese"
127.0.0.1:6379> scard language
(integer) 2
127.0.0.1:6379> spop language 1
1) "Chinese"
127.0.0.1:6379> smembers language
1) "English"
127.0.0.1:6379>
这可能使 Redis 最具特色的一个数据结构了,它类似于 Java 中 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以为每个 value 赋予一个 score 值,用来代表排序的权重。
zset底层实现使用了两个数据结构,第一个是hash,第二个是跳跃列表,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。跳跃列表的目的在于给元素value排序,根据score的范围获取元素列表。
127.0.0.1:6379> zadd language 4 English
(integer) 1
127.0.0.1:6379> zadd language 3 Japense
(integer) 1
127.0.0.1:6379> zrem language Japense
(integer) 1
127.0.0.1:6379> zadd language 3 Japanese
(integer) 1
127.0.0.1:6379> zrange language 0 -1 withscores
1) "Japanese"
2) "3"
3) "English"
4) "4"
5) "Chinese"
6) "5"
127.0.0.1:6379>
Redis 内部做了非常多的性能优化,比较重要的有下面 3 点:
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
Redis 提供了两个命令来生成 RDB 快照文件:
与快照持久化相比,AOF 持久化的实时性更好。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化(Redis 6.0 之后已经默认是开启了),可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
AOF 持久化功能的实现可以简单分为 5 步:
Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler)。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。
Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。
虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 redis.conf:
io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
另外:
开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 redis.conf :
io-threads-do-reads yes
但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启。
如果假设你设置了一批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进行删除的呢?
常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):
4.0 版本后增加以下两种:
对于过期 key,Redis 采用的是 定期删除+惰性/懒汉式删除 策略。定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。如何解决呢?
下面是两种常见的方法:
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:
bigkey 通常是由于下面这些原因产生的:
bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。
缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。
另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。
常见的缓存预热方式有两种:
Redis Sentinel是一个高可用性解决方案,用于监控和管理Redis集群。Redis是一个流行的开源内存数据库,但是在单个Redis实例故障或宕机时,可能会导致应用程序中断或数据丢失。Redis Sentinel通过提供自动故障检测、故障转移和集群管理等功能,帮助确保Redis集群的高可用性和稳定性。
Redis Sentinel的主要功能包括:
通过使用Redis Sentinel,可以提高Redis集群的可用性和容错性,减少因为单点故障而导致的系统中断。它还简化了Redis集群的管理和维护工作,提供了一种可靠的方式来监控和自动处理Redis实例的故障。
Redis Sentinel使用以下机制来检测节点是否下线:
通过这些机制,Redis Sentinel能够检测节点的健康状态并判断其是否下线。这样可以及时发现故障节点,并采取相应的故障转移措施,确保Redis集群的高可用性。
主观下线和客观下线的区别在于判断节点是否下线的依据和范围。主观下线是单个Sentinel节点的主观判断,而客观下线是整个Redis Sentinel集群的共识。只有当一个节点被足够多的Sentinel节点标记为主观下线时,它才会被认定为客观下线。这种机制可以减少误判,增加对节点故障的可靠性判断。
Redis Sentinel通过以下步骤实现故障转移:
通过这个故障转移过程,Redis Sentinel可以保证在主节点故障时自动选择新的主节点,并使Redis集群继续对外提供服务。这种故障转移机制可以提高Redis集群的可用性和容错性,减少因为主节点故障而导致的系统中断。
建议部署多个Sentinel节点(哨兵集群)的主要原因是提高Redis集群的高可用性和容错性。以下是几个关键的原因:
综上所述,部署多个Sentinel节点能够增加Redis集群的鲁棒性和可用性,提供故障检测和自动故障转移的功能,并确保在主节点故障时能够快速选举出新的主节点,保持数据一致性和服务的连续性。
在Redis Sentinel的故障转移过程中,选择新的主节点的策略是基于以下因素进行评估和决策:
总体而言,Redis Sentinel会综合考虑从节点的优先级、复制偏移量和健康状态等因素,选择出一个合适的从节点作为新的主节点。这个选择过程旨在保证新的主节点具有数据的一致性和可靠性,并尽可能地提高整个Redis集群的可用性。
Redis Sentinel在一定程度上可以帮助防止脑裂(Split-Brain)问题的发生,但并不能完全消除脑裂的可能性。脑裂是指在分布式系统中,由于网络分区或其他原因,导致集群中的节点无法正常通信,进而导致数据一致性和可用性的问题。
Redis Sentinel采用了共识机制和多数投票的方式来进行故障检测和故障转移的决策,这有助于减少脑裂的影响。当主节点发生故障时,Sentinel节点会进行共识确认,并选举出一个新的主节点。这个过程要求至少有一半加一(Quorum)的Sentinel节点都达成共识,确保选举结果的一致性。
然而,脑裂问题可能在以下情况下发生:
为了减少脑裂的风险,可以考虑以下策略:
需要注意的是,脑裂是分布式系统中常见的问题,对于高可用性和数据一致性的要求较高的应用场景,可能需要考虑使用更复杂的解决方案,如分布式一致性协议(如Paxos、Raft)或使用专门的分布式数据库系统来处理脑裂问题。
Redis Cluster是Redis提供的分布式解决方案,用于解决单节点Redis的性能和容量限制,并提供高可用性和数据冗余。它解决了以下问题并带来了一些优势:
总的来说,Redis Cluster解决了单节点Redis的性能和容量限制,提供了高可用性、扩展性和自动化管理的优势。它适用于需要处理大规模数据和高并发请求的应用场景,如缓存、会话存储和实时数据处理等
Redis Cluster使用哈希槽(Hash Slot)的方式进行数据分片。哈希槽是一个固定数量的槽位集合,通常为16384个槽位(0-16383)。每个槽位可以被分配给集群中的一个或多个节点。
数据分片的过程如下:
通过使用哈希槽进行数据分片,Redis Cluster可以实现数据在集群中的均衡存储和路由。每个节点只负责管理一部分哈希槽,从而避免了单节点Redis的性能和容量限制。同时,当集群的节点数量发生变化时,Redis Cluster可以自动进行哈希槽的重新分配,实现数据的动态迁移和负载均衡。这样可以有效地提高系统的扩展性和性能。
Redis Cluster选择16384个哈希槽的数量是出于权衡和设计考虑。
以下是一些理由:
总的来说,选择16384个哈希槽数量是为了提供均衡性、可扩展性和简化路由的优势,同时与之前的Redis版本保持兼容性。这个数量经过实践和经验的验证,被认为是一个合理的折衷选择。
确定给定键(key)应该分布到哪个哈希槽中,可以通过以下步骤进行:
例如,假设有一个键"mykey"需要分布到Redis Cluster的哈希槽中:
在Redis Cluster中,客户端会根据键的哈希值自动进行哈希槽的计算和路由。客户端可以通过使用Redis客户端库或者自定义的哈希函数来实现这个过程。对于大多数应用来说,这个过程是透明的,由Redis Cluster的客户端库来处理。客户端只需要指定键,而不需要关心具体的哈希槽计算过程。
是的,Redis Cluster支持重新分配哈希槽。当集群的节点数量发生变化,例如节点的加入或离开,Redis Cluster可以自动进行哈希槽的重新分配,以实现数据的动态迁移和负载均衡。
哈希槽的重新分配过程如下:
这种自动的哈希槽重新分配机制使得Redis Cluster能够适应节点的动态变化,实现数据的平衡分布和负载均衡。同时,这个过程对应用程序是透明的,应用程序无需手动干预,Redis Cluster会自动处理数据迁移和哈希槽的重新分配。
在Redis Cluster的扩容和缩容过程中,集群仍然可以提供服务,但可能会有一些短暂的影响和潜在的性能变化。
在扩容期间,当新节点加入集群并接管一部分哈希槽时,数据迁移过程会发生。在数据迁移期间,如果客户端发送命令到正在迁移的哈希槽上,集群会自动将请求重定向到正确的节点。这意味着客户端可能会在迁移过程中经历一些请求的重定向和稍微增加的延迟,但整体上仍然可以继续提供服务。
在缩容期间,当节点离开集群时,集群会将相应的哈希槽重新分配给其他节点,并进行数据迁移。在这个过程中,客户端的请求也会被重定向到正确的节点上。与扩容相似,客户端可能会经历一些请求重定向和轻微的延迟。
需要注意的是,数据迁移过程可能会对集群的整体性能产生一些影响。数据迁移可能会消耗网络带宽和节点资源,因此在迁移期间可能会出现一些性能波动。但一旦数据迁移完成,集群的性能应该恢复到正常水平,并且能够继续提供服务。
为了最小化影响,可以采取一些策略,如逐步扩容或缩容、控制迁移速率等。此外,合理的集群规划和节点配置也可以提高Redis Cluster在扩容和缩容期间的稳定性和性能表现。
总的来说,Redis Cluster在扩容和缩容期间可以继续提供服务,但可能会有一些短暂的影响和性能变化。这些影响通常是暂时的,一旦数据迁移完成,集群应该恢复到正常状态。
在Redis Cluster中,节点之间通过节点间通信(Node-to-Node Communication)来进行协调和数据同步。节点间通信是基于Redis自定义的二进制协议实现的。
以下是节点间通信的基本原理:
通过以上机制,Redis Cluster中的节点能够进行通信、协作和数据同步,以实现集群的高可用性和数据一致性。这些机制使得Redis Cluster能够自动处理节点间的协调和故障恢复,从而提供稳定可靠的服务。