Redis中相应的概念:
简单动态字符串
链表
字典
整数集合
压缩列表
对象
Redis
服务器将所有数据库都保存在服务器状态:redisServer
结构的db
数组中,db
数组中每个元素都是一个redisDb
结构,每个结构代表一个数据库
struct redisServer{
//...
//一个数组,保存着服务中的所有数据库
redisDb *db;
//服务器的数据库数量
int dbnum;
//...
};
dbnum
属性的值由服务器配置的database
选项决定,默认下是16,所以默认情况下会创建16个数据库
每个Redis
客户端都有自己的目标数据库,每当在客户端编写命令或者数据库执行命令时,目标数据库就会成为这些命令的操作对象。默认情况下,客户端的目标数据库是0号数据库,可以通过SELECT
命令切换数据库。
redis> SET msg "hello world"
OK
redis> GET msg
"hello world"
redis> SELECT 2
OK
redis[2]> GET msg
(nil) # 因为2号数据库并没有 msg 这个键,msg是存在于0号数据库中
redis[2]> SET msg "another world"
OK
redis[2]> GET msg
"another msg"
客户端状态redisClient
结构的db
属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb
结构的指针。
typedef struct redisClent{
//...
//记录客户端当前正在使用的数据库
redisDb *db;
//...
} redisClient;
redisClient.db
指针指向redisServer.db
数组中的元素,该元素代表当前客户端正在使用的数据库
例如当前客户端正在使用1号数据库,那么redisClient
和redisServer
的关系示意图如下:
执行SELECT 2
之后:
通过修改redisClient.db
指针,让它指向数组中的不同元素,从而实现切换目标数据库的功能,这就是SELECT
命令的实现原理。
Redis
是一个键值对服务器,每个数据库都由一个redisDb
结构表示,其中redisDb
结构的dict
字典中的所有键值对,我们将其称为键空间(key space
)
typedef struct redisDb{
//...
//数据库键空间,保存着数据库中的所有键值对
dict *dict;
//...
} redisDb;
键空间和用户所见的数据库是直接对应的:
Redis
对象如果执行下面的命令:
redis> SET message "hello world"
OK
redis> RPUSH alphabet "a" "b" "c"
(integer) 3
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josian L. Carlson"
(integer) 1
redis> HSET book publisher "Manning"
(integer) 1
执行过后,数据库的键空间会是这样的:【值得注意的是,这里的StringObject
代表字符串对象,HashObject
代表哈希表对象,ListObject
代表列表对象,这些均是为了简化表达方式】
alphabet
是一个列表键,键的名字是一个包含字符串alphabet
的字符串对象,键的值则是一个包含三个元素的列表对象book
是一个哈希表键,键的名字是一个包含字符串book
的字符串对象,键的值是一个包含三个键值对的哈希表对象message
是一个字符串键,键的名字是一个包含字符串message
的字符串对象,键的值则是一个包含字符串hello world
的字符串对象因为数据库的键空间是一个字典,所以所有针对数据库的操作,实际上都是对这个键空间字典进行操作来实现的。
添加一个新键,实际上就是将一个新的键值对添加到键空间字典中,其中键为字符串对象,而值则为任意一种类型的Redis
对象
比如当前状况下的键空间示意图如下所示:
当往这个键空间添加一个新的键值对之后:
redis> SET date "2013.12.1"
OK
添加之后的示意图:
这个新的键值对的键是一个字符串对象,值是一个包含字符串“2013.12.1”的字符串对象
删除数据库的一个键,实际上就是在键空间里面删除键所对应的键值对对象。
假设键空间的状态如图9-4所示,执行以下命令:
redis> DEL book
(integer) 1
执行命令之后的键空间状态为:
对一个数据库键进行更新,实际上就是在键空间里对键所对应的值对象进行更新,根据值对象的类型不同,更新的具体方法也会不同
假设当前键空间的状态如图9-4所示,执行下面命令:
redis> SET message "blah blah"
OK
执行命令后,键空间状态应为:
对一个数据库键进行取值,实际上就是在键空间中取出键所对应的值对象,根据值对象的类型不同,具体的取值方法也会有所不同
假设当前键空间的状态如图9-4所示,执行执行下列命令:
redis> GET message
"hello world"
那么这个取值过程会是这样的:
在对键空间进行相应读写操作时,Redis
还会执行一些额外的维护操作,包括:
hit
)次数或键空间不命中(miss
)次数LRU
时间,这个值可以用于计算键的闲置时间WATCH
命令监视某个键,那那么服务器在对这个键进行修改之后,会将这个键标记为脏(dirty
),从而让事务程序注意到这个键已经被修改过了Redis
有四个不同的命令可以用于设置键的生存时间(键可以存在多久)或过期时间(键什么时候被删除)
EXPIRE <key> <ttl>
:命令用于将键key
的生存时间设置为ttl
秒PEXPIRE <key> <ttl>
:命令用于将键key
的生存时间设置为ttl
毫秒EXPIREAT <key> <timestamp>
:命令用于将键key
的过期时间设置为timestamp
所指定的秒数时间戳PEXPIREAT <key> <timestamp>
:命令用于将键key
的过期时间设置为timestamp
所指定的毫秒数时间戳虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT
三个命令都是使用PEXPIREAT
命令实现的
# EXPIRE命令
def EXPIRE(key,ttl_in_sec):
# 将TTL从秒转换成毫秒
ttl_in_ms=sec_to_ms(ttl_in_sec)
PEXPIRE(key,ttl_in_ms)
######################################
# PEXPIRE又可以转换成PEXPIREAT
def PEXPIRE(key,ttl_in_ms):
# 获取以毫秒计算的当前UNIX时间戳
now_ms=get_current_unix_timestamp_in_ms()
# 当前时间加上TTL,得出毫秒格式的键过期时间
PEXPIREAT(key,now_ms+ttl_in_ms)
######################################
# EXPIREAT命令可以转换成PEXPIREAT
def EXPIREAT(key,expire_time_in_sec):
# 将过期时间从秒转换为毫秒
expire_time_in_ms=sec_to_ms(expire_time_in_sec)
PEXPIREAT(key,expire_time_in_ms)
redisDb
结构的expires
字典保存了数据库中的所有键的过期时间,我们称这个为过期字典
long long
类型的整数,这个整数保存了键所指向的数据库键的过期时间(一个毫秒精度的UNIX
时间戳)typedef struct redisDb{
//...
//过期字典,保存着键的过期时间
dict *expires;
//...
}redisDb;
值得一提的是:过期字典中的键是指向键空间的键对象,图9-12这样表示是为了展示方便
前文提到的PEXPIREAT
命令的伪代码如下:
def PEXPIREAT(key,expire_time_in_ms):
# 如果给定的键不存在于键空间,那么不能设置过期时间
if key not in redisDb.dict:
return 0
# 在过期字典中关联键和过期时间
redisDb.expires(key)=expire_time_in_ms
# 过期时间设置成功
return 1
PERSIST
命令可以移除一个键的过期时间
redis> PEXPIREAT message 1391234400000
(integer) 1
redis> TTL message
(integer) 13893281
redis> PERSIST message
(integer) 1
redis> TTL message
(integer) -1
PERSIST
命令就是PEXPIREAT
的反操作:PERSIST
命令会在过期字典中查找给定的键,并解除键和值在过期字典中的关联(也就是移除键的过期时间)
假设数据库当前状态如图9-12所示,当执行下面的命令后:
redis> PERSIST book
(integer) 1
PERSIST
命令的伪定义:
def PERSIST(key):
#如果键不存在,或者键没有设置过期时间,那么直接返回
if key not in redisDb.expires:
return 0
#移除过期字典中给定键的键值对关联
redisDb.expires.remove(key)
#键的过期时间移除成功
return 1
TTL
:以秒为单位返回键的剩余生存时间PTTL
:以毫秒为单位返回键的剩余生存时间PTTL伪代码
def PTTL(key):
# 键不存在于数据库
if key not in redisDb.dict:
return -2
# 尝试获取键的过期时间
# 如果键没有设置过期时间,那么expire_time_in_ms 将为none
expire_time_in_ms=redisDb.expires.get(key)
# 键没有设置过期时间
if expire_time_in_ms is None:
return -1
# 获取当前时间
now_ms=get_current_unix_timestamp_in_ms()
# 过期时间减去当前时间,得出的差就是键的剩余生存时间
return(expire_time_in_ms - now_ms)
TTL伪代码
def TTL(key):
#获取以毫秒为单位的剩余生存时间
ttl_in_ms=PTTL(key)
if ttl_in_ms <0 :
#处理返回值为-2和-1的情况
return ttl_in_ms
else:
#将毫秒转换为秒
return ms_to_sec(ttl_in_ms)
通过过期字典,程序用以下步骤检查一个键是否过期
UNIX
时间戳是否大于键的过期时间:如果是的话,那么键已经过期了;反之如果一个键过期了,那么它什么时候会被删除呢?
这个问题有三种可能的答案,代表着三种不同的删除策略:
定时删除
定时删除策略对内存非常友好:因为使用定时器,每当键过期了就会立马被删除。但如果存在了大量的过期键,程序需要花费一部分CPU
时间去删除这些过期键(可以理解为:删除过期键的动作会与主要任务抢夺CPU
的执行时间);并且创建一个定时器需要使用Redis
服务器中的时间事件,而当前时间事件的实现方式----无序链表,因为其查找时间复杂度为O(N)
,所以并不能高效的处理大量时间事件
惰性删除
惰性删除策略对CPU
时间来说是非常友好的:只有当用到某个键的时候才会去判断这个键是否过期,过期就将删除;反之。并且删除的目标仅限于当前处理的键,并不会在其他无关的键上花费任何CPU
时间。
但缺点就是:如果数据库中存在大量过期键,但是这些过期键又恰好都没有被访问到,那么它们或许永远都不会被删除,这可以看成是一种内存泄漏(垃圾数据占用了大部分内存,但却删除不了)
定期删除
CPU
时间,与任务执行抢夺时间,影响服务器的响应时间和吞吐量定期删除策略是对定时删除和惰性删除的一种整合和折中
CPU
时间的影响。这种策略的关键在于:删除操作的时长和频率的确定。如果时间太长或者执行太频繁,那就会退化成定时删除策略;如果时间太短或者频率过低,容易造成大量过期键堆积,退化成惰性删除。
Redis
服务器实际使用的是惰性删除和定期删除两种策略,通过两种策略配合使用,可以合理地在CPU
时间和内存上取得平衡