当前位置 博文首页 > RtxTitanV的博客:Redis常见问题总结
Redis是一个使用C语言开发的数据库,与传统数据库不同的是Redis的数据存在内存中 ,是内存数据库,所以读写速度非常快,因此Redis被广泛应用于缓存。另外,redis也经常用来做分布式锁和消息队列。redis提供了多种数据类型来支持不同的业务场景。redis还支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
用缓存的目的主要是为了高性能和高并发:
缓存分为本地缓存和分布式缓存。以Java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持redis或memcached服务的高可用,整个程序架构上较为复杂。
分布式缓存使用的比较多的主要是Memcached和Redis,不过现在基本上都用Redis来实现缓存。
Redis并不是简单的key-value存储,实际上它是一个数据结构服务器,其中键都是字符串,值支持多种不同类型(有5种基础类型),不同类型数据结构的差异就在于值的结构不一样。
Redis有5种基本数据类型:
SET key value
:为key
设置保存一个字符串值value
。如果key
已经保存了一个值,那么该操作会直接覆盖原来的值,并且忽略原始类型。当set
命令执行成功之后,之前设置的过期时间都将失效。GET key
:获取key
对应的值。如果key不存在,返回特殊值nil
,如果key
的值不是字符串,就返回错误。STRLEN key
:返回key
所储存的字符串值的长度。当key
保存的是非字符串值时将返回错误。APPEND key value
:如果键key
已存在并且它的值是一个字符串,将把value
追加到字符串末尾。如果key
不存在,则会创建它并先将其值置为空字符串在执行追加操作。INCR key
:将key
中储存的数字值加1。DECR key
:将key
中储存的数字值减1。MSET key value [key value …]
:同时为多个键设置值。MGET key [key …]
:返回给定的一个或多个字符串键的值。LPUSH key value [value …]
:将所有指定的值插入存储在key
的列表的开头(最左边,如有多个值从左到右依次插入)。RPUSH key value [value …]
:将所有指定的值插入存储在key
的列表的末尾(最右边)。LPOP key
:删除并返回存储在key
的列表中的第一个元素。RPOP key
:删除并返回存储在key
的列表中的最后一个元素。LRANGE key start stop
:返回存储在key
的列表中指定范围的元素。 偏移量start
和stop
是基于零开始的索引。LINDEX key index
:返回存储在key
的列表中索引index
处的元素。索引从零开始,负索引可用于指定从列表末尾开始的元素。LLEN key
:返回存储在key
的列表的长度。如果key
不存在,则将其解释为空列表并返回0, 当存储在key
的值不是列表时将返回错误。HSET key field value [field value ...]
:将存储在key
处的哈希表中的field
的值设置为value
。HGET key field
:返回存储在key
处的哈希表中field
的值。HEXISTS key field
:查看存储在key
处的哈希表中的field
是否存在。HGETALL key
:返回存储在key
处的哈希表中所有字段和值。HKEYS key
:返回存储在key
处的哈希表中的所有字段名称。HVALS key
:返回存储在key
处的哈希表中的所有值。HLEN key
:回存储在key
处的哈希表中包含的字段数。HDEL key field [field ...]
:从存储在key
处的哈希表中删除指定的字段。SADD key member [member ...]
:将指定元素添加到存储在key
的集合中,若指定元素在该集合存在将被忽略。SPOP key [count]
:从存储在key
的集合中删除并返回一个或多个随机元素。SMEMBERS key
:返回存储在key
的集合中的所有元素。SCARD key
:返回存储在key
的集合的元素数量。SISMEMBER key member
:检查member
是否存储在key
的集合中的元素。SINTER key [key ...]
:返回所有给定集合的交集产生的集合元素。SINTERSTORE destination key [key ...]
:该命令与SINTER
类似,但是它并不是直接返回结果集,而是将结果保存在destination
集合中,如果destination
集合存在,则会被覆盖。SREM key member [member ...]
:在存储在key
的集合中移除指定的元素。指定元素不是集合元素将被忽略,集合不存在会返回0,key
的类型不是集合会返回错误。SUNION key [key ...]
:返回所有给定集合的并集产生的集合元素。SUNIONSTORE destination key [key ...]
:该命令与SUNION
类似,但是它并不是直接返回结果集,而是将结果保存在destination
集合中,如果destination
集合存在,则会被覆盖。ZADD key score member [score member ...]
:添加所有指定了score的指定元素到存储在key
的有序集合中。如果指定元素已经是有序集中元素,则将更新score并在正确的位置重新插入元素,以确保正确排序。如果key
不存在,则会创建一个以指定元素为唯一元素的新有序集。 如果键存在但不包含有序集,则返回错误。ZCARD key
:返回存储在key
的有序集的元素数量。ZRANGE key min max
:返回存储在key
的有序集中的指定范围的元素。ZRANK key member
:返回存储在key
的有序集中的元素member
的排名,其中所有元素按score从低到高排序,score值最小的元素排名为0。ZREM key member [member ...]
:从存储在key
的排序集中删除指定的元素。 不存在的元素将被忽略,当key
存在且不包含有序集时将返回错误。ZSCORE key member
:返回存储在key
的有序集中的元素member
的score。如果元素不存在于有序集中或者key
不存在,则返回nil
。比较常见的几种使用场景如下:
lpush
和rpop
写入和读取消息。不过最好使用Kafka、RabbitMQ等消息中间件。SETNX
命令实现分布式锁,除此之外,还可以使用官方提供的RedLock分布式锁实现。Redis基于Reactor模式开发了自己的网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。由于文件事件处理器是以单线程方式运行的,所以才说Redis是单线程模型。它采?IO多路复?机制同时监听多个socket,根据socket上的事件
来选择对应的事件处理器进?处理。
虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个socket,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程方式运行的模块进行对接,这保持了Redis内部单线程设计的简单性。并且Redis不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
文件事件处理器包含4个组成部分:
文件事件是对套接字操作的抽象,当一个套接字准备执行连接应答、读取、写入、关闭等操作时,会产生一个与操作对应的文件事件。一个服务器通常会连接多个套接字,所以多个文件事件可能并发出现。但IO多路复用程序会负责监听多个套接字,并将所以产生事件的套接字都放到一个队列里,然后通过该队列,以有序、同步、每次一个套接字的方式将套接字传到文件事件分派器,当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。文件事件分派器接收I/O多路复用程序传来的套接字,并根据套接字产生的事件类型调用相应的事件处理器进行处理。文件事件处理器的组成部分及工作原理如下图:
I/O多路复用程序可以监听多个套接字的ae.h/AE_READABIE
事件和ae.h/AE_WRITABLE
事件。
AE_READABIE
事件:当套接字变得可读时(客户端对套接字执行write或close操作),或者有新的可应答套接字出现时(客户端对服务器的监听套接字执行connect操作),套接字产生AE_READABLE
事件。AE_WRITABLE
事件:当套接字变得可写时(客户端对套接字执行read操作),套接字产生AE_WRITABLE
事件。I/O多路复用程序允许服务器同时监听套接字的AE_READABLE
事件和AE_WRITABLE
事件,如果一个套接字同时产生了这两种事件,那么文件事件分派器会优先处理AE_READABLE
事件,等到AE_READABLE
事件处理完之后,才处理AE_WRITABLE
事件。即一个套接字又可读又可写,服务器将先读套接字,后写套接字。
事件处理器中最常用的就是与客户端进行通信的连接应答处理器、命令请求处理器和命令回复处理器。
networking.c/acceptTcpHandler
函数,用于对连接服务器监听套接字的客户端进行应答。当Redis服务器进行初始化时,程序会将连接应答处理器和服务器监听套接字的AE_READABLE
事件关联,当有客户端连接服务器监听套接字的时候,套接字就会产生AE_READABLE
事件,引发连接应答处理器执行,并执行相应的套接字应答操作。networking.c/readQueryFromClient
函数,负责从套接字中读入客户端发送的命令请求内容。当一个客户端通过连接应答处理器成功连接到服务器之后,服务器会将客户端套接字的AE_READABLE
事件和命令请求处理器关联,当客户端向服务器发送命令请求的时候,套接字就会产生AE_READABLE
事件,引发命令请求处理器执行,并执行相应的套接字读入操作。networking.c/sendReplyToClient
函数,负责将服务器执行命令后得到的命令回复通过套接字返回给客户端。当服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的AE_WRITABLE
事件和命令回复处理器关联,当客户端准备好接收服务器传回的命令回复时,就会产生AE_WRITABLE
事件,引发命令回复处理器执行,并执行相应的套接字写入操作。当命令回复发送完毕后,服务器就会解除命令回复处理器与客户端套接字的AE_WRITABLE
事件之间的关联。下图是客户端与Redis的一次通信流程示例图:
客户端与Redis的一次通信流程如下:
AE_READABLE
事件与连接应答处理器关联。AE_READABLE
事件。AE_READABLE
事件后,将该套接字压入队列中,然后通过该队列将该套接字传给文件事件分派器。AE_READABLE
事件关联的连接应答处理器进行处理。AE_READABLE
事件与命令请求处理器关联。AE_READABLE
事件。AE_READABLE
事件后,将该套接字压入队列中,然后通过该队列将该套接字传给文件事件分派器。AE_READABLE
事件关联的命令请求处理器进行处理。AE_WRITABLE
事件与命令回复处理器关联。AE_WRITABLE
事件。AE_WRITABLE
事件后,将该套接字压入队列中,然后通过该队列将该套接字传给文件事件分派器。AE_WRITABLE
事件关联的命令回复处理器进行处理。AE_WRITABLE
事件与命令回复处理器之间的关联。Redis6.0之前为什么不使用多线程:
Redis6.0之后为何引入多线程:
Redis6.0引入多线程主要是为了提高网络IO读写性能,如果把网络读写做成多线程的方式对性能会有很大提升。虽然Redis6.0引入了多线程,但是Redis的多线程只是用来处理网络数据的读写等这类耗时的操作,执行命令仍然是单线程。
Redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb
结构表示,其中,redisDb
结构的dict
字典被称为键空间(key space),它保存了数据库中的所有键值对。
typedef struct redisDb {
...
dict *dict; // 数据库键空间,保存数据库中所有键值对
...
} redisDb;
键空间的键也就是数据库的键,每个键都是一个string对象;键空间的值也就是数据库的值,每个值可以是string对象、list对象、hash对象、set对象和sorted set对象中的任意一种Redis对象。一个数据库键空间的示例图如下:
因为数据库的键空间是一个字典,所以所有针对数据库的操作,实际上都是通过对键空间字典进行操作来实现的。几种常见的数据库操作实现原理如下:
在对数据库进行读写,不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:
INFO stats
命令的keyspace_hits
属性和keyspace_misses
属性中查看。OBJECTidletime <key>
命令可以查看键的闲置时间。WATCH
命令监视了某个键,那么服务器在修改被监视的键之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过。在Redis中通过EXPIRE
、EXPIREAT
、PEXPIRE
和PEXPIREAT
命令可以设置键的过期时间(键什么时候会被删除)或生存时间(键可以存在多久),超过过期时间后,它会被自动删除。
注意:
SETEX
命令可以在设置一个字符串键的同时为键设置过期时间,这是一个类型限定的命令,只能用于字符串键,但SETEX
命令设置过期时间的原理和EXPIRE
命令完全一样。
设置过期时间的作用:
EXPIRE key seconds
起始版本:1.0.0
时间复杂度:O(1)
给key
设置过期时间,超过过期时间后,它会被自动删除。对已经有过期时间的key
执行EXPIRE
操作,将会更新它的过期时间。如果设置过期时间成功,则返回1,如果key不存在,则返回0。
注意:
- 过期时间只有通过删除或覆盖的key的内容的命令来清除,这些命令包括
DEL
,SET
,GETSET
和所有的*STORE
命令。这意味着所有在概念上修改存储在键上的值而不用新键替换的操作都将保持过期时间不变。- 可以使用
PERSIST
命令清除过期时间,使key变回一个永久的key。- 如果使用
RENAME
命令重命名了key,则相关的过期生存时间将转移到新key上。例如原来存在key_A,使用RENAME Key_B Key_A
命令,旧的key_A的值将被覆盖,并且不管原来Key_A是永久的还是有过期时间的,都会被Key_B的有效期状态覆盖。- 在Redis 2.1.3之前的版本中,使用修改key的值的命令修改具有过期时间的key的值会删除整个key,这一行为是受当时复制(replication)层的限制而作出的,现在这一限制已经被修复。
EXPIREAT key timestamp
起始版本:1.2.0
时间复杂度:O(1)
EXPIREAT
的作用和语义与EXPIRE
类似,但是它没有指定表示TTL(生存时间)的秒数,而是使用了绝对的Unix时间戳(自1970年1月1日以来的秒数)。设置在过去的时间戳会立即删除key。如果设置过期时间成功,则返回1,如果key不存在,则返回0。引入EXPIREAT
是为了将AOF持久性模式的相对超时转换为绝对超时,它可以直接用于指定给定key未来的某个给定时间到期。
PEXPIRE key milliseconds
起始版本:2.6.0
时间复杂度:O(1)
此命令的工作原理与EXPIRE
完全相同,但是key的生存时间以毫秒为单位而不是秒。如果设置过期时间成功,则返回1,如果key不存在,则返回0。
PEXPIREAT key milliseconds-timestamp
起始版本:2.6.0
时间复杂度:O(1)
PEXPIREAT
的作用和语义与EXPIREAT
类似,但它以毫秒为单位设置key的过期unix时间戳,而不是以秒为单位。如果设置过期时间成功,则返回1,如果key不存在,则返回0。
虽然这几个命令以不同单位和不同形式来设置过期时间,但实际上EXPIRE
、PEXPIRE
、EXPIREAT
三个命令都是使用PEXPIREAT
命令来实现的:无论客户端执行的是以上四个命令中的哪一个,经过转换之后,最终的执行效果都和执行PEXPIREAT
命令一样,即最终都会转换成PEXPIREAT
执行。这几个命令的转换如下,EXPIRE
----->PEXPIRE
----->PEXPIREAT
<-----EXPIREAT
。
Redis通过过期字典即redisDb
结构的expires
字典来保存数据库中所有键的过期时间。过期字典的键是一个指针,指向键空间中的某个键对象(数据库键),过期字典的值是一个long long
类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的UNIX时间戳。
typedef struct redisDb {
...
dict *dict; // 数据库键空间,保存数据库中所有键值对
dict *expires // 过期字典,保存键的过期时间
...
} redisDb;
一个带有过期字典的示例图如下:
注意:图中键空间和过期字典中重复出现了两次键对象。在实际中,键空间的键和过期字典的键都指向同一个键对象,所以不会出现任何重复对象,也不会浪费任何空间。
当执行PEXPIREAT
或者其他三个会转换成PEXPIREAT
的命令为一个数据库键设置过期时间时,服务器会在数据库的过期字典中关联给定的数据库键和过期时间。通过过期字典,Redis可以判定给定键是否过期,首先检查给定键是否存在于过期字典,如果存在,取得键的过期时间,然后检查当前UNIX时间截是否大于键的过期时间,大于则键已过期,否则,键未过期。
PERSIST key
起始版本:2.2.0
时间复杂度:O(1)
移除给定key
的过期时间,将这个key从『易失的』(设置有过期时间的key)转换成『持久的』(不带过期时间、永不过期的key)。如果移除过期时间成功,则返回1,如果key不存在或没有设置过期时间,则返回0。
PERSIST
命令就是PEXPIREAT
命令的反操作:PERSIST
命令在过期字典中查找给定的键,并解除键和值(过期时间)在过期字典中的关联。
TTL key
起始版本:1.0.0
时间复杂度:O(1)
以秒为单位返回具有过期时间的key
的剩余生存时间。在Redis 2.6及之前版本,如果key不存在或者key存在但未设置过期时间时返回-1。
从Redis2.8开始,key不存在返回-2,key存在但未设置过期时间返回-1。
PTTL key
起始版本:2.6.0
时间复杂度:O(1)
以毫秒为单位返回具有过期时间的key
的剩余生存时间。在Redis 2.6及之前版本,如果key不存在或者key存在但未设置过期时间时返回-1。从Redis2.8开始,key不存在返回-2,key存在但未设置过期时间返回-1。
TTL
和PTTL
两个命令都是通过计算键的过期时间(键会被删除的时间)和当前时间之间的差来实现的。
一个键过期了,它什么时候会被删除取决于过期键的删除策略,而过期键的删除策略有以下三种: