使用hash(String 可以做的事情,Hash 都可以做。)

简介

  Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。

  Redis的性能瓶颈并不在CPU上,而是在内存和网络上。

简称

BIO: background IO,是redis中运行的后台IO。

RDB: Redis DataBase

AOF:Append Only File

SDS: Simple Dynamic Strings

概念

  1. Redis 持久化
    1. RDB:简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上;
    2. AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。

同时使用:如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。(官方的建议)
没有数据持久化的需求:关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库(memcache)
RDB执行流程:
redis - 图1

  • Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。
    • bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。
  • 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令
  • 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令
  • 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换
  • 子进程发送信号给父进程表示完成,父进程更新统计信息

AOF重写内部运行原理:

  • 在重写即将开始之际,redis 会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
  • 与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外。
  • 当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中。
  • 当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中了。

基本理解

  1. 单线程
    Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
    • 文件事件:Redis服务器通过套接字(Socket)与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象;服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作,比如连接accept,read,write,close等;
    • 时间事件:Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象,比如过期键清理,服务状态统计等。

redis - 图2Redis将文件事件和时间事件进行抽象,时间轮询器会监听I/O事件表,一旦有文件事件就绪,Redis就会优先处理文件事件,接着处理时间事件。在上述所有事件处理上,Redis都是以单线程形式处理,所以说Redis是单线程的。
redis - 图3Redis基于Reactor模式开发了自己的I/O事件处理器,也就是文件事件处理器,Redis在I/O事件处理上,采用了I/O多路复用技术,同时监听多个套接字,并为套接字关联不同的事件处理函数,通过一个线程实现了多客户端并发处理。
因为这样的设计,在数据处理上避免了加锁操作,既使得实现上足够简洁,也保证了其高性能。

  1. Lazy Free机制
    Redis在处理客户端命令时是以单线程形式运行,而且处理速度很快,期间不会响应其他客户端请求,
    但若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,导致服务器卡住好几秒,对负载较高的缓存系统而言将会是个灾难。
    为了解决这个问题,在Redis 4.0版本引入了Lazy Free,将慢操作异步化,这也是在事件处理上向多线程迈进了一步。
    Redis最终实现上是将大键的删除操作异步化,采用非阻塞删除(对应命令UNLINK),大键的空间回收交由单独线程实现,主线程只做关系解除,可以快速返回,继续处理其他事件,避免服务器长时间阻塞。
    以删除(DEL命令)为例,看看Redis是如何实现的,下面就是删除函数的入口,其中,lazyfree_lazy_user_del是是否修改DEL命令的默认行为,一旦开启,执行DEL时将会以UNLINK形式执行。
    同步删除很简单,只要把key和value删除,如果有内层引用,则进行递归删除;
    异步删除,Redis在回收对象时,会先计算回收收益,只有回收收益在超过一定值时,采用封装成Job加入到异步处理队列中,否则直接同步回收,这样效率更高。回收收益计算也很简单,比如String类型,回收收益值就是1,而Set类型,回收收益就是集合中元素个数。
    通过引入a threaded lazy free,Redis实现了对于Slow Operation的Lazy操作,避免了在大键删除,FLUSHALL,FLUSHDB时导致服务器阻塞。
    当然,在实现该功能时,不仅引入了lazy free线程,也对Redis聚合类型在存储结构上进行改进。
    因为Redis内部使用了很多共享对象,比如客户端输出缓存。当然,Redis并未使用加锁来避免线程冲突,锁竞争会导致性能下降,而是去掉了共享对象,直接采用数据拷贝。
    去掉共享对象,不但实现了lazy free功能,也为Redis向多线程跨进带来了可能,
    在3.x和6.x中ZSet节点value的不同实现。
    ```java void delCommand(client *c) { delGenericCommand(c,server.lazyfree_lazy_user_del); }

/ This command implements DEL and LAZYDEL. / void delGenericCommand(client *c, int lazy) { int numdel = 0, j;

  1. for (j = 1; j < c->argc; j++) {
  2. expireIfNeeded(c->db,c->argv[j]);
  3. // 根据配置确定DEL在执行时是否以lazy形式执行
  4. int deleted = lazy ? dbAsyncDelete(c->db,c->argv[j]) :
  5. dbSyncDelete(c->db,c->argv[j]);
  6. if (deleted) {
  7. signalModifiedKey(c,c->db,c->argv[j]);
  8. notifyKeyspaceEvent(NOTIFY_GENERIC,
  9. "del",c->argv[j],c->db->id);
  10. server.dirty++;
  11. numdel++;
  12. }
  13. }
  14. addReplyLongLong(c,numdel);

}

  1. ```java
  2. /* Delete a key, value, and associated expiration entry if any, from the DB.
  3. * If there are enough allocations to free the value object may be put into
  4. * a lazy free list instead of being freed synchronously. The lazy free list
  5. * will be reclaimed in a different bio.c thread. */
  6. #define LAZYFREE_THRESHOLD 64
  7. int dbAsyncDelete(redisDb *db, robj *key) {
  8. /* Deleting an entry from the expires dict will not free the sds of
  9. * the key, because it is shared with the main dictionary. */
  10. if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
  11. /* If the value is composed of a few allocations, to free in a lazy way
  12. * is actually just slower... So under a certain limit we just free
  13. * the object synchronously. */
  14. dictEntry *de = dictUnlink(db->dict,key->ptr);
  15. if (de) {
  16. robj *val = dictGetVal(de);
  17. // 计算value的回收收益
  18. size_t free_effort = lazyfreeGetFreeEffort(val);
  19. /* If releasing the object is too much work, do it in the background
  20. * by adding the object to the lazy free list.
  21. * Note that if the object is shared, to reclaim it now it is not
  22. * possible. This rarely happens, however sometimes the implementation
  23. * of parts of the Redis core may call incrRefCount() to protect
  24. * objects, and then call dbDelete(). In this case we'll fall
  25. * through and reach the dictFreeUnlinkedEntry() call, that will be
  26. * equivalent to just calling decrRefCount(). */
  27. // 只有回收收益超过一定值,才会执行异步删除,否则还是会退化到同步删除
  28. if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
  29. atomicIncr(lazyfree_objects,1);
  30. bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
  31. dictSetVal(db->dict,de,NULL);
  32. }
  33. }
  34. /* Release the key-val pair, or just the key if we set the val
  35. * field to NULL in order to lazy free it later. */
  36. if (de) {
  37. dictFreeUnlinkedEntry(db->dict,de);
  38. if (server.cluster_enabled) slotToKeyDel(key->ptr);
  39. return 1;
  40. } else {
  41. return 0;
  42. }
  43. }
  1. // 3.2.5版本ZSet节点实现,value定义robj *obj
  2. /* ZSETs use a specialized version of Skiplists */
  3. typedef struct zskiplistNode {
  4. robj *obj;
  5. double score;
  6. struct zskiplistNode *backward;
  7. struct zskiplistLevel {
  8. struct zskiplistNode *forward;
  9. unsigned int span;
  10. } level[];
  11. } zskiplistNode;
  12. // 6.0.10版本ZSet节点实现,value定义为sds ele
  13. /* ZSETs use a specialized version of Skiplists */
  14. typedef struct zskiplistNode {
  15. sds ele;
  16. double score;
  17. struct zskiplistNode *backward;
  18. struct zskiplistLevel {
  19. struct zskiplistNode *forward;
  20. unsigned long span;
  21. } level[];
  22. } zskiplistNode;
  1. Redis源码学习之BIO
    6.0发布的多线程并未将事件处理改成多线程,而是在I/O上,此外,如果把事件处理改成多线程,不但会导致锁竞争,而且会有频繁的上下文切换,即使用分段锁来减少竞争,对Redis内核也会有较大改动,性能也不一定有明显提升。
    redis - 图4
    上图红色部分,就是Redis实现的多线程部分,利用多核来分担I/O读写负荷。在事件处理线程每次获取到可读事件时,会将所有就绪的读事件分配给I/O线程,并进行等待,在所有I/O线程完成读操作后,事件处理线程开始执行任务处理,在处理结束后,同样将写事件分配给I/O线程,等待所有I/O线程完成写操作。
    Redis中的多进程:
    在写入备份(RDB,AOF)的时候,会fork出子进程进行备份文件的写入。
    1. AOF的备份模式中,如果我们设置的是AOF_FSYNC_EVERYSEC(每秒备份一次,这个设置可理解为弱同步备份),redis会create一个backgroud线程,在这个线程中执行aof备份文件的写入。
    2. 新生成的AOF文件,在覆盖旧AOF文件时。 如果在此之前AOF备份已经开启,在执行该fd的close前,我们的Redis进程与旧的AOF文件存在引用, 旧的AOF文件不会真正被删除。 所以当我们执行close(oldfd)时,旧AOF文件的被打开该文件的进程数为0,即没有进程打开过这个文件,这时这个文件在执行close时会被真正删除。 而删除旧AOF文件可能会阻塞服务,所以我们将它放到另一个线程调用。
    3. 执行DEL操作,假如碰巧这个key对应有非常多对象,那么这个删除操作会阻塞服务器几秒钟时间, 所以将删除操作放到另一个线程执行。

Redis将所有多线程操作封装到BIO中,在C语言文件bio.c,bio.h中可以看到。
bio_jobs[]关联所有逻辑

  1. /* Exported API */
  2. void bioInit(void);//初始化BIO
  3. unsigned long long bioPendingJobsOfType(int type);//获取当前BIO任务类型,队列中待执行的任务个数
  4. unsigned long long bioWaitStepOfType(int type);//阻塞等待某个类型的BIO任务的执行,返回等待任务个数
  5. void bioKillThreads(void);//中断所有BIO进程
  6. void bioCreateCloseJob(int fd);
  7. void bioCreateFsyncJob(int fd);
  8. void bioCreateLazyFreeJob(lazy_free_fn free_fn, int arg_count, ...);
  • bioCreateLazyFreeJob
    • bioSubmitJob
      • pthread_cond_signal
  • bioCreateCloseJob
    • bioSubmitJob
      • pthread_cond_signal
  • bioCreateFsyncJob

    • bioSubmitJob
      • pthread_cond_signal
        1. void bioSubmitJob(int type, struct bio_job *job) {
        2. job->time = time(NULL);
        3. pthread_mutex_lock(&bio_mutex[type]);
        4. listAddNodeTail(bio_jobs[type],job);
        5. bio_pending[type]++;
        6. pthread_cond_signal(&bio_newjob_cond[type]);
        7. pthread_mutex_unlock(&bio_mutex[type]);
        8. }
  • bioInit 初始化BIO,插入bio_jobs

    • bioProcessBackgroundJobs: 执行BIO任务线程。(异步删除的时候使用)
      • pthread_cond_wait[versioned_symbol (libpthread, __pthread_cond_wait, pthread_cond_wait,GLIBC_2_3_2);] // glibc/nptl/pthread_cond_wait.c
        • __pthread_cond_wait_common : 内部动作Unlock -> Wait(sleep) -> Lock。 ```java void bioProcessBackgroundJobs(void arg) { struct bio_job *job; unsigned long type = (unsigned long) arg; sigset_t sigset;

    / Check that the type is within the right interval. / if (type >= BIO_NUM_OPS) { serverLog(LL_WARNING,

    1. "Warning: bio thread started with wrong type %lu",type);

    return NULL; }

    switch (type) { case BIO_CLOSE_FILE: redis_set_thread_title(“bio_close_file”); break; case BIO_AOF_FSYNC: redis_set_thread_title(“bio_aof_fsync”); break; case BIO_LAZY_FREE: redis_set_thread_title(“bio_lazy_free”); break; }

    redisSetCpuAffinity(server.bio_cpulist);

    makeThreadKillable();

    pthread_mutex_lock(&bio_mutex[type]); /* Block SIGALRM so we are sure that only the main thread will

    • receive the watchdog signal. */ sigemptyset(&sigset); sigaddset(&sigset, SIGALRM); if (pthread_sigmask(SIG_BLOCK, &sigset, NULL)) serverLog(LL_WARNING,
      1. "Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));

    // AOF 落盘、懒删除逻辑和关闭大文件fd while(1) { listNode *ln;

    / The loop always starts with the lock hold. / if (listLength(bio_jobs[type]) == 0) {

    1. // pthread_cond_wait 实际上就是一次 Unlock -> Wait -> Lock。
    2. pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);
    3. continue;

    } / Pop the job from the queue. / ln = listFirst(bio_jobs[type]); job = ln->value; /* It is now possible to unlock the background system as we know have

    • a stand alone job structure to process.*/ pthread_mutex_unlock(&bio_mutex[type]);

      / Process the job accordingly to its type. / if (type == BIO_CLOSE_FILE) { close(job->fd); } else if (type == BIO_AOF_FSYNC) { /* The fd may be closed by main thread and reused for another

      • socket, pipe, or file. We just ignore these errno because
      • aof fsync did not really fail. */ if (redis_fsync(job->fd) == -1 && errno != EBADF && errno != EINVAL) { int last_status; atomicGet(server.aof_bio_fsync_status,last_status); atomicSet(server.aof_bio_fsync_status,C_ERR); atomicSet(server.aof_bio_fsync_errno,errno); if (last_status == C_OK) {
        1. serverLog(LL_WARNING,
        2. "Fail to fsync the AOF file: %s",strerror(errno));
        } } else { atomicSet(server.aof_bio_fsync_status,C_OK); } } else if (type == BIO_LAZY_FREE) { job->free_fn(job->free_args); } else { serverPanic(“Wrong job type in bioProcessBackgroundJobs().”); } zfree(job); // 其他代码 } } ``` 局限性:6.0版本的多线程并非彻底的多线程,I/O线程只能同时执行读或者同时执行写操作,期间事件处理线程一直处于等待状态,并非流水线模型,有很多轮训等待开销。
  1. Tair多线程
    相较于6.0版本的多线程,Tair的多线程实现更加优雅。
    redis - 图5
    • Tair的Main Thread负责客户端连接建立等,
    • IO Thread负责请求读取、响应发送、命令解析等,
    • Worker Thread线程专门用于事件处理。
    • IO Thread读取用户的请求并进行解析,之后将解析结果以命令的形式放在队列中发送给Worker Thread处理。
    • Worker Thread将命令处理完成后生成响应,通过另一条队列发送给IO Thread。
    • 为了提高线程的并行度,IO Thread和Worker Thread之间采用无锁队列和管道进行数据交换,整体性能会更好。

小结:Redis 4.0引入Lazy Free线程,解决了诸如大键删除导致服务器阻塞问题,在6.0版本引入了I/O Thread线程,正式实现了多线程,但相较于Tair,并不太优雅,而且性能提升上并不多,压测看,多线程版本性能是单线程版本的2倍,Tair多线程版本则是单线程版本的3倍。
Redis多线程无非两种思路,I/O threading和Slow commands threading

  1. 内存管理机制和实现
    redis - 图6
    1. 内存管理:过期键的懒性删除和过期删除以及内存溢出控制策略。
    2. maxmemory 参数限制最大可用内存(默认值为0,表示无限制)
      redis - 图7
      • 防止所用内存超过服务器物理内存。当超出内存上限 maxmemory 时使用 LRU 等删除策略释放空间。maxmemory 限制的是Redis实际使用的内存量
      • used_memory统计项对应的内存。由于内存碎片率的存在,实际消耗的内存 可能会比maxmemory设置的更大
    3. 内存回收策略
    4. 删除到达过期时间的键值对象
      Redis 采用惰性删除和定时任务删除机制实现过期键的内存回收
      原因:当 Redis保存大量的键,对每个键都进行精准的过期删除可能会导致消耗大量的 CPU,会阻塞 Redis 的主线程,拖累 Redis 的性能
      1. 惰性删除:是指当客户端操作带有超时属性的键时,会检查是否超过键的过期时间,然后会同步或者异步执行删除操作并返回键已经过期。这样可以节省 CPU成本考虑,不需要单独维护过期时间链表来处理过期键的删除。(过期删除业务逻辑可以借鉴)
        • 过期键的惰性删除策略由 db.c/expireifNeeded 函数实现,所有对数据库的读写命令执行之前都会调用 expireifNeeded 来检查命令执行的键是否过期。
        • expireIfNeeded(4.0)
          • propagateExpire
          • dbAsyncDelete
            • dictUnlink(将键值从键值表中删除)
            • bioCreateBackgroundJob(->bioProcessBackgroundJobs)
            • dictFreeUnlinkedEntry(释放)
          • dbSyncDelete
            当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。
      2. 定时任务:定时任务中采用了自适应算法,根据键的 过期比例、使用快慢两种速率模式回收键
        • 在redis事件驱动的循环中的eventLoop->beforesleep和周期性操作 databasesCron 都会调用expire.c/activeExpireCycle 来处理过期键。
          • activeExpireCycle
            • activeExpireCycleTryExpire(同expireIfNeeded)
    5. 内存溢出控制策略
      当内存达到 maxmemory 时触发内存移除控制策略,强制删除选择出来的键值对象。
      具体策略受maxmemory-policy参数控制
      • 1)noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。
      • 2)volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直 到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
      • 3)allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。
      • 4)allkeys-random:随机删除所有键,直到腾出足够空间为止。
      • 5)volatile-random:随机删除过期键,直到腾出足够空间为止。
      • 6)volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
  2. Redis 数据类型介绍

    1. Redis5 内部编码
      • define OBJENCODING_RAW 0 / Raw representation _/

      • define OBJENCODING_INT 1 / 编码为整数 _/

      • define OBJENCODING_HT 2 / 编码为散列表 _/

      • define OBJENCODING_ZIPMAP 3 / 编码为zipmap _/

      • define OBJENCODING_LINKEDLIST 4 / 不再使用:旧列表编码_/

      • define OBJENCODING_ZIPLIST 5 / 编码为压缩列表 _/

      • define OBJENCODING_INTSET 6 / 编码为整数集合_/

      • define OBJENCODING_SKIPLIST 7 / 编码为跳表_/

      • define OBJENCODING_EMBSTR 8 / 编码为简短字符串_/

      • define OBJENCODING_Quicklist 9 / 编码为快速链表_/

      • define OBJENCODING_STREAM 10 / 编码为listpacks的基数树_/

    2. Strings:二进制安全的字符串
    • 二进制安全是与操作字符串的方法的相关术语,该方法的参数可以包含任何字符,方法会公平的对待数据流的每个字符,不特殊处理其中某一个字符,包括特殊字符。
      • C语言中字符串是以特殊字符“\0”来作为字符串的结束标识。对于字符串str=”0123456789\0123456789”来说,在C语言里面str的长度就是10(strlen(str)=10),所以strlen()函数不是二进制安全的。
    • 简单动态字符串
      1. 柔性数组: 这种代码结构产生于对动态结构体的需求,主要用途: 满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题
      • C99标准的定义如下:
        1. struct test {
        2. short len; // 必须至少有一个其它成员
        3. char arr[]; // 柔性数组必须是结构体最后一个成员(也可是其它类型,如:int、double、...)
        4. };
        ```
    • 柔性数组成员必须定义在结构体里面且为最后元素;
    • 结构体中不能单独只有柔性数组成员;
    • 柔性数组不占内存。

      1. - 在一个结构体的最后,申明一个长度为空的数组,就可以使得这个结构体是可变长的。
      2. - 对于编译器来说,此时长度为 0 的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量,
      3. - 但对于这个数组的大小,我们可以进行动态分配
      4. 2. Redis 3.2 之前的SDS主要是通过C语言中结构体类型确定的,包括已占用字节数len、剩余可用字节数free、实际保存字符串的字符数组来确定的。(源码:C语言头文件sds.h
      5. ```java
      6. struct sdshdr {
      7. //buf中已占用字节数
      8. int len;
      9. //buf剩余可用字节数
      10. int free;
      11. //实际保存字符串的字符数组
      12. char buf[];
      13. };

      (1)由于有长度的统计变量len的存在,读写字符串时不依赖“\0”终止符,保证了二进制安全
      (2)Redis保存的字符串对外暴露的是数组的长度指针,而不是结构体的指针,上层可以像操作普通字符串一样操作SDS。
      (3)SDS采用柔性数组的缺点: 不同SDS字符串占用了相同大小的头部空间(buf、leng长度),浪费空间。

      1. Redis 5.0改进了SDS,将根据字符串的长度,分成了5种类型sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64。(源码:C语言头文件sds.h) ```java /* Note: sdshdr5 is never used, we just access the flags byte directly.
      • However is here to document the layout of type 5 SDS strings. / // sdshdr5 从未使用过,我们只是直接访问标志字节。 struct attribute ((packed)) sdshdr5 { unsigned char flags; / 3 lsb of type, and 5 msb of string length 3 lsb 类型,5 msb 字符串长度 / char buf[]; }; struct attribute ((packed)) sdshdr8 { uint8_t len; / used / uint8_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr16 { uint16_t len; / used / uint16_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr32 { uint32_t len; / used / uint32_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[]; }; struct attribute ((packed)) sdshdr64 { uint64_t len; / used / uint64_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits */ char buf[]; }; ``` (1)len表示buf中已占用字节数。
        (2)alloc表示buf中已分配字节数,记录的是为buf分配的总长度,不同于free。
        (3)flags标识当前结构体的类型,低3位用作标识位,高5位预留。
        (4)buf柔性数组,真正存储字符串的数据空间。
      1. 区别

(1)五种类型都多了一个flags字段,但sdsdr5没有了头部(len和alloc)
(2)sdshdr5结构中,flags占1个字符,其低3位表示结构体类型,高5位表示长度,能表示的长度区间为0~31(表示字节数量),flags后面就是字符串的内容。
(3)而长度大于31的字符串,1个字节存不下,那么就要将len和alloc单独存放,因此redis存放数据时会先检查字符串长度,再根据字符串长度计算好不同类型的头部和初始长度,然后动态分配内存

  • 存储原理
    Redis 是 KV 的数据库,它是通过 hashtable 实现的,例,”set hello word”,在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。
    redis - 图8
    1. dictEntry

每个键值对都会有一个 dictEntry(源码:C语言头文件dict.h)

  1. typedef struct dictEntry {
  2. void *key;
  3. union {
  4. void *val;
  5. uint64_t u64;
  6. int64_t s64;
  7. double d;
  8. } v;
  9. struct dictEntry *next; /* Next entry in the same hash bucket. */
  10. void *metadata[]; /* An arbitrary number of bytes (starting at a
  11. * pointer-aligned address) of size as returned
  12. * by dictType's dictEntryMetadataBytes(). */
  13. } dictEntry;
  14. typedef struct dictType {
  15. uint64_t (*hashFunction)(const void *key);
  16. void *(*keyDup)(dict *d, const void *key);
  17. void *(*valDup)(dict *d, const void *obj);
  18. int (*keyCompare)(dict *d, const void *key1, const void *key2);
  19. void (*keyDestructor)(dict *d, void *key);
  20. void (*valDestructor)(dict *d, void *obj);
  21. int (*expandAllowed)(size_t moreMem, double usedRatio);
  22. /* Allow a dictEntry to carry extra caller-defined metadata. The
  23. * extra memory is initialized to 0 when a dictEntry is allocated. */
  24. size_t (*dictEntryMetadataBytes)(dict *d);
  25. } dictType;
  26. struct dict {
  27. dictType *type;
  28. dictEntry **ht_table[2];
  29. unsigned long ht_used[2];
  30. long rehashidx; /* rehashing not in progress if rehashidx == -1 */
  31. /* Keep small vars at end for optimal (minimal) struct padding */
  32. int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
  33. signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
  34. };

(1)key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS中。
(2)value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。

  1. 1. redisObject

redisObject (源码:C语言头文件server.h)

  1. typedef struct redisObject {
  2. /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
  3. unsigned type:4;
  4. /* 内部编码类型 */
  5. unsigned encoding:4;
  6. /* LRU time (relative to global lru_clock) or
  7. * LFU data (least significant 8 bits frequency
  8. * and most significant 16 bits access time). */
  9. /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
  10. unsigned lru:LRU_BITS;
  11. int refcount;
  12. void *ptr;
  13. } robj;
  1. 1. SDS
  2. 1. 内部编码 encoding

字符串类型的内部编码有三种:
1、int,存储 8 个字节的长整型(long,2^63-1)。
2、embstr, 代表 embstr 格式的 SDS,存储小于 44 个字节的字符串(源码:C语言类文件object.c)。

  1. #define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44

3、raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)。为什么是 39?
redis - 图9

  1. 1. 数据访问

redis - 图10

  • Redis字符串能包含任意类型的数据,例如: 一张JPEG格式的图片或者一个序列化的Ruby对象。
  • 一个字符串类型的值最多能存储512M字节的内容。
  1. 利用INCR命令簇(INCR, DECR, INCRBY)来把字符串当作原子计数器使用。
  2. 使用APPEND命令在字符串后添加内容。
  3. 将字符串作为GETRANGE 和 SETRANGE的随机访问向量。
  4. 在小空间里编码大量数据,或者使用 GETBIT 和 SETBIT创建一个Redis支持的Bloom过滤器。
  5. Lists: 按插入顺序排序的字符串元素的集合。他们基本上就是链表(linked lists)。
  6. Sets: 不重复且无序的字符串元素的集合。
  7. Sorted sets,类似Sets,但是每个字符串元素都关联到一个叫score浮动数值(floating number value)。里面的元素总是通过score进行着排序,所以不同的是,它是可以检索的一系列元素。(例如你可能会问:给我前面10个或者后面10个元素)。
  8. Hashes,由field和关联的value组成的map。field和value都是字符串的。这和Ruby、Python的hashes很像。
  9. Bit arrays (或者说 simply bitmaps): 通过特殊的命令,你可以将 String 值当作一系列 bits 处理:可以设置和清除单独的 bits,数出所有设为 1 的 bits 的数量,找到最前的被设为 1 或 0 的 bit,等等。
  10. HyperLogLogs: 这是被用于估计一个 set 中元素数量的概率性的数据结构。
    1. 事务
  11. 背景
    数据库事务是比较复杂的东西,若照搬到redis中去,理所当然redis就不是那么简单纯碎的东西,但是,事务是我们写程序无法逃避的场景,所以redis作者折衷的写了个简化版的事务机制
  12. a

    1. a