Lua脚本
Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类 似。 使用 Lua 脚本来执行 Redis 命令的好处:
- 1、一次发送多个命令,减少网络开销。
- 2、Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
- 3、对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
在 Redis 中调用 Lua 脚本
- eval 代表执行 Lua 语言的命令。
- lua-script 代表 Lua 语言脚本内容。
- key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。
- [key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。
- [value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的
redis> eval "return 'Hello World'" 0
使用 redis.call(command, key [param1, param2…])进行操作
- command 是命令,包括 set、get、del 等。
- key 是被操作的键。
- param1,param2…代表给 key 的参数。
127.0.0.1:6379> set key1 3
OK
127.0.0.1:6379> set key2 0
OK
127.0.0.1:6379> eval "local v1 = redis.call('get',KEYS[1]); return redis.call('incrby',KEYS[2],v1);" 2 key1 key2
(integer) 3
127.0.0.1:6379>
案例:对IP进行限流
-- ip_limit.lua -- IP 限流,对某个 IP 频率进行限制 ,6 秒钟访问 10 次
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
6 秒钟内限制访问 10 次,调用测试(连续调用 10 次)
./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 6 10
缓存Lua脚本
-
为什么要缓存
在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端, 会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发 者通过脚本内容的 SHA1 摘要来执行脚本
-
如何缓存
Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执 行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果 找到了则执行脚本,否则会返回错误:“NOSCRIPT No matching script. Please use EVAL.”
127.0.0.1:6379> script load "return 'Hello World'" "470877a599ac74fbfda41caa908de682c5fc7d4b" 127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0 "Hello World"
-
自乘案例
Redis 有 incrby 这样的自增命令,但是没有自乘,比如乘以 3,乘以 5。 我们可以写一个自乘的运算,让它乘以后面的参数:
local curVal = redis.call("get", KEYS[1]) if curVal == false then curVal = 0 else curVal = tonumber(curVal) end curVal = curVal * tonumber(ARGV[1]) redis.call("set", KEYS[1], curVal) return curVal
把这个脚本变成单行,语句之间使用分号隔开,script load ‘命令’
127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal' "be4f93d8a5379e5e5b768a74e77c8a4eb0434441"
调用
127.0.0.1:6379> set num 2 OK 127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6 (integer) 12
脚本超时
Redis 的指令执行本身是单线程的,这个线程还要执行客户端的 Lua 脚本,如果 Lua 脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?
eval 'while(true) do end' 0
为 了防 止 某个 脚本 执 行时 间 过长 导 致 Redis 无 法提 供 服务 , Redis 提 供 了 lua-time-limit 参数限制脚本的最长运行时间,默认为 5 秒钟。
# redis.conf
################################ LUA SCRIPTING ###############################
# Max execution time of a Lua script in milliseconds.
#
# If the maximum execution time is reached Redis will log that a script is
# still in execution after the maximum allowed time and will start to
# reply to queries with an error.
#
# When a long running script exceeds the maximum execution time only the
# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
# used to stop a script that did not yet called write commands. The second
# is the only way to shut down the server in the case a write command was
# already issued by the script but the user doesn't want to wait for the natural
# termination of the script.
#
# Set it to 0 or a negative value for unlimited execution without warnings.
lua-time-limit 5000
当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚 本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。 Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端:script kill
如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的。
127.0.0.1:6379> eval "redis.call('set','gupao','666') while true do end" 0
因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子 性的要求。最终要保证脚本要么都执行,要么都不执行。
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
遇到这种情况,只能通过 shutdown nosave
命令来强行终止 redis。 shutdown nosave
和 shutdown
的区别在于 shutdown nosave
不会进行持久化 操作,意味着发生在上一次快照后的数据库修改都会丢失
如果我们有一些特殊的需求,可以用 Lua 来实现,但是要注意那些耗时的操作
lua源码
struct redisServer {
...
/* Scripting */
// lua解释器,所有客户端公用
lua_State *lua; /* The Lua interpreter. We use just one for all clients */
// lua中向Redis查询的“伪客户端”
client *lua_client; /* The "fake client" to query Redis from Lua */
// 正在执行脚本调用的客户端
client *lua_caller; /* The client running EVAL right now, or NULL */
// sha1 和原始脚本的字典映射
dict *lua_scripts; /* A dictionary of SHA1 -> Lua scripts */
// 缓存脚本使用的内存,单位:字节
unsigned long long lua_scripts_mem; /* Cached scripts' memory + oh */
// 脚本超时,单位 毫秒
mstime_t lua_time_limit; /* Script timeout in milliseconds */
// 脚本启动时间
mstime_t lua_time_start; /* Start time of script, milliseconds time */
// 脚本执行期间有调用写命令,则为true
int lua_write_dirty; /* True if a write command was called during the
execution of the current script. */
// 脚本执行期间有调用随机命令
int lua_random_dirty; /* True if a random command was called during the
execution of the current script. */
// 如果是脚本效果复制,则为true
int lua_replicate_commands; /* True if we are doing single commands repl. */
// 如果是传播事务,则为true
int lua_multi_emitted;/* True if we already proagated MULTI. */
// 脚本复制标志
int lua_repl; /* Script replication flags for redis.set_repl(). */
// 脚本执行超时,则为true
int lua_timedout; /* True if we reached the time limit for script
execution. */
// 杀死脚本,则为true
int lua_kill; /* Kill the script if true. */
// 默认复制类型
int lua_always_replicate_commands; /* Default replication type. */
};
scripting.c
在Redis服务初始化程序initServer
调用 脚本初始化函数scriptingInit
redis.call
具体执行函数 luaRedisGenericCommand
发布与订阅
通过列表实现消息队列局限
通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消 费者需要不停地调用 lpop 查看 List 中是否有等待处理的消息(比如写一个 while 循环)。 为了减少通信的消耗,可以 sleep()一段时间再消费,但是会有两个问题
1、如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内 存。
2、消息的实时性降低。
list 还提供了一个阻塞的命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞
基于list 实现的消息队列,不支持一对多的消息分发
发布订阅模式
除了通过 list 实现消息队列之外,Redis 还提供了一组命令实现发布/订阅模式。 这种方式,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝 试获取消息
首先,我们有很多的频道(channel),我们也可以把这个频道理解成 queue。订 阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。 只要有消息到达了频道,所有订阅了这个频道的订阅者都会收到这条消息。
需要注意的注意是,发出去的消息不会被持久化,因为它已经从队列里面移除了, 所以消费者只能收到它开始订阅这个频道之后发布的消息
# 调阅三个频道
subscribe channel-1 channel-2 channel-3
# 发布消息
publish channel-1 111
# 需要订阅(不能在订阅状态使用)
unsubscribe channel-1
# 按pattern订阅 支持?和*
psubscribe *sport
psubscribe news*
# 生成者
publish news-sport yaoming
publish news-music jaychou
相关源码
struct redisServer{
...
/* Pubsub */
// key为channel值,value为一个个clients
dict *pubsub_channels; /* Map channels to list of subscribed clients */
// 链表,节点值为一个个pubsubPattern
list *pubsub_patterns; /* A list of pubsub_patterns */
int notify_keyspace_events; /* Events to propagate via Pub/Sub. This is an
xor of NOTIFY_... flags. */
...
}
typedef struct pubsubPattern {
client *client; // 订阅该模式的客户端
robj *pattern; // 模式结构体
} pubsubPattern;
typedef struct client {
...
int flags; /* Client flags: CLIENT_* macros. */
...
dict *pubsub_channels; /* channels a client is interested in (SUBSCRIBE) */
list *pubsub_patterns; /* patterns a client is interested in (SUBSCRIBE) */
...
}
pubsub.c
/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
...
/* Send to clients listening for that channel */
// 从pubsub_channels取出调阅该channel的客户端
de = dictFind(server.pubsub_channels,channel);
// 如果有订阅该channel的客户端,依次向客户端发送该消息
if (de) {
list *list = dictGetVal(de);
listNode *ln;
listIter li;
listRewind(list,&li);
while ((ln = listNext(&li)) != NULL) {
client *c = ln->value;
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.messagebulk);
addReplyBulk(c,channel);
addReplyBulk(c,message);
receivers++;
}
}
/* Send to clients listening to matching channels */
if (listLength(server.pubsub_patterns)) {
listRewind(server.pubsub_patterns,&li);
channel = getDecodedObject(channel);
while ((ln = listNext(&li)) != NULL) {
pubsubPattern *pat = ln->value;
if (stringmatchlen((char*)pat->pattern->ptr,
sdslen(pat->pattern->ptr),
(char*)channel->ptr,
sdslen(channel->ptr),0)) {
addReply(pat->client,shared.mbulkhdr[4]);
addReply(pat->client,shared.pmessagebulk);
addReplyBulk(pat->client,pat->pattern);
addReplyBulk(pat->client,channel);
addReplyBulk(pat->client,message);
receivers++;
}
}
decrRefCount(channel);
}
return receivers;
}
void subscribeCommand(client *c) {
int j;
// 依次对每个订阅的channel执行订阅操作
for (j = 1; j < c->argc; j++)
pubsubSubscribeChannel(c,c->argv[j]);
// 客户端设置CLIENT_PUBSUB,进入pub/sub模式
c->flags |= CLIENT_PUBSUB;
}
/* Subscribe a client to a channel. Returns 1 if the operation succeeded, or
* 0 if the client was already subscribed to that channel. */
int pubsubSubscribeChannel(client *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
// 首先将订阅的channel加入client的pubsub_channels中,如果存在则直接返回
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
/* Add the client to the channel -> list of clients hash table */
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
// 将该client加入server.pubsub_channels中订阅该channel的值链表
listAddNodeTail(clients,c);
}
/* Notify the client */
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.subscribebulk);
addReplyBulk(c,channel);
addReplyLongLong(c,clientSubscriptionsCount(c));
return retval;
}
事务
Redis 的事务涉及到四个命令:multi(开启事务)
,exec(执行事务)
,discard (取消事务)
,watch(监视)
案例
set jack 1000
set mic 1000
multi
decrby jack 100
incrby mic 100
exec
一个事务从开始到结束通常经历三个阶段
事务开始,命令入队,事务执行
通过 multi 的命令开启事务。事务不能嵌套,多个 multi 命令效果一样。 multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被 执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执行。
通过 exec 的命令执行事务。如果没有执行 exec,所有的命令都不会被执行。
如果中途不想执行事务了,怎么办?
可以调用 discard 可以清空事务队列,放弃执行
multi
set k1 1
set k2 2
set k3 3
discard
watch 命令
在 Redis 中还提供了一个 watch 命令。
它可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修 改的情况下,才更新成新的值。
我们可以用 watch 监视一个或者多个 key,如果开启事务之后,至少有一个被监视 key 键在 exec 执行之前被修改了, 那么整个事务都会被取消(key 提前过期除外)。可 以用 unwatch 取消
#client1
127.0.0.1:6379> set balance 1000
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby balance 100
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get balance
# client2
# client1 watch balance期间执行
decrby balance 100
事务可能遇到的问题
我们把事务执行遇到的问题分成两种,
- 一种是在执行 exec 之前发生错误
# 比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误) # 在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 666 QUEUED 127.0.0.1:6379> hset k2 2434 (error) ERR wrong number of arguments for 'hset' command 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors.
- 一种是在 执行 exec 之后发生错误
# 比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误 127.0.0.1:6379> multi OK 127.0.0.1:6379> set k1 1 QUEUED 127.0.0.1:6379> hset k1 a b QUEUED 127.0.0.1:6379> exec 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> get k1 "1" # 最后我们发现 set k1 1 的命令是成功的,也就是在这种发生了运行时异常的情况下, # 只有错误的命令没有被执行,但是其他命令没有受到影响 ```
为什么在一个事务中存在错误,Redis 不回滚?
Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速
Redis内存回收
Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回 收。内存回收主要分为两类,一类是 key 过期,一类是内存使用达到上限(max_memory) 触发内存淘汰。
struct redisServer {
...
int hz; /* serverCron() calls frequency in hertz */
redisDb *db;
dict *commands; /* Command table */
dict *orig_commands; /* Command table before command renaming. */
aeEventLoop *el;
unsigned int lruclock; /* Clock for LRU eviction */
}
# redisDb中的expires 过期key
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
/* 设置了过期时间的键值对 */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
过期策略
要实现 key 过期,我们有几种思路
定时过期(主动淘汰)
每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策 略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的 数据,从而影响缓存的响应时间和吞吐量。
惰性过期(被动淘汰)
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最 大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再 次被访问,从而不会被清除,占用大量内存。
例如 String,在 getCommand
里面会调用 expireIfNeeded
server.c
expireIfNeeded(redisDb *db, robj *key)
第二种情况,每次写入 key 时,发现内存不够,调用 activeExpireCycle
释放一部分 内存。
expire.c
activeExpireCycle(int type)
定期过期
每隔一定的时间,会扫描一定数量的数据库的 expires 字典
(redisDb中属性
)中一定数量的 key,并清 除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和 每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
Redis 中同时使用了惰性过期和定期过期两种过期策略。
淘汰策略
Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来 决定清理掉哪些数据,以保证新数据的存入。
maxmemory-policy noeviction
# 根据 LRU 算法删除设置了超时属性(expire)的键,直到腾出足够内存为止。
# 如果没有 可删除的键对象,回退到 noeviction 策略
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# 根据 LRU 算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
# allkeys-lru -> Evict any key using approximated LRU.
# 在带有过期时间的键中选择最不常用的。
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# 在所有的键中选择最不常用的,不管数据有没有设置超时属性。
# allkeys-lfu -> Evict any key using approximated LFU.
# 在带有过期时间的键中随机选择。
# volatile-random -> Remove a random key among the ones with an expire set.
# 随机删除所有键,直到腾出足够内存为止。
# allkeys-random -> Remove a random key, any key.
# 根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略。
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)
# OOM command not allowed when used memory,此时 Redis 只响应读操作。
# noeviction -> Don't evict anything, just return an error on write operations.
LRU,Least Recently Used
:最近最少使用。判断最近被使用的时间,目前最远的 数据优先被淘汰LFU,Least Frequently Used
,最不常用,4.0 版本新增。random
,随机删除。
如果没有符合前提条件的 key 被淘汰,那么 volatile-lru、volatile-random 、 volatile-ttl 相当于 noeviction(不做内存回收)
动态修改淘汰策略:
redis> config set maxmemory-policy volatile-lru
建议使用 volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的 key。
如果基于传统 LRU 算法实现 Redis LRU 会有什么问题?
LRU 淘汰原理
需要额外的数据结构存储,消耗内存。
Redis LRU 对传统的 LRU 算法进行了改良,通过随机采样来调整算法的精度。 如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个), 随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据。所以采样参数m配置的数值越大, 就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计 算,执行效率降低。
如何找出热度最低的数据?
Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段 用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值。 但是不是获取系统当前的时间戳,而是设置为全局变量server.lruclock
的值。
Redis 中 有 个 定 时 处 理 的 函 数 serverCron
, 默 认 每 100 毫 秒 调 用 函 数 updateCachedTime 更新一次全局变量的 server.lruclock 的值,它记录的是当前 unix 时间戳。
server.c
/* We take a cached value of the unix time in the global state because with
* virtual memory and aging there is to store the current time in objects at
* every object access, and accuracy is not needed. To access a global var is
* a lot faster than calling time(NULL) */
void updateCachedTime(void) {
time_t unixtime = time(NULL);
atomicSet(server.unixtime,unixtime);
server.mstime = mstime();
/* To get information about daylight saving time, we need to call localtime_r
* and cache the result. However calling localtime_r in this context is safe
* since we will never fork() while here, in the main thread. The logging
* function will call a thread safe version of localtime that has no locks. */
struct tm tm;
localtime_r(&server.unixtime,&tm);
server.daylight_active = tm.tm_isdst;
}
为什么不获取精确的时间而是放在全局变量中?不会有延迟的问题吗?
这样函数 lookupKey 中更新数据的 lru 热度值时,就不用每次调用系统函数 time,可 以提高执行效率
当对象里面已经有了 LRU 字段的值,就可以评估对象的热度了。
函数 estimateObjectIdleTime
评估指定对象的 lru 热度,思想就是对象的 lru 值和 全局的 server.lruclock
的差值越大(越久没有得到更新), 该对象热度越低。
源码 evict.c
/* Given an object returns the min number of milliseconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {
unsigned long long lruclock = LRU_CLOCK();
if (lruclock >= o->lru) {
return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;
} else {
return (lruclock + (LRU_CLOCK_MAX - o->lru)) *
LRU_CLOCK_RESOLUTION;
}
}
server.lruclock
只有 24 位,按秒为单位来表示才能存储 194 天。当超过 24bit 能表 示的最大时间的时候,它会从头开始计算
#define LRU_CLOCK_MAX ((1<<LRU_BITS)-1) /* Max value of obj->lru */
在这种情况下,可能会出现对象的 lru 大于 server.lruclock 的情况,如果这种情况 出现那么就两个相加而不是相减来求最久的 key。
为什么不用常规的哈希表+双向链表的方式实现?需要额外的数据结构,消耗资源。 而 Redis LRU 算法在 sample 为 10 的情况下,已经能接近传统 LRU 算法了。
除了消耗资源之外,传统 LRU 还有什么问题
假设 A 在 10 秒内被访问了 5 次,而 B 在 10 秒内被访问了 3 次。因为 B 最 后一次被访问的时间比 A 要晚,在同等的情况下,A 反而先被回收
LFU
/**
* redis对象
* redis对象系统实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象时,这个对象所有占用的内存就会自动释放
* redis还通过引用计数技术实现了对象共享机制
*
**/
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// LRU|LFU 记录对象最后一次命令访问的时间
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
// 引用计数
int refcount;
// 指向底层实现数据结构的指针
void *ptr;
} robj;
当这 24 bits 用作 LFU 时,其被分为两部分:
- 高 16 位用来记录访问时间(单位为分钟,ldt,last decrement time)
- 低 8 位用来记录访问频率,简称 counter(logc,logistic counter)
counter 是用基于概率的对数计数器实现的,8 位可以表示百万次的访问频率。 对象被读写的时候,lfu 的值会被更新
db.c——lookupKey
void updateLFU(robj *val) {
unsigned long counter = LFUDecrAndReturn(val);
counter = LFULogIncr(counter);
val->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
增长的速率由,lfu-log-factor
越大,counter 增长的越慢
# lfu-log-factor 10
如果计数器只会递增不会递减,也不能体现对象的热度。没有被访问的时候, 计数器怎么递减呢?
减少的值由衰减因子 lfu-decay-time
(分钟)来控制,如果值是 1 的话,N 分 钟没有访问就要减少 N。 redis.conf 配置文件
# lfu-decay-time 1
持久化机制
Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者 宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis 提供了两种持久 化的方案,一种是 RDB 快照(Redis DataBase),一种是 AOF(Append Only File)。
struct redisServer {
...
/* RDB / AOF loading information */
int loading; /* We are loading data from disk if true */
off_t loading_total_bytes;
off_t loading_loaded_bytes;
time_t loading_start_time;
off_t loading_process_events_interval_bytes;
...
/* AOF persistence */
int aof_state; /* AOF_(ON|OFF|WAIT_REWRITE) */
int aof_fsync; /* Kind of fsync() policy */
char *aof_filename; /* Name of the AOF file */
int aof_no_fsync_on_rewrite; /* Don't fsync if a rewrite is in prog. */
int aof_rewrite_perc; /* Rewrite AOF if % growth is > M and... */
off_t aof_rewrite_min_size; /* the AOF file is at least N bytes. */
off_t aof_rewrite_base_size; /* AOF size on latest startup or rewrite. */
off_t aof_current_size; /* AOF current size. */
off_t aof_fsync_offset; /* AOF offset which is already synced to disk. */
int aof_rewrite_scheduled; /* Rewrite once BGSAVE terminates. */
pid_t aof_child_pid; /* PID if rewriting process */
list *aof_rewrite_buf_blocks; /* Hold changes during an AOF rewrite. */
sds aof_buf; /* AOF buffer, written before entering the event loop */
int aof_fd; /* File descriptor of currently selected AOF file */
int aof_selected_db; /* Currently selected DB in AOF */
time_t aof_flush_postponed_start; /* UNIX time of postponed AOF flush */
time_t aof_last_fsync; /* UNIX time of last fsync() */
time_t aof_rewrite_time_last; /* Time used by last AOF rewrite run. */
time_t aof_rewrite_time_start; /* Current AOF rewrite start time. */
int aof_lastbgrewrite_status; /* C_OK or C_ERR */
unsigned long aof_delayed_fsync; /* delayed AOF fsync() counter */
int aof_rewrite_incremental_fsync;/* fsync incrementally while aof rewriting? */
int rdb_save_incremental_fsync; /* fsync incrementally while rdb saving? */
int aof_last_write_status; /* C_OK or C_ERR */
int aof_last_write_errno; /* Valid if aof_last_write_status is ERR */
int aof_load_truncated; /* Don't stop on unexpected AOF EOF. */
int aof_use_rdb_preamble; /* Use RDB preamble on AOF rewrites. */
/* AOF pipes used to communicate between parent and child during rewrite. */
int aof_pipe_write_data_to_child;
int aof_pipe_read_data_from_parent;
int aof_pipe_write_ack_to_parent;
int aof_pipe_read_ack_from_child;
int aof_pipe_write_ack_to_child;
int aof_pipe_read_ack_from_parent;
int aof_stop_sending_diff; /* If true stop sending accumulated diffs
to child process. */
sds aof_child_diff; /* AOF diff accumulator child side. */
/* RDB persistence */
// 计数器 记录距离上一成功保存rdb文件后,服务器对数据库状态进行了多少次修改
long long dirty; /* Changes to DB from the last save */
long long dirty_before_bgsave; /* Used to restore dirty on failed BGSAVE */
pid_t rdb_child_pid; /* PID of RDB saving child */
// rdb条件参数 决定什么时候进行rdb
struct saveparam *saveparams; /* Save points array for RDB */
// rdb参数长度
int saveparamslen; /* Number of saving points */
// rdb文件名
char *rdb_filename; /* Name of RDB file */
int rdb_compression; /* Use compression in RDB? */
int rdb_checksum; /* Use RDB checksum? */
// 上一次保存时间
time_t lastsave; /* Unix time of last successful save */
time_t lastbgsave_try; /* Unix time of last attempted bgsave */
time_t rdb_save_time_last; /* Time used by last RDB save run. */
time_t rdb_save_time_start; /* Current RDB save start time. */
int rdb_bgsave_scheduled; /* BGSAVE when possible if true. */
int rdb_child_type; /* Type of save by active child. */
int lastbgsave_status; /* C_OK or C_ERR */
int stop_writes_on_bgsave_err; /* Don't allow writes if can't BGSAVE */
int rdb_pipe_write_result_to_parent; /* RDB pipes used to return the state */
int rdb_pipe_read_result_from_child; /* of each slave in diskless SYNC. */
/* Pipe and data structures for child -> parent info sharing. */
int child_info_pipe[2]; /* Pipe used to write the child_info_data. */
struct {
int process_type; /* AOF or RDB child? */
size_t cow_size; /* Copy on write size. */
unsigned long long magic; /* Magic value to make sure data is valid. */
} child_info_data;
/* Propagation of commands in AOF / replication */
redisOpArray also_propagate; /* Additional command to propagate. */
/* Logging */
char *logfile; /* Path of log file */
int syslog_enabled; /* Is syslog enabled? */
char *syslog_ident; /* Syslog ident */
int syslog_facility; /* Syslog facility */
...
}
/***
* 这个结构体记录配置文件中设置
* 进行rdb的条件
* save 900 1
* save 300 10
* save 60 1000
*/
struct saveparam {
// 秒数
time_t seconds;
// 修改数
int changes;
};
redis.conf
rdb和aof配置
################################ SNAPSHOTTING ################################
#
# Save the DB on disk:
#
# save <seconds> <changes>
#
# Will save the DB if both the given number of seconds and the given
# number of write operations against the DB occurred.
#
# In the example below the behaviour will be to save:
# after 900 sec (15 min) if at least 1 key changed
# after 300 sec (5 min) if at least 10 keys changed
# after 60 sec if at least 10000 keys changed
#
# Note: you can disable saving completely by commenting out all "save" lines.
#
# It is also possible to remove all the previously configured save
# points by adding a save directive with a single empty string argument
# like in the following example:
#
# save ""
save 900 1
save 300 10
save 60 10000
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes
# Compress string objects using LZF when dump .rdb databases?
# For default that's set to 'yes' as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes
# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes
# The filename where to dump the DB
dbfilename dump.rdb
# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./
############################## APPEND ONLY MODE ###############################
# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.
appendonly no
# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".
# appendfsync always
appendfsync everysec
# appendfsync no
# When the AOF fsync policy is set to always or everysec, and a background
# saving process (a background save or AOF log background rewriting) is
# performing a lot of I/O against the disk, in some Linux configurations
# Redis may block too long on the fsync() call. Note that there is no fix for
# this currently, as even performing fsync in a different thread will block
# our synchronous write(2) call.
#
# In order to mitigate this problem it's possible to use the following option
# that will prevent fsync() from being called in the main process while a
# BGSAVE or BGREWRITEAOF is in progress.
#
# This means that while another child is saving, the durability of Redis is
# the same as "appendfsync none". In practical terms, this means that it is
# possible to lose up to 30 seconds of log in the worst scenario (with the
# default Linux settings).
#
# If you have latency problems turn this to "yes". Otherwise leave it as
# "no" that is the safest pick from the point of view of durability.
no-appendfsync-on-rewrite no
# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# An AOF file may be found to be truncated at the end during the Redis
# startup process, when the AOF data gets loaded back into memory.
# This may happen when the system where Redis is running
# crashes, especially when an ext4 filesystem is mounted without the
# data=ordered option (however this can't happen when Redis itself
# crashes or aborts but the operating system still works correctly).
#
# Redis can either exit with an error when this happens, or load as much
# data as possible (the default now) and start if the AOF file is found
# to be truncated at the end. The following option controls this behavior.
#
# If aof-load-truncated is set to yes, a truncated AOF file is loaded and
# the Redis server starts emitting a log to inform the user of the event.
# Otherwise if the option is set to no, the server aborts with an error
# and refuses to start. When the option is set to no, the user requires
# to fix the AOF file using the "redis-check-aof" utility before to restart
# the server.
#
# Note that if the AOF file will be found to be corrupted in the middle
# the server will still exit with an error. This option only applies when
# Redis will try to read more data from the AOF file but not enough bytes
# will be found.
aof-load-truncated yes
# When rewriting the AOF file, Redis is able to use an RDB preamble in the
# AOF file for faster rewrites and recoveries. When this option is turned
# on the rewritten AOF file is composed of two different stanzas:
#
# [RDB file][AOF tail]
#
# When loading Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, and continues loading the AOF
# tail.
aof-use-rdb-preamble yes
RDB
RDB 是 Redis 默认的持久化方案。当满足一定条件的时候,会把当前内存中的数 据写入磁盘,生成一个快照文件 dump.rdb。Redis 重启会通过加载 dump.rdb 文件恢 复数据。
RDB触发时机
自动触发
a) 配置规则触发。 redis.conf, SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。 如果不需要 RDB 方案,注释 save 或者配置成空字符串""。
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)
save 300 10 # 400 秒内至少有 10 个 key 被修改
save 60 10000 # 60 秒内至少有 10000 个 key 被修改
注意上面的配置是不冲突的,只要满足任意一个都会触发
RDB 文件位置和目录:
# 文件路径,
dir ./
# 文件名称
dbfilename dump.rdb
# 是否是 LZF 压缩 rdb 文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
问题:为什么停止 Redis 服务的时候没有 save,重启数据还在?
RDB 还有两种触发方式:
b)shutdown 触发,保证服务器正常关闭。
c)flushall,RDB 文件是空的,没什么意义
手动触发
如果我们需要重启服务或者迁移数据,这个时候就需要手动触 RDB 快照保存。Redis 提供了两条命令:
- a)save save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果 内存中的数据比较多,会造成 Redis 长时间的阻塞。生产环境不建议使用这个命令。
为了解决这个问题,Redis 提供了第二种方式。
b)bgsave
执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请 求。
具体操作是 Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化 过程由子进程负责,完成后自动结束。它不会记录 fork 之后后续的命令。阻塞只发生在 fork 阶段,一般时间很短。
用 lastsave 命令可以查看最近一次成功生成快照的时间。
RDB 数据的恢复(演示)
1、shutdown 持久化
# 添加键值
redis> set k1 1
redis> set k2 2
redis> set k3 3
redis> set k4 4
redis> set k5 5
# 停服务器,触发 save
redis> shutdown
# 备份 dump.rdb 文件
cp dump.rdb dump.rdb.bak
# 启动服务器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
# 数据都在
redis> keys *
2、模拟数据丢失
# 模拟数据丢失,触发 save
redis> flushall
# 停服务器
redis> shutdown
# 启动服务器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
# 啥都没有
redis> keys *
3、通过备份文件恢复数据
# 停服务器
redis> shutdown
# 重命名备份文件
mv dump.rdb.bak dump.rdb
# 启动服务器
/usr/local/soft/redis-5.0.5/src/redis-server /usr/local/soft/redis-5.0.5/redis.conf
# 查看数据
redis> keys *
RDB 文件的优势和劣势
一、优势
1.RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据 集。这种文件非常适合用于进行备份和灾难恢复。
2.生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主 进程不需要进行任何磁盘 IO 操作。
3.RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
二、劣势
1、RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要 执行 fork 操作创建子进程,频繁执行成本过高。
2、在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后 一次快照之后的所有修改(数据有丢失)。 如果数据相对来说比较重要,希望将损失降到最小,则可以使用 AOF 方式进行持久 化。
AOF
Append Only File
AOF:Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。
Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复 工作。
AOF配置 redis.conf
# 开关 Redis 默认只开启 RDB 持久化,开启 AOF 需要修改为 yes
appendonly no
# 文件名 路径也是通过 dir 参数配置 config get dir
appendfilename "appendonly.aof
数据都是实时持久化到磁盘吗?
由于操作系统的缓存机制,AOF 数据并没有真正地写入硬盘,而是进入了系统的硬
盘缓存。根据appendfsync
配置的AOF持久化策略
AOF 持久化策略(硬盘缓存到磁盘),默认 everysec
- no 表示不执行 fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;
- always 表示每次写入都执行 fsync,以保证数据同步到磁盘,效率很低;
- everysec 表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。通常选择 everysec , 兼顾安全性和效率。
问题:文件越来越大,怎么办?aof文件重写
由于 AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进 行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间 越长。
例如 set k1 666,执行 1000 次,结果都是 k1=666。
为了解决这个问题,Redis 新增了重写机制,当 AOF 文件的大小超过所设定的阈值 时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集
可以使用命令 bgrewriteaof 来重写。
AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对, 然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原 来的 AOF 文件。
# 重写触发机制
# 默认值为 100。aof 自动重写配置,当目前 aof 文件大小超过上一次重写的 aof 文件大小的
# 百分之多少进行重写,即当 aof 文件增长到一定大小的时候,
# Redis 能够调用 bgrewriteaof 对日志文件进行重写。当前 AOF 文件大小是上次日志重写
# 得到 AOF 文件大小的二倍(设 置为 100)时,自动启动新的日志重写过程。
auto-aof-rewrite-percentage 100
# 默认 64M。设置允许重写的最小 aof 文件大小,
# 避免了达到约定百分比但尺寸仍然很小的 情况还要重写。
auto-aof-rewrite-min-size 64mb
重写过程中,AOF 文件被更改了怎么办?
no-appendfsync-on-rewrite
在 aof 重写或者写入 rdb 文件的时候,会执行大量 IO,此时对于 everysec 和 always 的 aof 模式来说,执行 fsync 会造成阻塞过长时间,no-appendfsync-on-rewrite 字段设置为默认设 置为 no。如果对延迟要求很高的应用,这个字段可以设置为 yes,否则还是设置为 no,这 样对持久化特性来说这是更安全的选择。设置为 yes 表示 rewrite 期间对新写操作不 fsync, 暂时存在内存中,等 rewrite 完成后再写入,
默认为 no,建议修改为 yes。Linux 的默认 fsync 策略是 30 秒。可能丢失 30 秒数据。
aof-load-truncated
aof 文件可能在尾部是不完整的,当 redis 启动的时候,aof 文件的数据被载入内存。重启 可能发生在 redis 所在的主机操作系统宕机后,尤其在 ext4 文件系统没有加上 data=ordered 选项,出现这种现象。redis 宕机或者异常终止不会造成尾部不完整现象,可以选择让 redis 退出,或者导入尽可能多的数据。如果选择的是 yes,当截断的 aof 文件被导入的时候, 会自动发布一个 log 给客户端然后 load。如果是 no,用户必须手动 redis-check-aof 修复 AOF 文件才可以。默认值为 yes。
AOF 数据恢复: 重启 Redis 之后就会进行 AOF 文件的恢复
AOF 优势与劣势
优点:
1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步 一次,Redis 最多也就丢失 1 秒的数据而已。
缺点:
缺点: 1、对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB 存的是数据快照)。 2、虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较 高的性能。在高并发的情况下,RDB 比 AOF 具好更好的性能保证
RDB和AOF比较
那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?
如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要 比 AOF 恢复的速度要快。
否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而 是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始 的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
事件
Redis服务器是一个事件驱动程序,需要处理以下两类事件
- 文件事件(file event): Redis服务器通过套接字和客户端(或其他redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象
- 时间事件(time event): 如
serverCron函数
需要给定时间点执行,时间事件就是服务器对这类定时器操作的抽象
/* Types and data structures */
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
typedef void aeBeforeSleepProc(struct aeEventLoop *eventLoop);
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent;
/* A fired event */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
/* State of an event based program */
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
time_t lastTime; /* Used to detect system clock skew */
// 文件事件数组,存储已经注册的文件事件
aeFileEvent *events; /* Registered events */
// 存储被触发的文件事件
aeFiredEvent *fired; /* Fired events */
// Redis有多个定时任务,因此理论上应该有多个时间事件,多个时间事件形成链表
// timeEventHead为时间事件链表头节点
aeTimeEvent *timeEventHead;
// stop 标识事件循环是否结束
int stop;
void *apidata; /* This is used for polling API specific data */
// Redis服务器需要阻塞等待文件事件的发生,进程阻塞之前会调用beforesleep函数
// 进程因为某种原因被唤醒之后调用afersleep函数
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
aeEventLoop *aeCreateEventLoop(int setsize);
void aeDeleteEventLoop(aeEventLoop *eventLoop);
void aeStop(aeEventLoop *eventLoop);
server.c
中main
函数调用 ae.c/aeMain
// server.c 中 main函数片段
aeSetBeforeSleepProc(server.el,beforeSleep);
aeSetAfterSleepProc(server.el,afterSleep);
aeMain(server.el);
aeDeleteEventLoop(server.el);
// ae.c 中
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 事件处理主函数,分文件事件和时间事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
...
shortest = aeSearchNearestTimer(eventLoop);
....
/* How many milliseconds we need to wait for the next
* time event to fire? */
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
...
// 阻塞等待文件事件的发生
numevents = aeApiPoll(eventLoop, tvp);
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
// 处理文件事件,即根据类型执行rfileProc或wfileProc
}
// 处理时间事件
processed += processTimeEvents(eventLoop);
...
}
文件事件
客户端在操作的时候,会产生具有不同事件类型的 socket。在服务端,I/O 多路复 用程序(I/O Multiplexing Module)会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。
Redis基Reactor模式
开发的网络事件处理器:称文件事件处理器file event handler
- I/O多路复用程序:负责监听多个套接字,并向文件事件分派器传送套接字
- 文件事件分派器: 接受I/O多路复用程序传来的套接字,并根据套接字产生的事件类型,调用相应的事件处理器
虽然文件事件处理器以单线程方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样单线程方式运行的模块对接,这样保持了Redis内部单线程设计的简单性
Redis的I/O多路复用实现
Redis 的多路复用, 提供了 select, epoll, evport, kqueue 几种选择,在编译的时 候来选择一种。源码 ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
evport 是 Solaris 系统内核提供支持的;
epoll 是 LINUX 系统内核提供支持的;
kqueue 是 Mac 系统提供支持的;
select 是 POSIX 提供的,一般的操作系统都有支撑(保底方案);
源码 ae_epoll.c、ae_select.c、ae_kqueue.c、ae_evport.c
事件类型
ae.h/AE_READABLE
和ae.h/AE_WRITABLE
- 当套接字变的可读(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字执行connectc操作),套接字产生
AE_READABLE
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
// 指向可读事件处理函数
aeFileProc *rfileProc;
// 指向写事件处理函数
aeFileProc *wfileProc;
// 指向对应的客户端对象
void *clientData;
} aeFileEvent;
API
// 文件事件ae.h 和 ae.capi
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
int aeGetFileEvents(aeEventLoop *eventLoop, int fd);
ae.c/aeWait()
ae.c/aeApiPoll()
// 文件事件分派器 先调用aeApiPoll函数来等待事件产生,然后遍历所有已经产生的事件
// 并调用相应的事件处理器处理这些事件
ae.c/aeProcessEvents()
// 返回I/O多路复用程序底层是那个函数库如"epool","select"
ac.c/aeGetApiName()
/*************文件事件处理器**/
// Redis连接应答处理器 抽象封装accept函数
networking.c/acceptTcpHandler()
// 命令请求处理器
networking.c/readQueryFromClient()
// 命令回复处理器
networking.c/sendReplyToClient()
时间事件
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *prev;
struct aeTimeEvent *next;
} aeTimeEvent;
redisServer/ aeEventLoop *el;
// server.c/initServer
/* Create the timer callback, this is our way to process many background
* operations incrementally, like clients timeout, eviction of unaccessed
* expired keys and so forth. */
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
Redis的时间时间分两类
- 定时事件
- 周期事件
一个时间事件主要有id
,when
和timeProc
时间事件处理器
Redis服务器将所有的时间时间都放在一个无序链表中,每当时间事件执行时,就遍历整个链表,找到已经到达的时间事件,并调用相应的事件处理器
API
// 创建新的时间事件到服务器
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc);
// 接受一个时间事件ID作为参数,然后从服务器中删除该ID对应的时间事件
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
// 时间事件的执行器
int aeProcessEvents(aeEventLoop *eventLoop, int flags);
serverCron函数
持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期、稳定地运行,这些定期操作由redis.c/serverCron
函数负责执行
- 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等
- 清理数据库中的过期键值对
- 关闭和清理连接失效的客户端
- 尝试进行AOF或RDB持久化操作
- 如果服务器是主服务器,那么对从服务器进行定期同步
- 如果处于集群模式,对集群进行定期同步和连接测试
- serverCron 每隔100毫秒运行一次
Redis高效之道
基准测试benchmarks
# 在本地虚拟机运行
[root@manager bin]# redis-benchmark -t set,lpush -n 100000 -q
SET: 77279.75 requests per second # 每秒处理7万多次set请求
LPUSH: 76335.88 requests per second # 每秒处理7万多此lpush请求
# 每秒70972次lua脚本调用
[root@manager bin]# redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
script load redis.call('set','foo','bar'): 70972.32 requests per second
根据官方的数据,Redis 的 QPS 可以达到 10 万左右(每秒请求数)
Redis 为什么这么快?
总结:1)纯内存结构、2)单线程、3)多路复用
- 内存
KV 结构的内存数据库,时间复杂度 O(1)
- 单线程
单线程有什么好处呢?
- 1、没有创建线程、销毁线程带来的消耗
- 2、避免了上线文切换导致的 CPU 消耗
- 3、避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等
- 异步非阻塞
异步非阻塞 I/O,多路复用处理并发连接。
因为单线程已经够用了,CPU 不是 redis 的瓶颈。Redis 的瓶颈最有可能是机器内存 或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单 线程的方案了