学Redis的第二天

数据库

数据库保存在一个db数组中

struct redisServer{
redisDb *db;
int dbnum;
}

然后通过dbnum属性来决定服务器启动时将会初始化出多少个数据库。
在服务器内部 客户端的结构中也有着对应的db指针指向服务器的目标数据库,在编程中有可能会遗忘正在操作哪个数据库。因此最好在执行命令前先使用SELECT指令明确指定到对应数据库。
Redis是一个键值对数据库服务器。每个数据库都由redisDb结构表示

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    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 */
    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;
  • dict:字典实现的并且保存所有键值对。其KEY都是字符串对象、VALUE可以是任意对象。
  • expires:该字典保存了所有键的过期时间,其KEY是dict中存在的KEY、VALUE则是UNIX的过期时间戳。当执行设置过期时间或生存时间指令时,会同时在本字典中添加对应键值对。
  • 程序检查键是否过期通过以下步骤
    1. 检查是否存在过期字典中。
    2. 存在取得其过期时间。
    3. 与当前时间进行对比并判断。
    4. 当然也可以使用TTL与PTTL指令判断剩余时间的值来进行判断。访问字典较为快索引Redis一般使用第一种方式进行判断。
  • 删除过期键大体上如何删除呢?
  1. 如果使用定时删除那么就在每个键位过期时实时删除但是会浪费CPU资源。
  2. 那么采用惰性删除呢?用到的时候才进行判断并删除。但是会浪费内存。
  3. 使用定期删除,选定时间间隔检查过期键。其实现难点是确定时长与频率。
  • 在Redis中采用的是惰性删除与定期删除相结合的模式进行删除过期键处理
惰性删除

其采用expireIfNeeded函数实现

int expireIfNeeded(redisDb *db, robj *key) {
    // 取出键的过期时间
    mstime_t when = getExpire(db,key);
    mstime_t now;
    // 没有过期时间
    if (when < 0) return 0; /* No expire for this key */
    /* Don't expire anything while loading. It will be done later. */
    // 如果服务器正在进行载入,那么不进行任何过期检查
    if (server.loading) return 0;
    /* If we are in the context of a Lua script, we claim that time is
     * blocked to when the Lua script started. This way a key can expire
     * only the first time it is accessed and not in the middle of the
     * script execution, making propagation to slaves / AOF consistent.
     * See issue #1525 on Github for more information. */
   
    now = server.lua_caller ? server.lua_time_start : mstime();
    
    /* If we are running in the context of a slave, return ASAP:
     * the slave key expiration is controlled by the master that will
     * send us synthesized DEL operations for expired keys.
     *
     * Still we try to return the right information to the caller, 
     * that is, 0 if we think the key should be still valid, 1 if
     * we think the key is expired at this time. */
    // 当服务器运行在 replication 模式时
    // 附属节点并不主动删除 key
    // 它只返回一个逻辑上正确的返回值
    // 真正的删除操作要等待主节点发来删除命令时才执行
    // 从而保证数据的同步
    if (server.masterhost != NULL) return now > when;
    // 运行到这里,表示键带有过期时间,并且服务器为主节点
    /* Return when this key has not expired */
    // 如果未过期,返回 0
    if (now <= when) return 0;
    /* Delete the key */
    server.stat_expiredkeys++;
    // 向 AOF 文件和附属节点传播过期信息
    propagateExpire(db,key);
    // 发送事件通知
    notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
        "expired",key,db->id);
    // 将过期键从数据库中删除
    return dbDelete(db,key);
}

其实现原理基本上来说是对键值对进行操作之前进行过期时间进行判断,如果已过期遍返回空并删除对应键值对,否则继续执行命令。

定期删除策略

其调用了activeExpireCycle函数实现。而本函数在进行服务器周期性操作时就会被调用。也就是调用serverCron函数时也会调用activeExpireCycle函数

void activeExpireCycle(int type) {
    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    // 静态变量,用来累积函数连续执行时的数据
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */
    unsigned int j, iteration = 0;
    // 默认每次处理的数据库数量
    unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;
    // 函数开始的时间
    long long start = ustime(), timelimit;
    // 快速模式
    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exited
         * for time limt. Also don't repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        // 如果上次函数没有触发 timelimit_exit ,那么不执行处理
        if (!timelimit_exit) return;
        // 如果距离上次执行未够一定时间,那么不执行处理
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        // 运行到这里,说明执行快速处理,记录当前时间
        last_fast_cycle = start;
    }
    /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,
     * 除非:
     * 1) Don't test more DBs than we have.
     *    当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don't want
     * expired keys to use memory for too much time. 
     *     如果上次处理遇到了时间上限,那么这次需要对所有数据库进行扫描,
     *     这可以避免过多的过期键占用空间
     */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time
     * per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    // 函数处理的微秒时间上限
    // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    // 如果是运行在快速模式之下
    // 那么最多只能运行 FAST_DURATION 微秒 
    // 默认值为 1000 (微秒)
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

    // 遍历数据库
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        // 指向要处理的数据库
        redisDb *db = server.db+(current_db % server.dbnum);
        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出
        // 那么下次会直接从下个 DB 开始处理
        current_db++;
        /* Continue to expire if at the end of the cycle more than 25%
         * of the keys were expired. */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            /* If there is nothing to expire try next DB ASAP. */
            // 获取数据库中带过期时间的键的数量
            // 如果该数量为 0 ,直接跳过这个数据库
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            // 获取数据库中键值对的数量
            slots = dictSlots(db->expires);
            // 当前时间
            now = mstime();

            /* When there are less than 1% filled slots getting random
             * keys is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)
            // 跳过,等待字典收缩程序运行
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. 
             *
             * 样本计数器
             */
            // 已处理过期键计数器
            expired = 0;
            // 键的总 TTL 计数器
            ttl_sum = 0;
            // 总共处理的键计数器
            ttl_samples = 0;
            // 每次最多只能检查 LOOKUPS_PER_LOOP 个键
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
            // 开始遍历数据库
            while (num--) {
                dictEntry *de;
                long long ttl;
                // 从 expires 中随机取出一个带过期时间的键
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                // 计算 TTL
                ttl = dictGetSignedIntegerVal(de)-now;
                // 如果键已经过期,那么删除它,并将 expired 计数器增一
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl < 0) ttl = 0;
                // 累积键的 TTL
                ttl_sum += ttl;
                // 累积处理键的个数
                ttl_samples++;
            }

            /* Update the average TTL stats for this database. */
            // 为这个数据库更新平均 TTL 统计数据
            if (ttl_samples) {
                // 计算当前平均值
                long long avg_ttl = ttl_sum/ttl_samples;
                                // 如果这是第一次设置数据库平均 TTL ,那么进行初始化
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                /* Smooth the value averaging with the previous one. */
                // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值
                db->avg_ttl = (db->avg_ttl+avg_ttl)/2;
            }
            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            // 我们不能用太长时间处理过期键,
            // 所以这个函数执行一定时间之后就要返回

            // 更新遍历次数
            iteration++;
            // 每遍历 16 次执行一次
            if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */
                (ustime()-start) > timelimit)
            {
                // 如果遍历次数正好是 16 的倍数
                // 并且遍历的时间超过了 timelimit
                // 那么断开 timelimit_exit
                timelimit_exit = 1;
            }
            // 已经超时了,返回
            if (timelimit_exit) return;
            /* We don't repeat the cycle if there are less than 25% of keys
             * found expired in the current DB. */
            // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %
            // 那么不再遍历
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

从代码中可以分析出其工作模式大致如下

  1. 运行函数时从一定数据库中取出一定数量的随机键检查并删除。
  2. 并将现在已检查的数据库数量保存在current_db中以便下次从下个数据库开始检查
  3. 当确认全部数据库检查完成便将current_db值置0。

持久化对过期键处理

作为数据库Redis也需要持久化的操作。而有以下两种持久化方式RDB(类似快照)以及AOF(日志文件)

RDB:

当服务器启动时,如果服务器开启了RDB功能,服务器便会将RDB文件载入。这其中有涉及到主从服务器的区别。

  • 主服务器:载入时会进行键值对检查,忽视过期键。
  • 从服务器:不检查,等待主服务器同步清理
AOF:

当服务器启动AOF模式时,如果键已过期但是没被删除时,将依旧保存在AOF文件中直至被删除之后程序会向AOF文件追加一天DEL命令显示记录删除该键。当然服务器启动时其步骤与RDB类似。

复制模式对过期键处理

emmm其实也可以叫主从服务器。从服务器的过期键由主服务器控制。也就是说如果有一个键已过期,当你访问从服务器时会正常执行命令,但是当你访问主服务器时,该键位会被删除,与此同时主服务器对从服务器发送一条DEL命令删除该键。

数据库通知

该功能是Redis2.8版本新增加的功能可以通过订阅来获知数据库键的变化。
SUBSCRIBE :订阅命令
__keyspace@0 __: :0号数据库的键空间
keyname :关于keyname的操作
通过该功能可以知道某个键执行过什么命令
当然keyname换成命令关键字可以知道该命令被什么键执行了。
对其返回的通知类型可以在服务器的notify-keyspace-events配置中修改

############################# EVENT NOTIFICATION ##############################
# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#  K     Keyspace events, published with __keyspace@<db>__ prefix.
#  E     Keyevent events, published with __keyevent@<db>__ prefix.
#  g     Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
#  $     String commands
#  l     List commands
#  s     Set commands
#  h     Hash commands
#  z     Sorted set commands
#  x     Expired events (events generated every time a key expires)
#  e     Evicted events (events generated when a key is evicted for maxmemory)
#  A     Alias for g$lshzxe, so that the "AKE" string means all the events.
#  The "notify-keyspace-events" takes as argument a string that is composed
#  of zero or multiple characters. The empty string means that notifications
#  are disabled.
#  Example: to enable list and generic events, from the point of view of the
#           event name, use:
#  notify-keyspace-events Elg
#  Example 2: to get the stream of the expired keys subscribing to channel
#             name __keyevent@0__:expired use:
#  notify-keyspace-events Ex
#  By default all notifications are disabled because most users don't need
#  this feature and the feature has some overhead. Note that if you don't
#  specify at least one of K or E, no events will be delivered.
notify-keyspace-events ""

RDB

RDB是Redis数据库持久化的一种方式其可以手动执行持久化也可以通过配置定期执行,其生成的文件是经过压缩的二进制文件。当然如果同时开启了AOF的配置,那么无论是保存亦或是加载都是优先使用AOF的形式,手动持久化使用的是SAVE与BGSAVE命令。

  • SAVE:本命令会阻塞运行直至命令完成。

  • BGSAVE:服务器会派生出个子进程,并由其执行持久化操作
    通过设置Redis服务器配置的save选项可以定期执行BGSAVE命令其命令是多少秒内进行了多少次修改才会执行。
    在BGSAVE执行时会重置dirty计数器,并覆盖lastsave属性

    • dirty计数器:保存从上次执行SAVE或BGSAVE之后对数据库进行的修改次数
    • lastsave:保存最新一次执行SAVE或BGSAVE的时间戳
    • serverCron函数:其会检查dirty与lastsave的值是否满足save选项里面的数值,如果满足便调用BGSAVE命令,该函数默认100毫秒调用执行一次。
    • RDB文件结构
      < REDIS >< db_version >< databases >< EOF >< CHECK_SUM >
      • REDIS:就包含’r’ ‘e’ ‘d’ ‘i’ 's’五个字符其目的是检查是否是RDB文件
      • db_version :标记版本号、4个字节。
      • databases :包含数据库中的内容。数组形式存在。
        • 其子结构为< datebase 0 > < datebase 1 >< datebase 3 >等如数据库无数据即为空例如数据库2。
        • 每个数据库结构为< SELECTDB >< db_number >< key_value_pairs >
        • SELECTDB :表示接下来是个数据库号码
        • db_number :数据库号码
        • key_value_pairs :键值对
          • 其结构为< TYPE >< KEY >< VALUE >
          • 如带过期时间的话在前面补充 < EXPIRETIME_MS>< ms >
          • 分别为过期时间标识、时间戳、值类型、键、值。
      • EOF:表示正文结束也就是键值对加载完毕,1个字节。
      • CHECK_SUM :校验和,判断RDB文件是否有错误或损坏。8个字节
  • 通过od -c XXX.rdb 可以查看RDB文件

AOF

AOF持久化是通过保存服务器的写命令来记录数据库状态的。其实现可以分为追加、文件写入、文件同步三个步骤。
当命令执行之后,服务器会以协议格式将对应命令追加到aof_buf缓冲区末尾。当然Redis是一个事件循环的进程,在每个事件循环周期结束之前会调用flushAppendOnlyFile函数判断是否将缓冲区的内容写入AOF文件中。该函数可以通过服务器配置中appendfsync选项来决定:默认值为everysec

# appendfsync always
appendfsync everysec
# appendfsync no
  • always:所有内容写入并同步到AOF文件。效率最慢同时最安全
  • everysec:所有内容写入并判断上次同步时间,如果超过1秒便使用子线程再次进行同步。最多丢失1S数据
  • no:所有内容写入同步时间由操作系统决定。效率快但是最不安全。
    读取文件还原步骤:
  1. 创建伪客户端
  2. 分析读取AOF文件(循
  3. 伪客户端执行命令 环)
  • AOF随着运行时间的流逝。其文件会越来越大所以需要进行优化。
    其实现步骤:
  1. 读写当期数据库状态
  2. 然后将可合并的写命令合并。
  3. 将多命令合并为一条命令
  4. 将缓存区内容通过子线程写入、与此同时将新的命令放入专门的重写缓冲区与普通缓冲区
  5. 当子线程执行完毕后发送信号给主线程
  6. 接收信号将重写缓冲区写入AOF文件(此时造成阻塞)
  7. 对新AOF文件命名覆盖现有AOF文件

事件

Redis中有两类事件文件事件和时间事件

  • 文件事件:客户端与服务器端的通信会产生相应的文件事件,服务器通过监听处理这些事件完成通信操作。
  • 时间事件:内部某些函数需要在给定时间点执行
文件事件

Redis基于Reactor模式开发了网络事物处理器,被称为文件事件处理器。
其主要组成部分为

  • 套接字:每当一个套接字准备好执行链接应答、写入、读取、关闭等操作时就会产生一个文件事件。
  • I/O多路复用程序:负责监听套接字并向文件事物分派器分配已有事件的套接字,通过队列进行分配
  1. 其包装了常见的多路复用函数库例如:select、epoll、evport、kquue等
  2. 其监听套接字的AE_READABLE以及AE_WRITABLE事件并且优先处理AE_READABLE事件
  • 文件事件分派器:根据类型分配至事件处理器
  • 事件处理器:执行事件
时间事件
  • 定时事件:指定时间后执行一次
  • 周期性事件:每隔一段时间运行一次
  • 事件主要有以下部分组成
  1. id:唯一标识号
  2. when:时间戳
  3. timeProc:时间事件处理器,其返回值决定是为定时时间或者周期性事件,返回AE_NOMORE便为定时事件。
  • 时间事件放置于无序链表中,每当其运行时遍历整个链表并查找已到达的事件与调用。
  • 例子:serverCron()
  • 工作内容有:
    1. 更新服务器统计信息。
    2. 清理过期键值对
    3. 关闭清理链接失效的客户端
    4. 尝试进行持久化操作
    5. 定期同步
    6. 集群模式中定期同步与连接测试
    7. 默认每秒运行10次
      TIPS:可修改redis.conf中hz选项来调整频率
# Redis calls an internal function to perform many background tasks, like
# closing connections of clients in timeot, purging expired keys that are
# never requested, and so forth.
#
# Not all tasks are perforemd with the same frequency, but Redis checks for
# tasks to perform according to the specified "hz" value.
#
# By default "hz" is set to 10. Raising the value will use more CPU when
# Redis is idle, but at the same time will make Redis more responsive when
# there are many keys expiring at the same time, and timeouts may be
# handled with more precision.
#
# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

客户端!

Redis是典型的一对多服务器程序。通过使用I/O多路复用技术实现单线程处理命令请求每个客户端都有相应的结构。

typedef struct redisClient {
    // 套接字描述符
    int fd;
    // 当前正在使用的数据库
    redisDb *db;
    // 当前正在使用的数据库的 id (号码)
    int dictid;
    // 客户端的名字
    robj *name;             /* As set by CLIENT SETNAME */
    // 查询缓冲区
    sds querybuf;
    // 查询缓冲区长度峰值
    size_t querybuf_peak;   /* Recent (100ms or more) peak of querybuf size */
    // 参数数量
    int argc;
    // 参数对象数组
    robj **argv;
    // 记录被客户端执行的命令
    struct redisCommand *cmd, *lastcmd;
    // 请求的类型:内联命令还是多条命令
    int reqtype;
    // 剩余未读取的命令内容数量
    int multibulklen;       /* number of multi bulk arguments left to read */
    // 命令内容的长度
    long bulklen;           /* length of bulk argument in multi bulk request */
    // 回复链表
    list *reply;
    // 回复链表中对象的总大小
    unsigned long reply_bytes; /* Tot bytes of objects in reply list */
    // 已发送字节,处理 short write 用
    int sentlen;            /* Amount of bytes already sent in the current
                               buffer or object being sent. */
    // 创建客户端的时间
    time_t ctime;           /* Client creation time */
    // 客户端最后一次和服务器互动的时间
    time_t lastinteraction; /* time of the last interaction, used for timeout */
    // 客户端的输出缓冲区超过软性限制的时间
    time_t obuf_soft_limit_reached_time;
    // 客户端状态标志
    int flags;              /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
    // 当 server.requirepass 不为 NULL 时
    // 代表认证的状态
    // 0 代表未认证, 1 代表已认证
    int authenticated;      /* when requirepass is non-NULL */
    // 复制状态
    int replstate;          /* replication state if this is a slave */
    // 用于保存主服务器传来的 RDB 文件的文件描述符
    int repldbfd;           /* replication DB file descriptor */
    // 读取主服务器传来的 RDB 文件的偏移量
    off_t repldboff;        /* replication DB file offset */
    // 主服务器传来的 RDB 文件的大小
    off_t repldbsize;       /* replication DB file size */
        sds replpreamble;       /* replication DB preamble. */
    // 主服务器的复制偏移量
    long long reploff;      /* replication offset if this is our master */
    // 从服务器最后一次发送 REPLCONF ACK 时的偏移量
    long long repl_ack_off; /* replication ack offset, if this is a slave */
    // 从服务器最后一次发送 REPLCONF ACK 的时间
    long long repl_ack_time;/* replication ack time, if this is a slave */
    // 主服务器的 master run ID
    // 保存在客户端,用于执行部分重同步
    char replrunid[REDIS_RUN_ID_SIZE+1]; /* master run id if this is a master */
    // 从服务器的监听端口号
    int slave_listening_port; /* As configured with: SLAVECONF listening-port */
    // 事务状态
    multiState mstate;      /* MULTI/EXEC state */
    // 阻塞类型
    int btype;              /* Type of blocking op if REDIS_BLOCKED. */
    // 阻塞状态
    blockingState bpop;     /* blocking state */
    // 最后被写入的全局复制偏移量
    long long woff;         /* Last write global replication offset. */
    // 被监视的键
    list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */
    // 这个字典记录了客户端所有订阅的频道
    // 键为频道名字,值为 NULL
    // 也即是,一个频道的集合
    dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    // 链表,包含多个 pubsubPattern 结构
    // 记录了所有订阅频道的客户端的信息
    // 新 pubsubPattern 结构总是被添加到表尾
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
    sds peerid;             /* Cached peer ID. */
    /* Response buffer */
    // 回复偏移量
    int bufpos;
    // 回复缓冲区
    char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;

并且由redisServer结构可得知客户端在服务器中是以链表形式存在。

  • fd:套接字描述符根据客户端类型的不同值也不同,-1代表为伪客户端有且仅在载入AOF以及Lua时使用。普通客户端的值均为大于-1的整数

  • name:可以使用 CLIENT setname 来为客户端命名,使得客户端更为清晰

  • flags:标志记录客户端的角色以及所处状态,其可以是单个标志,也可以是多个标志的二进制或(以|为分隔符)其值可能为:
    1. REDIS_MASTER:表示为主服务器
    2. REDIS_SLAVE:表示为从服务器
    3. REDIS_PRE_PSYNC:表示为版本低于2.8的从服务器并且只能在REDIS_SLAVE打开时使用
    4. REDIS_LUA_CLIENT:表示专门处理LUA脚本的伪客户端
    5. REDIS_MONITOR:表示正在执行MONITOR命令
    6. REDIS_UNIX_SOCKET:表示正在使用UNIX套接字连接客户端
    7. REDIS_BLOCKED:表示正被BRPOP/BLPOP等命令阻塞
    8. REDIS_UNBLOCKED:表示不再阻塞,只能在REDIS_BLOCKED打开时使用
    9. REDIS_MULTI:正在执行事务
    10. REDIS_DIRTY_CAS:事务使用WATCH监视数据库键被修改,表示事务出现错误安全性受到破坏。只能在REDIS_MULTI打开时使用。
    11. REDIS_DIRTY_EXEC:表示事务出现错误安全性受到破坏只能在REDIS_MULTI打开时使用。
    12. REDIS_CLOSE_ASAP:客户端输出缓冲区超出范围,将在下次循环时关闭客户端
    13. REDIS_CLOSE_AFTER_REPLY:表示该客户端被执行CLIENT KILL命令等可能被强制关闭客户端行为。
    14. REDIS_ASKING:表示客户端向集群节点发送ASKING命令
    15. REDIS_FORCE_AOF:表示正在强制执行AOF持久化。
    16. REDIS_FORCE_REPL:表示主服务器将当前命令复制给所有从服务器,其主要使用于会产生副作用的命令,目的是为了保证影响一致。
    - querbuf:输入缓冲区,保存命令的SDS值。
    - argv/argc:命令与命令参数argv是数组 其首项为执行的命令后项为参数,argc记录argv的长度。
    - authenticated:身份验证,判断其值为0或1,0则代表没通过身份验证。
    - ctime:记录连接时间。
    - lastinteraction:记录最后一次互动时间,可以计算空转时间。
    - obuf_soft_limit_reached_time:记录输出缓冲区第一次到达软性限制的时间。

    创建客户端
    1. 调用connect函数连接到服务器
    2. 调用连接事件处理器
    3. 添加至客户端链表末尾
    关闭客户端

    原因如下:

    1. 正常退出
    2. 发送了不符合协议格式的命令请求
    3. 成为CLIENT KILL命令目标
    4. 空转时长超限(正在被阻塞、执行订阅命令、或本身是主从服务器则不会被关闭)
    5. 命令请求大小超过输入缓冲区大小(默认1GB)
    6. 命令回复大小超过输出缓冲区大小
    7. 命令回复大小超过输出软性限制一段时间
      • client-output-buffer-limit:可以为普通客户端从服务器客户端执行发布与订阅客户端分别设置不同的限制。其格式如下:
        client-output-buffer-limit
        < class> < hard limit> < soft limit>< soft seconds>
        |-normal ~ |-Xmb~~~~~ |-Xmb ~~~ |-X
        |-slave ~~ |-Xmb ~~~~~ |-Xmb~~~~|-X
        |-pubsub ~ |-Xmb ~~~~ |-Xmb ~~~ |-X

服务器!

命令请求从发送到获得回复的过程中有以下操作

  1. 客户端发送命令请求
  2. 服务器接收命令请求
  3. 服务器回复命令请求
  4. 客户端接收回复并回显
  • 发送命令请求:将命令转换成协议格式然后链接到套接字并发送
  • 接受命令请求:读取套接字中的协议格式命令请求并保存到输入缓冲区,对命令请求进行分析,提取命令参数、以及命令参数个数,分别保存在客户端中的argv与argc中。调用命令执行器执行命令,
    • 命令执行器:根据argv[0]参数,在命令表中查找对应命令并保存与客户端状态cmd属性中
      • 命令表是一个字典,键是命令名字,值是redisCommand结构
struct redisCommand {
    // 命令名字
    char *name;
    // 实现函数
    redisCommandProc *proc;
    // 参数个数
    int arity;
    // 字符串表示的 FLAG
    char *sflags; /* Flags as string representation, one char per flag. */
    // 实际 FLAG
    int flags;    /* The actual flags, obtained from the 'sflags' field. */
    /* Use a function to determine keys arguments in a command line.
     * Used for Redis Cluster redirect. */
    // 从命令中判断命令的键参数。在 Redis 集群转向时使用。
    redisGetKeysProc *getkeys_proc;
    /* What keys should be loaded in background when calling this command? */
    // 指定哪些参数是 key
    int firstkey; /* The first argument that's a key (0 = no keys) */
    int lastkey;  /* The last argument that's a key */
    int keystep;  /* The step between first and last key */
    // 统计信息
    // microseconds 记录了命令执行耗费的总毫微秒数
    // calls 是命令被执行的总次数
    long long microseconds, calls;
};
  • 命令执行器(续):服务器将命令实现函数、参数、参数个数收集其了。在此同时检查cmd是否指向NULL并做出处理,接着检查命令给定参数个数是否正确,检查客户端是否通过身份验证,检查内存情况,检查stop-writes-on-bgsave-error 等,检查是否订阅,检查是否数据载入,检查是否因为LUA脚本进入阻塞状态,检查是否执行事务,发送对应信息给监视器。之后可以进行真正的执行命令了。执行完后检查是否进入慢查询,进行持久化,传播给所有从服务器。
  • 回复客户端:命令回复保存在输出缓冲区中,当客户端套接字可写时将命令回复发送给客户端。
serverCron函数

本函数是为了管理服务器资源并保持服务器的自身良好运转的时间周期性事件,该事件默认100毫秒执行一次。Redis服务器中有许多功能需要获取系统当前时间,每次获取系统当前时间都需要进行一次系统调用,为了减少调用的执行次数,服务器状态中的unixtime和unixtime属性被用作当前时间的缓存,serverCron函数会以100毫秒一次的频率进行更新。
该函数会以每10秒一次的频率更新lrulock的值,其值是为了计算空转时间(INFO SEVER)可查看值。
severCron:(默认100毫秒一次)

  1. 更新unixtime和unixtime
  2. 更新lrulock的值(10秒次)
  3. 更新每秒执行命令次数(trackOperationsPerSecond函数)
  4. 更新服务器内存峰值记录(INFO memory)可查看
  5. 处理SIGTERM信号 接受到时将 shutdown_asap置一 进行持久化操作后关闭服务器
  6. 调用clientsCron函数检查客户端(空连时间,输入缓冲区界限)
  7. 调用databasesCron检查过期键
  8. 执行延迟的BGREWRITEAOF命令(通过aof_rewrite_scheduled记录)
  9. 检查持久化子进程是否执行
初始化服务器
  • 设置服务器运行ID
  • 设置服务器默认运行频率
  • 设置默认配置文件路径
  • 设置运行架构
  • 设置默认端口号
  • 设置持久化条件
  • 初始化LRU时钟
  • 创建命令表
  • 启动服务器时 可以通过给定配置参数或者指定配置文件来修改服务器中的默认配置。例如:修改端口号,数据库数量,持久化功能
  • 设置进程信号处理器
  • 创建共享对象
  • 打开服务器监听端口
  • 为severCron创建时间事件
  • 初始化后台I/O模块
  • 恢复数据(优先AOF RDB)

呼 写了三天终于写完单机部分的了。

命令作用
SELECT选择几号数据库
KEYS获得键
SET设置或者更新键值对
GET获得键值对
FLUSHDB清空键值对
RPUSH创建value是列表对象的键值对
HSET创建value是哈希表对象的键值对
DEL删除对应键值对
RANDOMKEY随机返回一个键
DBSIZE返回键值对的数量
EXISTS查找键是否存在
RENAME重新设置key
EXPIRE以秒为单位设置生存时间
PEXPIRE以毫秒为单位设置生存时间
EXPIREAT以秒为单位设置过期时间
PEXPIREAT以毫秒为单位设置过期时间
TTL以秒为单位返回以上时间
PTTL以毫秒为单位返回以上时间
PERSIST移除过期时间
SAVE创建RDB文件会阻塞
BGSAVE通过子进程创建RDB文件
FSYNC强制实时写入磁盘
FDATASYNC强制实时写入磁盘
BGREWRITEAOF重写AOF文件
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值