Redis(Remote Dictionary Server)是一个开源的、使用内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种类型的数据结构,如字符串(String)、哈希表(Hash)、列表(List)、集合(Set)、有序集合(Zset)和三种特殊类型:地理位置(Geo)、基数统计(HyperLogLog)、位图(Bitmaps)。由于其出色的性能,Redis 常被用作快速访问数据的缓存和分布式锁等。
1. Redis为什么这么快
内存存储:Redis是一个基于内存的数据库,它将所有数据存储在内存中,因此它可以直接在内存中进行数据的读取和写入操作,这比传统数据库需要从磁盘读取数据要快得多。内存中的读写速度通常比磁盘快几个数量级,这使得Redis能够提供极高的响应速度。
单线程模型:Redis采用单线程模型来处理请求。尽管这听起来可能会限制Redis的并发处理能力,但实际上,由于Redis的操作主要是内存操作,且其数据结构相对简单,因此单线程模型反而能够避免多线程或多进程之间的上下文切换和锁竞争,从而减少了CPU的消耗,提高了处理速度。
高效的数据结构:Redis支持多种类型的数据结构,如字符串、列表、哈希表、集合和有序集合等,这些数据结构都经过了优化,以提供快速的数据访问和操作。例如,Redis的哈希表(跳跃表)实现具有很低的冲突率和高效的查找、插入和删除操作。
非阻塞I/O:Redis使用非阻塞I/O模型来处理网络请求。这意味着Redis可以在等待I/O操作完成时继续处理其他任务,而不是阻塞当前线程。这种机制使得Redis能够充分利用CPU资源,提高吞吐量。
多路I/O复用:Redis采用多路I/O复用技术(如epoll)来同时处理多个网络连接。多路I/O复用允许单个线程监听多个网络连接上的I/O事件,并在事件发生时进行处理。这种技术减少了线程切换的开销,并提高了网络请求的处理效率。
高效的持久化机制:虽然Redis主要将数据存储在内存中,但它也提供了多种持久化机制(如RDB和AOF)来确保数据的可靠性和持久性。这些持久化机制在不影响Redis性能的前提下,将数据定期保存到磁盘上,以防止数据丢失。
采用C语言编写:C 语言是一种高效且接近硬件的编程语言,非常适合用于开发高性能的系统软件,如数据库。
2. Redis数据结构
2.1. 字符串(String)
字符串是 Redis 中最基本的数据结构,用于存储任意类型的数据,如数字、文本、序列化的对象等。支持动态扩展,最大可以存储 512MB 的数据。常用作缓存、计数器、限流、分布式锁和会话管理等场景。
Redis字符串的内部有三种编码方式:
int编码:当字符串长度小于等于12字节并且字符串可以表示为整数时,Redis会使用int编码。这样可以节省内存,并且在执行一些命令时可以直接进行数值计算。
embstr编码:当字符串长度小于等于39字节时,Redis会使用embstr编码。这种编码方式会将字符串和存储它的结构体一起分配在内存中,这样可以减少内存碎片和结构体的开销。
raw编码:当字符串长度大于39字节或者字符串不能表示为整数时,Redis会使用raw编码。这种编码方式直接将字符串存储在一个结构体中,没有进行任何优化。
提供的方法和使用的例子:
操作命令:
例子:
# 基本操作: SET 和 GET
SET mykey "Hello Redis"
GET mykey
# 批量操作: MSET 和 MGET
MSET key1 value1 key2 value2 key3 value3
MGET key1 key2 key3
# 数值操作: INCR 和 DECR 递增和递减(+1 or -1)key不存在的话设置成0再+-1
INCR mykey
DECR mykey
# 数值操作: INCRBY 和 DECRBY 递增和递减(+n or -n)
NCRBY mykey 5
DECRBY mykey 3
# 字符串操作: APPEND追加、STRLEN获取value长度、GETRANGE获取字符串子串长度、SETRANGE设置字符串子串
APPEND mykey " World"
STRLEN mykey
GETRANGE mykey 2 6
SETRANGE mykey 2 "World"
# 符合操作: SETEX设置key过期时间、SETNX不存在时设置kv、GETSET设置kv并返回原有的值
SETEX key seconds value
SETNX key value
GETSET key value
2.2. 哈希表(Hash)
哈希是键值对的集合,其中键值对的值可以是字符串、列表或其他哈希类型。常用存储对象和复杂的机构化数据(缓存)。
Redis哈希表的内部有两种编码方式:
ziplist(压缩列表):当哈希中的元素数量较少且元素较小时,Redis 使用压缩列表
ziplist
作为底层实现来节省内存(关于ziplist的详情信息请看我的另一篇文章《理解Redis的ziplist》)。hashtable(字典):当哈希中的元素数量较多或元素较大时,Redis 使用字典
hashtable
作为底层实现。
提供的方法和使用的例子:
操作命令:
例子
# 基本操作:HSET、HGET、HGETALL获取所有字段和值、HDEL删除
HSET key field value
HGET key field
HGETALL key
HDEL key field1 [field2 ...]
# 批量操作:HMSET 和 HMGET
HMSET key field1 value1 [field2 value2 ...]
HMGET key field1 [field2 ...]
# 数值操作:HINCRBY值加上增量、HINCRBYFLOAT浮点数值加上增量
HINCRBY key field increment
HINCRBYFLOAT key field increment
# 其他操作:HEXISTS是否存在、HLEN字段的数量
HEXISTS key field
HLEN key
2.3. 列表(List)
Redis 的 List 类型是一种简单的字符串列表,按照插入顺序排序。你可以向列表的头部或尾部添加元素。List 类型经常用于实现消息队列、时间序列、排行榜、最近访问记录、栈等功能。
Redis列表的内部有两种编码方式:
ziplist(压缩列表):同hash的ziplist,这里就不重复介绍了。
linkedlist(双端列表):是一种常规的双向链表结构,它可以存储任意长度的列表,并且支持高效的插入和删除操作。在linkedlist中,每个节点都包含了一个指向前一个节点和后一个节点的指针,以及一个存储元素数据的指针。linkedlist适用于存储大数量的列表,它没有像ziplist那样的内存限制,但是会占用更多的内存空间。
下面将通过一些具体的例子来展示 Redis List 的用法。
例子:
# 添加元素:LPUSH、RPUSH、LPOP、RPOP
LPUSH mylist "hello"
RPUSH mylist "world"
# 移除元素:LPOP、RPOP
LPOP mylist
RPOP mylist
# 查看元素:LRANGE(-1 表示列表的最后一个元素(尾部))
LRANGE mylist 0 -1
# 获取列表长度:LLEN
LLEN mylist
# 阻塞式操作:BLPOP 和 BRPOP 是 LPOP 和 RPOP 的阻塞版本,0 表示无限等待,直到有元素可以弹出。
BLPOP mylist 0
2.4. 集合(Set)
Redis 中的集合(Set)是一种无序的、不包含重复元素的字符串集合。它主要用于存储不需要排序的、唯一的元素集合。Redis 集合的操作非常丰富,包括添加、删除、查询元素,以及集合间的运算等。Set类型经常用户实现标签系统、好友关系、订阅关系等系场景。
Redis集合的内部有两种编码方式:
intset(整数集合):当Set类型只包含整数类型的数据,并且元素数量较少(小于512个)时,Redis会使用intset作为Set类型的内部编码。intset是一种紧凑的、压缩的整数集合结构,可以节省内存空间,并且支持快速的查找、插入和删除操作。在intset中,所有元素都按照从小到大的顺序排列,并且可以使用不同的编码方式(16位、32位、64位)存储不同大小范围内的整数。
hashtable(字典):当Set类型包含字符串类型或者元素数量较多时,Redis会使用hashtable作为Set类型的内部编码。hashtable是一种基于链表的哈希表结构,可以快速地进行随机访问、插入和删除操作。在hashtable中,每个元素都被存储为一个字符串,并且使用哈希函数将字符串映射到一个桶中,然后在桶中进行查找、插入和删除操作。
以下是 Redis 集合的一些详细用法举例:
例子:
# 基本操作:SADD添加如果key已经存在则忽略
SADD myset "Hello"
# 获取集合中所有元素,SMEMBERS
SMEMBERS myset
# 判断元素是否在集合中 SISMEMBER,元素存在,返回 1;如果不存在,返回 0。
SISMEMBER myset "Hello"
# 获取集合中元素的个数 SCARD
SCARD myset
# 删除集合中的元素 SREM
SREM myset "World"
# 随机删除并返回集合中的元素
SPOP myset
SPOP myset 2 # 或者删除多个元素
# 随机获取集合中的元素
SRANDMEMBER myset
SRANDMEMBER myset 2 # 或者随机获取多个元素
# 交集运算 SINTER
SINTER set1 set2
# 并集运算 SUNION
SUNION set1 set2
# 差集运算 SDIFF
SDIFF set1 set2
# 存储集合运算的结果:可以将集合运算的结果存储到新的集合中,使用 SINTERSTORE、SUNIONSTORE 和 SDIFFSTORE 命令
SINTERSTORE newSet set1 set2
SUNIONSTORE newSet set1 set2
SDIFFSTORE newSet set1 set2
# 移动集合中的元素 SMOVE
SMOVE source destination member
2.5. 有序集合(Zset)
Redis 有序集合(Sorted Set)是一种特殊的数据结构,它不仅保持了集合(Set)的特性——元素不重复,还额外为集合中的每个元素关联了一个浮点数分数(score)。这使得有序集合能够按照分数进行排序,从而实现更复杂的操作。ZSet类型经常用户实现排行榜、好友关系、订阅关系等系场景。
Redis有序集合的内部有两种编码方式:
ziplist(压缩列表):同hash的ziplist,这里就不重复介绍了。
skiplist(跳跃表):当Zset中元素个数大于等于128个,或者有一个元素的长度大于64字节时,Redis会使用skiplist编码存储Zset。这种编码方式支持高效的随机访问和范围查询,但是需要占用更多的内存空间。
以下是 Redis 有序集合的一些详细用法举例:
例子:
# 添加元素 ZADD ,需要指定分数
ZADD myzset 2 "two" 3 "three"
# 获取有序集合中的所有元素 ZRANGE,WITHSCORES按分数从低到高排序
ZRANGE myzset 0 -1 WITHSCORES
# 获取指定分数范围的元素 ZRANGEBYSCORE
ZRANGEBYSCORE myzset 1 2 WITHSCORES # 分数在 1 到 2(包括 1 和 2)之间的所有元素
# 删除元素 ZREM
ZREM myzset "one"
# 更新元素的分数
ZINCRBY myzset 2 "two"
# 获取元素的排名 ZRANK
ZRANK myzset "two"
# 获取元素的分数 ZSCORE
ZSCORE myzset "two"
3. Rediskey过期策略
Redis的过期策略是Redis管理内存和数据有效性的重要机制之一。Redis主要采用了两种过期策略来删除过期的键值对:惰性删除和定期删除。以下是这两种策略的详细解释:
3.1. 惰性删除(Lazy Deletion)
原理:惰性删除是指Redis服务器不主动删除过期的键值对,而是在客户端尝试访问某个键时,Redis会先检查这个键是否已过期。如果键已经过期,则Redis会删除这个键,并且不返回任何值给客户端(通常返回nil)。
优点:这种策略可以节省CPU资源,因为只有在键被访问时才会检查其是否过期。
缺点:如果一些过期的键长时间没有被访问,那么它们将一直占用内存空间,可能导致内存泄漏。
3.2. 定期删除(Active Deletion)
原理:定期删除是指Redis服务器会周期性地检查数据库中的键,并删除那些已过期的键。Redis默认每秒进行10次过期扫描(这个值可以通过配置文件中的
hz
参数进行调整)。扫描过程中,Redis会随机选取一定数量的键(默认为20个),检查这些键是否过期,并删除已过期的键。如果这20个键中有超过四分之一的键已经过期,Redis会重复这个过程,直到过期键的比例下降到一定水平。优点:这种策略可以确保过期的键及时被删除,避免内存浪费。
缺点:定期删除会占用一定的CPU资源,尤其是在键的过期时间比较集中时,可能会引发性能问题。
在实际应用中,Redis通常将惰性删除和定期删除结合使用,以达到内存使用效率和性能的平衡。当Redis内存使用接近上限时,如果还有大量的过期键未被删除,Redis会采用内存淘汰策略来释放空间。
4. Redis内存淘汰机制
Redis提供了多种内存淘汰策略,用于在内存不足时淘汰键值对。这些策略包括:
noeviction:默认策略,不淘汰任何数据,当内存不足时,对于写请求将返回错误信息(但对于DEL请求和部分特殊请求除外)。
volatile-lru:淘汰设置了过期时间的最少使用的键(LRU,最近最少使用)。
volatile-ttl:淘汰设置了过期时间的键,且淘汰的键的剩余生存时间(TTL)最短。
volatile-random:随机淘汰设置了过期时间的键。
allkeys-lru:淘汰所有键中最少使用的键(LRU)。
allkeys-random:随机淘汰所有键。
volatile-lfu(Redis 4.0及以后版本):淘汰设置了过期时间且使用频率最低的键(LFU,最少频率使用)。
allkeys-lfu(Redis 4.0及以后版本):淘汰所有键中使用频率最低的键(LFU)。
这些策略允许用户根据应用的需求和数据的特性来选择合适的淘汰策略,以优化Redis的性能和内存使用效率。
5. Redis的持久化方式
Redis的持久化方式主要有两种:RDB(Redis Database Backup)和AOF(Append-Only File),以及从Redis 4.0版本开始支持的混合持久化方式。下面将分别介绍这三种持久化方式。
5.1. RDB持久化
定义与原理:
RDB是Redis创建的数据库快照,它可以将数据集快照以二进制文件的形式存储在磁盘上。
RDB持久化会根据配置的规则定时将内存中的数据持久化到硬盘上。
特点与优势:
性能:生成RDB文件的过程不会影响Redis的性能,因为Redis会fork出一个子进程来完成快照的创建,主进程依然处理客户端请求。
恢复速度:RDB文件非常适合大型数据集的备份和数据恢复,加载RDB文件时速度非常快。
配置灵活:通过配置save选项,可以在一定的写命令数量或时间间隔后自动生成RDB文件。
触发机制:
手动触发:可以使用
SAVE
命令(同步方式,会阻塞Redis)或BGSAVE
命令(异步方式,不阻塞Redis)来手动触发RDB持久化。自动触发:通过配置文件中的save指令来设置自动触发条件,如“save 900 1”表示900秒内如果有1个键发生改变,则自动触发RDB持久化。
配置示例:
# 在redis.conf文件中配置RDB选项
save 900 1 # 900秒内至少有1个键被修改,则触发RDB持久化
save 300 10 # 300秒内至少有10个键被修改,则触发RDB持久化
save 60 10000 # 60秒内至少有10000个键被修改,则触发RDB持久化
dbfilename dump.rdb # 指定RDB文件名
dir /var/lib/redis # 指定RDB文件存放路径
5.2. AOF持久化
定义与原理:
AOF持久化是通过将每个写操作都记录在一个日志文件中来实现的。
当Redis重启时,通过重新执行AOF日志文件中的写命令来在内存中重建整个数据库的内容。
特点与优势:
实时性:AOF以追加方式写入文件,数据恢复的即时性非常高。
数据安全:可以通过配置fsync策略保证数据的持久化频率,如每秒fsync一次。
文件管理:AOF文件随着写操作的增加而不断增大,但Redis提供了AOF重写机制来压缩文件大小。
配置示例:
# 在redis.conf文件中配置AOF选项
appendonly yes # 开启AOF持久化
appendfilename "appendonly.aof" # AOF文件名
appendfsync everysec # 每秒fsync一次,折中方案
# AOF重写配置
auto-aof-rewrite-percentage 100 # AOF文件大小增长了上一次重写时大小的百分比
auto-aof-rewrite-min-size 64mb # AOF文件最小大小
5.3. 混合持久化(Redis 4.0及以上版本)
定义与原理:
混合持久化是RDB和AOF的结合体,在AOF重写时,将新的AOF文件开头保存为一个RDB格式的二进制快照,后面再追加增量的AOF日志指令。
特点与优势:
加载速度:结合了RDB文件的重启加载速度快的特点。
数据实时性:保留了AOF日志文件的实时性。
配置:
aof-use-rdb-preamble yes # 开启AOF文件混合模式
Redis的持久化方式各有特点,用户可以根据具体的应用场景和数据重要性来选择适合的持久化策略。例如,对于需要快速恢复且对数据实时性要求不高的场景,可以选择RDB持久化;对于需要高实时性和数据安全性的场景,可以选择AOF持久化;而混合持久化方式则结合了RDB和AOF的优点,适用于对数据恢复速度和实时性都有较高要求的场景。
6. Redis集群部署方式
6.1. 主从复制(Master-Slave Replication)
主从复制是最简单的集群部署方式,由一个主节点(Master)和多个从节点(Slave)组成。在这种模式下,所有的写操作都在主节点上进行,然后主节点将数据同步到从节点,从节点主要进行读操作。主从复制的主要特点包括:
读写分离:主节点负责写操作,从节点负责读操作,有助于提升系统的读性能。
数据一致性:通过复制机制保持主从节点之间的数据一致性。
容错性:当主节点出现故障时,从节点可以作为备份继续提供服务,但不会自动升级为主节点。
6.2. 哨兵(Sentinel)模式
哨兵模式在主从复制的基础上增加了一个或多个哨兵节点(Sentinel),用于监控Redis主从节点的运行状态。哨兵的主要功能包括:
监控:哨兵节点会周期性地向Redis节点发送PING命令,以检测它们是否在线。
自动故障转移:当主节点出现故障时,哨兵节点会自动将从节点中的一个提升为主节点,并进行相应的配置更新,以确保服务的连续性。
通知:哨兵节点会将主节点的变化通知给客户端和其他从节点。
6.3. 集群(Cluster)模式
Redis Cluster是Redis的官方分布式解决方案,它提供了无中心化的集群配置,将数据分布在多个节点上。Redis Cluster的主要特点包括:
无中心化:集群中的每个节点都是平等的,没有中心节点。
数据分片:集群将数据分成多个槽(slot),每个槽负责存储一定范围内的数据。
高可用性和容错性:每个节点都有一个或多个从节点,当主节点出现故障时,从节点可以自动升级为主节点。
重定向客户端:当客户端访问的数据不在当前节点上时,节点会将请求重定向到正确的节点。
6.4. 部署建议
根据需求选择部署方式:对于需要高可用性和容错性的应用,建议使用哨兵模式或集群模式;对于读多写少的应用,主从复制模式可能是一个更好的选择。
注意节点数量和配置:根据集群的规模和性能要求,合理配置节点数量和资源。
定期维护和监控:定期检查集群的运行状态和性能指标,及时发现并解决问题。
综上所述,Redis提供了多种集群部署方式以满足不同的应用需求。在选择部署方式时,需要根据实际的应用场景和需求进行综合考虑。
7. 发布/订阅模式
Redis 的发布和订阅(Pub/Sub)是一种消息通信模式,在这种模式下,消息的发送者(发布者)不会直接将消息发送给特定的接收者(订阅者)。相反,它发布消息到某个频道(channel),而任何订阅了该频道的订阅者都可以接收到该消息。这个模式支持多对多的通信,即多个发布者可以发布消息到同一频道,同时多个订阅者也可以订阅同一频道。
7.1. 基本命令
# 发布消息 如果频道不存在,则自动创建,返回订阅了频道的客户端数量
PUBLISH <channel> <message>
# 订阅频道 订阅操作是阻塞的,即客户端会一直等待直到有消息到达
SUBSCRIBE <channel> [channel ...]
# 取消订阅 取消订阅一个或多个频道。如果没有指定频道,则取消订阅所有已订阅的频道
UNSUBSCRIBE <channel> [channel ...]
7.2. 使用场景
Redis 的发布和订阅模式非常适合于实现实时消息系统、通知系统、实时数据分析等场景。例如,一个网站可以使用 Redis 的发布和订阅功能来实时通知用户他们关注的文章或商品有更新。
7.3. 注意事项
消息丢失:如果订阅者在消息发布时未连接,则无法接收该消息。Redis 不提供消息持久化或重试机制。
资源消耗:如果订阅者过多,发布消息时可能会消耗大量资源。
消息不保证送达:Redis 的发布和订阅模式不保证消息一定能够送达订阅者。
模式匹配:使用
PSUBSCRIBE
和PUNSUBSCRIBE
可以实现基于模式的订阅,这在某些场景下非常有用,但请注意模式匹配可能会增加 Redis 服务器的负担。安全性:由于 Redis 的发布和订阅功能是开放的,因此需要注意不要暴露敏感频道或消息。
替代方案:对于需要消息持久化或更高级的消息队列功能,可以考虑使用 Redis Streams 或其他消息队列系统(如 RabbitMQ、Kafka 等)。
8. 事务支持
Redis事务是一个单独的隔离操作,其特点与关系型数据库中的事务有所不同。以下是关于Redis事务的详细解释:
8.1. Redis事务的特点
隔离性:Redis事务中的所有命令都会序列化,即按照顺序执行,且在执行过程中不会被其他客户端发送的命令请求所打断。这保证了事务的隔离性,即事务在执行时不会受到其他客户端操作的影响。
不保证原子性:与关系型数据库不同,Redis事务不保证原子性。这意味着,如果事务中的某个命令执行失败,其他命令仍然会继续执行,而不是像关系型数据库中那样进行回滚。Redis单条命令是原子性的,但事务作为一个整体并不具备原子性。
无隔离级别:Redis没有传统数据库中的隔离级别(如读未提交、读已提交、可重复读、串行化)的概念。Redis的隔离性主要体现在事务执行时不会被其他客户端的命令打断。
持久性无法保证:Redis事务的持久性取决于Redis本身的持久化配置。如果使用了RDB或AOF持久化模式,并且配置得当,那么事务的结果可能会被持久化到磁盘上。但是,如果Redis服务在事务执行后但持久化操作完成前崩溃,那么事务的结果可能会丢失。
8.2. Redis事务的操作命令
Redis事务主要涉及到以下几个命令:
MULTI:用于开启一个事务。当执行MULTI命令后,客户端进入事务状态,接下来输入的命令都会被放入事务队列中,但不会立即执行。
EXEC:用于执行事务队列中的所有命令。当执行EXEC命令后,Redis会按照顺序执行事务队列中的所有命令,并返回每个命令的执行结果。
DISCARD:用于取消事务,放弃执行事务队列中的所有命令。如果在执行EXEC命令之前调用了DISCARD命令,那么事务将被取消,事务队列中的所有命令都不会被执行。
WATCH:用于监视一个或多个key。如果在事务执行之前被WATCH的key被其他命令所改动(包括其他客户端的操作或当前客户端在事务开启后、EXEC命令执行前的操作),那么事务将被打断,EXEC命令将返回nil。
UNWATCH:用于取消WATCH命令对所有key的监视。如果在执行WATCH命令之后,EXEC命令或DISCARD命令先被执行了,那么就不需要再执行UNWATCH命令了。
8.3. Redis事务的使用场景
Redis事务主要使用在分布式系统和高并发等场景下,具体例如:
批量操作:将批量的Redis命令进行打包,可以减少Redis服务器的通信次数。
分布式锁:在分布式系统中需要实现分布式锁时,可以通过Redis事务将不同客户端的操作放在同一个事务中进行加锁和解锁的操作。
数据库相关操作:确保数据库执行操作的先后顺序,将需要执行的操作放在一个事务中让它串行化执行。
8.4. 注意事项
由于Redis事务不保证原子性,因此在设计应用时需要考虑到这一点,并采取适当的措施来处理可能出现的错误情况。
Redis事务的持久性取决于Redis的持久化配置,因此在设计应用时也需要考虑到数据的持久化需求。
在使用WATCH命令时,需要注意其监视的key在事务执行前不能被其他命令所改动,否则事务将被打断。同时,也需要注意取消WATCH命令的使用,以避免不必要的资源消耗。
8.5. 例子
MULTI
INCR key1
GET key2
EXEC
import redis
# 连接到Redis服务器
r = redis.StrictRedis(host='localhost', port=6379, db=0)
# 开启事务
pipeline = r.pipeline()
try:
# 命令入队
pipeline.multi()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.incr('counter')
# 执行事务
pipeline.execute()
print("事务执行成功")
except redis.exceptions.RedisError as e:
print(f"事务执行失败: {e}")
# 取消事务
pipeline.discard()
# 检查结果
print(r.get('key1')) # 输出: b'value1'
print(r.get('key2')) # 输出: b'value2'
print(r.get('counter')) # 输出: b'1'
评论区