学习回顾

174 阅读9分钟

线程

  • 线程状态:new,runnable(running,ready),timeWaiting,waiting,blocked,Terminated
  • 启动方法:runnable.start(); new Thread(Runnable r);
  • synchronized: 偏向(对象头),轻量(自旋,占cpu),重量(os,等待)
  • synchronized:锁对象,(this,xx.class)

volatile

  • 保证线程可见性
    • cpu 缓存一致性协议
  • 禁止指令重排序
    • 单例双重检查锁案例
  • 不保证原子性
    • i++

锁优化

  • 锁细化
  • 锁粗化(竞争太频繁)

cas

  • compare and swap
  • atomic包 unsafe 类
  • cpu 原语
  • aba 问题
    • 时间戳/版本号 AtomicStampedReference 可以用版本号
  • LongAdder 分段锁cas

unsafe

  • allocateMemory 直接分配内存
  • compareAndSetXX

BIO,NIO

BIO

  • 阻塞的,效率低下,线程和fd一对一

NIO 第一版

  • 用户线程不断轮询,线程和fd一对多,但是用户线程需要轮询非常多次,多次调用内核,成本大

NIO 第二版 select

  • 用户线程调用select,内核做轮询。但是需要传参拷贝fd list

NIO 第三版 epoll

  • 用户线程和内核共享内存(mmap),解决了传递fd list 的问题

零拷贝

  • 数据不经过内核态,直接指定 in 和 out (sendfile方法)

epoll + 零拷贝 = kafka

  • 生产者写入通过mmap将数据从网卡存储到磁盘
  • 消费者通过零拷贝直接将数据从磁盘发送到网卡

redis

概述

  • 一个进程16个库
  • 都是key-value,value不同类型
  • 二进制安全的,只存放字节
  • 正负索引 正序0123 倒序-1-2-3

基础数据结构

string

底层数据

  • string 类型
  • int 类型
  • bitmap 类型

list

  • 同向操作=栈 反向=队列 index=数组 blpop阻塞弹出=阻塞队列/订阅

概述

  • 当链表entry数据超过512、或单个value 长度超过64,底层就会zipList转化成linkedList编码;
  • 在版本3.2之前,列表底层的编码是ziplist和linkedlist实现的:
    • 对linkedlist来说,这些命令都是基于prev、next、head、tail指针完成的,双向链表基于指针,实现比较容易。
    • 而ziplist则是 通过当前entry的指针,加减位移的偏移量(前一个entry的长度),进行跳跃节点。
  • 在版本3.2之后,重新引入 quicklist,列表的底层都由quicklist实现;

list是如何实现阻塞队列的?

  • 将客户端的状态设为“正在阻塞”,并记录阻塞这个客户端的各个键,以及阻塞的最长时限(timeout)等数据。
  • 将客户端的信息记录到 server.db[i]->blocking_keys 中(其中 i 为客户端所使用的数据库号码)。
  • 继续维持客户端和服务器之间的网络连接,但不再向客户端传送任何信息,造成客户端阻塞。
  • 步骤 2 是将来解除阻塞的关键, server.db[i]->blocking_keys 是一个字典, 字典的键是那些造成客户端阻塞的键, 而字典的值是一个链表, 链表里保存了所有因为这个键而被阻塞的客户端 (被同一个键所阻塞的客户端可能不止一个):

底层数据

压缩列表

  • 压缩列表(ziplist)是为了节约内存而设计的,是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构,一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值

  • 一个压缩列表可以包含多个节点(entry),每个节点可以保存一个字节数组或者一个整数值

  • 各部分组成说明如下

    • zlbytes:记录整个压缩列表占用的内存字节数,在压缩列表内存重分配,或者计算zlend的位置时使用
    • zltail:记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过该偏移量,可以不用遍历整个压缩列表就可以确定表尾节点的地址
    • zllen:记录压缩列表包含的节点数量,但该属性值小于UINT16_MAX(65535)时,该值就是压缩列表的节点数量,否则需要遍历整个压缩列表才能计算出真实的节点数量
    • entryX:压缩列表的节点
    • zlend:特殊值0xFF(十进制255),用于标记压缩列表的末端
  • 每个压缩列表节点可以保存一个字节数字或者一个整数值,结构如下

    • previous_entry_ength:记录压缩列表前一个字节的长度
    • encoding:节点的encoding保存的是节点的content的内容类型
    • content:content区域用于保存节点的内容,节点内容类型和长度由encoding决定

hash

底层数据

字典

  • 字典最外层结构如下
typedef struct dict {
    // 和类型相关的处理函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引,当rehash不再进行时,值为-1
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 迭代器数量
    unsigned long iterators; /* number of iterators currently running */
} dict;
  • dict的ht属性是两个元素的数组,包含两个dictht哈希表,一般字典只使用ht[0]哈希表,ht[1]哈希表会在对ht[0]哈希表进行rehash(重哈希)的时候使用,即当哈希表的键值对数量超过负载数量过多的时候,会将键值对迁移到ht[1]上
  • rehashidx也是跟rehash相关的,rehash的操作不是瞬间完成的,rehashidx记录着rehash的进度,如果目前没有在进行rehash,它的值为-1
  • type属性和privdata属性是针对不同类型的键值对,用于创建多类型的字典,type是指向dictType结构的指针,privdata则保存需要传给类型特定函数的可选参数,关于dictType结构和类型特定函数可以看下面代码
typedef struct dictType {
    // 计算哈希值的行数
    uint64_t (*hashFunction)(const void *key);
    // 复制键的函数
    void *(*keyDup)(void *privdata, const void *key);
    // 复制值的函数
    void *(*valDup)(void *privdata, const void *obj);
    // 对比键的函数
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    // 销毁键的函数
    void (*keyDestructor)(void *privdata, void *key);
    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);
} dictType;
  • Redis的字典底层是使用哈希表实现的,一个哈希表里面可以有多个哈希表节点,每个哈希表节点中保存了字典中的一个键值对
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值,等于size-1
    unsigned long sizemask;
    // 哈希表已有节点的数量
    unsigned long used;
} dictht;
  • 哈希表是由数组table组成,table中每个元素都是指向dict.h/dictEntry结构的指针,哈希表节点的定义如下,其中key是我们的键;
typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    // 指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;

v是键值,可以是一个指针,也可以是整数或浮点数;next属性是指向下一个哈希表节点的指针,可以让多个哈希值相同的键值对形成链表,解决键冲突问题

set

  • 无序的
  • srandmember 随机抽取 x 个元素。 正数不重复,负数可重复
  • spop 取出一个

底层数据

  • 如果能够转成int的对象(isObjectRepresentableAsLongLong),那么就用intset保存。
  • 如果用intset保存的时候,如果长度超过512(REDIS_SET_MAX_INTSET_ENTRIES)就转为hashtable编码。
  • 其他情况统一用hashtable进行存储。

整数集合 intset

整数集合的定义为intset.h/intset

typedef struct intset {
    // 编码方式
    uint32_t encoding;
    // 集合包含的元素数量
    uint32_t length;
    // 保存元素的数组
    int8_t contents[];
} intset;
  • contents数组:整数集合的每个元素在数组中按值的大小从小到大排序,且不包含重复项
  • length记录整数集合的元素数量,即contents数组长度
  • encoding决定contents数组的真正类型,如INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64

hashtable

zset

概述

  • 有序不重复

底层结构

skiplist + hash(key,score)


跳跃表其实可以把它理解为多层的链表,它有如下的性质

  • 多层的结构组成,每层是一个有序的链表
  • 最底层(level 1)的链表包含所有的元素
  • 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)
  • 跳跃表是一种随机化的数据结构(通过抛硬币来决定层数)

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    // 成员对象 (robj *obj;)
    sds ele;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        // 跨度实际上是用来计算元素排名(rank)的,在查找某个节点的过程中,将沿途访过的所有层的跨度累积起来,
        // 得到的结果就是目标节点在跳跃表中的排位
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist;

zskiplistNode结构

  • level数组(层):每次创建一个新的跳表节点都会根据幂次定律计算出level数组的大小,也就是次层的高度,每一层带有两个属性-前进指针和跨度,前进指针用于访问表尾方向的其他指针;跨度用于记录当前节点与前进指针所指节点的距离(指向的为NULL,阔度为0)
  • backward(后退指针):指向当前节点的前一个节点
  • score(分值):用来排序,如果分值相同看成员变量在字典序大小排序
  • obj或ele:成员对象是一个指针,指向一个字符串对象,里面保存着一个sds;在跳表中各个节点的成员对象必须唯一,分值可以相同

zskiplist结构

  • header、tail表头节点和表尾节点
  • length表中节点的数量
  • level表中层数最大的节点的层数

如何上浮?

  • 先抛硬币确定层数
  • 往前遍历直到找到拥有对应层数的节点,并且记录下相应的跨度span
  • 将该节点对应层的指针指向上浮的节点