标签 redis 下的文章

一、基本用法

字符串是redis中的基本数据类型之一,能存储任何形式的字符串,包括二进制数据。同时,它还可以进行字符串运算数据运算位运算等操作。一个字符串最大能有512M。

字符串主要的操作命令有两个:

  • GET KEY: 如果KEY存在就返回对应的值,如果不存在则返回空值nil
  • SET KEY VALUE: 给KEY设置值为VALUE,如果KEY已经存在则更新值。

例如:

# 设置一个新值
127.0.0.1:6379> set test HelloWorld
OK
# 获取值
127.0.0.1:6379> get test
"HelloWorld"
# 获取不存在的值返回nil
127.0.0.1:6379> get aaaa
(nil)

字符串也支持同时给多个key同时赋值,方法为:

  • MGET key value [key value]: 同时设置多个键值。
  • MSET key value [key value]: 同时获取多个键值。
127.0.0.1:6379> mset keya aaa keyb bbb keyc ccc
OK
127.0.0.1:6379> mget keya keyb keyc
1) "aaa"
2) "bbb"
3) "ccc"

还有一个常用的操作就是在获取key的同时并设置key的值:

  • GETSET key value:给key赋值并返回先前的元素,如果元素不存在返回nil
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> getset k 123
(nil)
127.0.0.1:6379> getset k 456
"123"

二、操作API

字符串主要有以下操作命令:

2.1. APPEND

在尾部增加字符串,命令格式:append key value,成功将会返回添加后的字符串长度。

127.0.0.1:6379> set test Hello
OK
127.0.0.1:6379> append test World
(integer) 10  # 添加成功返回总长度
127.0.0.1:6379> get test
"HelloWorld"

2.2. STRLEN

获取字符串长度,命令格式:strlen key,成功将会返回该值得长度。

127.0.0.1:6379> strlen test
(integer) 10
127.0.0.1:6379> strlen aaaa
(integer) 0  # 键不存在返回0

2.3. GETRANGE

获取指定偏移范围内的字符,命令格式:getrange key start end,键不存在返回空。

127.0.0.1:6379> getrange test 5 10
"World"
127.0.0.1:6379> getrange aaaa 1 2
""  # 键不存在返回空

和大多数程序语言一样,redis的字符串下标从0开始,到len(key) - 1结束。要注意的是redis中可以使用-1表示最后一位。

127.0.0.1:6379> getrange test 0 -1 # 获取第一个字符到最后一个字符
"HelloWorld"

三、数据运算

当我们存入一个十进制整数或者浮点数到redis当中去的时候,redis会自动察觉到这一点,并允许我们使用相关的命令来操作它们。

3.1 INCR和DECR

把整形数据加一或者减一,命令格式:incr key decr key,执行成功会返回增加过后的值,如果key不存在时会自动创建。

127.0.0.1:6379> set num 100
OK
127.0.0.1:6379> incr num
(integer) 101  
127.0.0.1:6379> incr num
(integer) 102
127.0.0.1:6379> decr num
(integer) 101
127.0.0.1:6379> get num
"101"
127.0.0.1:6379> incr aaaa
(integer) 1  # 自动创建aaaa并初始化值为0然后+1

3.2 INCRBY、DECRBY和INCRBYFLOAT

增加或者删除指定的大小,命令格式为:incrby key incrementINCRBYDECRBY针对整数,INCRBYFLOAT针对浮点数,不可以使用INCRBYDECRBY操作浮点数。

127.0.0.1:6379> set num 100
OK
127.0.0.1:6379> incrby num 10
(integer) 110
127.0.0.1:6379> decrby num 5
(integer) 105
127.0.0.1:6379> incrbyfloat num 1.1
"106.1"
127.0.0.1:6379> incrby num 10
(error) ERR value is not an integer or out of range  # 使用整形命令操作浮点数会报错

使用数据运算的同时还可以字符串命令:

127.0.0.1:6379> set num 100
OK
127.0.0.1:6379> append num 999  # 在尾部添加
(integer) 6
127.0.0.1:6379> get num
"100999"
127.0.0.1:6379> incr num  # 仍然可以执行数据运算
(integer) 101000

除了这些以外,redis还支持位运算,可参考:Redis中的位运算

四、实现原理

字符串内部使用了三种编码方式,分别是int/raw/embstr。

4.1 int编码

当存储的字符串是一个整形数据的时候,redis会自动以整形数据来保存。

127.0.0.1:6379> set mobile 10086
OK
127.0.0.1:6379> OBJECT ENCODING mobile
"int"

当使用int类型编码的时候,字符串对象结构为:

4.2 raw和embstr编码

raw和embstr编码底层都是使用sdshdr来存储字符串,使用raw方式编码的字符串对象结构:

embstr和raw编码的不同在于使用embstr编码的时候,redis会把上面的redisObject和shshdr作为一个对象同时申请内存,它们在内存上是连续的,而raw编码的字符串在内存上并不连续。

当存储的字符串长度小于39时,redis会使用embstr来编码。这样做的目的主要有以下几个:

  1. 减少内存的分配和释放次数
  2. 内存连续适合小对象缓存

一、AOF持久化

1.1 实现机制

AOF(Append Only File)是redis持久化方式的一种,它通过把所有redis执行过的命令都写入到文件来维持持久化。一旦服务崩溃,则可以重放这些命令来恢复服务数据。

例如,在redis上执行下面2条语句:

127.0.0.1:6379> set data "helloworld"
OK
127.0.0.1:6379> sadd zdata hello world
(integer) 2

那么AOF文件中的内容就类似是:

set data "helloworld"
sadd zdata hello world

当然,文件中保存不是直接的命令语句。而是按照redis命令请求协议保存的,除了执行的命令以外还有一些其他的内容也会保存到文件。redis协议是redis执行命令专用的一种协议,当客户端向服务端发送请求也是使用的redis协议,AOF也使用redis协议的好处是可以直接使用redis协议解析库,不用再单独实现一套。

AOF数据并不是实时写入到文件中的,而是优先保存在缓冲区。redisServer对象中保存了一个aof_buf字段,它就是aof命令的保存缓冲区。当执行完命令后,会先把指令保存到这里,然后根据策略把缓冲区中的内容写入到磁盘。一个要注意的是,文件写入磁盘并不会立马刷新到文件系统,因为操作系统对系统IO有缓存,缓存到达一定条件后才会同步缓存到文件系统。

为了避免数据没有及时写入到文件系统(还在缓存中),AOF提供了三种策略来同步缓存:

策略描述
always每处理一次事件循环,就把aof文件写入到文件。这种办法最稳妥,数据丢失后的恢复效果最好。
Everysec每秒同步一次AOF文件缓存到文件系统。
no不主动同步缓存,由操作系统决定什么时候写入到文件系统。

1.2 AOF重写

当服务运行久了之后,AOF文件会变得很大,redis提供了AOF重写功能来优化AOF文件数据。

所做的优化就是整合指令,例如:

127.0.0.1:6379> zadd age 14 hello 15 world 16 nginx
(integer) 3
127.0.0.1:6379> zrem age hello
(integer) 1

执行完这两个命令后,数据库保存了一个有序集合age,里面包含了两个元素world和nginx。其中hello在第一个命令中加进来了,但是第二个指令又删除了,所以实际上写入AOF文件并不需要上面两行,只需要一行就可以完成:

zdd age 15 world 16 nginx

AOF重写就是基于这种策略来重写的,但是有一个要注意的地方是:在执行AOF写入的时候,会导致redis阻塞。所以一般建议是使用后台重写来完成整个AOF文件的重写,后台重写会新建一个子进程,避免在父进程中操作阻塞正常业务。执行后台重写的指令是BGREWRITEAOF

二、RDB持久化

RDB持久化是redis的第二种持久化方式,它持久化的原理是把数据库中所有键值对的二进制数据直接写入到文件。RDB持久化的优先级一般低于AOF持久化,因为AOF持久化的实时性高于RDB,所以在系统启动的时候如果存在AOF持久化,redis会优先通过AOF持久化恢复数据。

执行持久化的操作指令是SAVEBGSAVE,一个是前台执行,一个是后台执行。和AOF持久化一样,前台执行会阻塞当前redis进程,所有客户断请求都会被阻塞,因此一般建议是用BGSAVE,它也会新创建一个子进程来做持久化操作。RDB备份是全量备份。

可以在redis.conf中配置RDB相关的操作,主要有以下几个配置:

save <seconds> <changes>
dbfilename dump.rdb
dir ./

第一个配置是执行BGSAVE的策略,当在secnods秒之内执行了changes次操作时,会自动把改动写入到RDB文件。可以同时有多个save配置段存在,例如官方默认的配置中就有三个save配置:

save 900 1
save 300 10
save 60 10000

这三个配置,只要在任意时间段内满足了任意一条就会执行(三者互不影响,互相独立,各自备份各自的):

  1. 900秒内执行了一次更新,BGSAVE就会执行。
  2. 300秒内执行了10次更新,BGSAVE就会执行。
  3. 60秒呢执行了10000次更新,BGSAVE就会执行。

dbfilenamedir指定了备份文件名和路径,执行后就会把RDB文件写入到这个文件中。例如,设置保存的路径为/appdata/redis/dump.rdb,执行save命令后,就会生成这个文件:

RDB持久化文件可以使用官方的redis-check-rdb程序(低版本叫redis-check-dump)来检测相关数据:

当然,这个文件只提供了基本检测功能(即验证rdb文件是否合法),并不包含导出所有数据的功能。

如果需要处理实际的数据可以通过其他工具来实现,google一下redis rdb parser就能看到了。

github上也有很多,例如https://github.com/sripathikrishnan/redis-rdb-tools

三、AOF和RDB对比

RDB持久化AOF持久化
全量备份,一次保存整个数据库增量备份,只保存新修改的数据
保存的间隔较长保存的间隔默认一秒
更适合数据备份,默认开启更适合用来保存数据,和一般SQL持久化方式一样,默认关闭
save会阻塞,但bgsave或者自动不会阻塞无论是平时还是AOF重写,都不会阻塞
轻重 : 重轻重: 轻
启动优先级 : 低启动优先级 : 高
体积 : 小体积 : 大
恢复速度 : 快恢复速度 : 慢
数据安全性 : 丢数据数据安全性 : 根据策略决定
表格从Redis持久化AOF和RDB对比整理得来。

当使用的内存到达上限后,redis提供了6种策略来淘汰键值:

策略描述
volatile-lru在所有设置了过期时间的键值中根据LRU算法淘汰最近最少使用的
allkeys-lru对数据库中所有元素根据LRU算法淘汰最近最少使用的
volatile-random从设置了过期时间的元素中随机淘汰
allkeys->random数据库所有元素中随机淘汰
volatile-ttl从设置了过期时间的键值中淘汰快要超时的
noeviction不淘汰任何已有键值,直接给写操作返回错误
LRU是最近最少使用的,直译出来就是最久没有使用的。

redis默认的淘汰策略是volatile-lru,修改淘汰策略可以通过修改redis.conf文件中的maxmemory-policy字段,配置中关于各种淘汰策略也有详细的解释。使用grep volatile-lru redis.conf -A 6 -n可以过滤出这部分配置 :

一、链表定义

链表在redis中的使用十分广泛,例如列表的底层实现之一就是链表,包括发布、订阅等等功能都是有用到链表的。redis中链表在adlist.hadlist.c中实现,只用了300+行代码,十分简单。

redis中的链表是一个双向不循环的链表,两个核心的数据结构是listNodelist,其中listNode的定义如下:

typedef struct listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;

listNode是链表节点定义,链表节点和平常用到差别不大,由一个数据域和两个分别指向前后节点的指针组成。

list的定义为:

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned int len;
} list;

它和普通链表定义的不同点在于:除了包含首尾指针以及链表长度以外,链表的结构还包含了3个函数指针结构:

  • dup: 用于拷贝节点的函数。
  • free: 用于释放节点的函数。
  • match: 用于对比节点是否相等的函数。

链表的宏观结构:

二、链表操作

2.1 链表创建和释放

初始化的过程主要是创建链表对象,并初始化值:

list *listCreate(void)
{
    struct list *list;

    // 分配内存空间
    if ((list = zmalloc(sizeof(*list))) == NULL)
        return NULL;

    // 初始化成员
    list->head = list->tail = NULL;
    list->len = 0;
    list->dup = NULL;
    list->free = NULL;
    list->match = NULL;
    return list;
}

释放链表:

void listRelease(list *list)
{
    unsigned int len;
    listNode *current, *next;

    current = list->head;
    len = list->len;

    // 循环遍历链表
    while(len--) {
        next = current->next;
        // 如果有自定义的free函数,先调用函数释放节点内部数据
        if (list->free) list->free(current->value);
        // 释放节点
        zfree(current);
        current = next;
    }

    // 释放链表
    zfree(list);
}

2.2 插入节点

插入节点提供了三个函数,分别是从头部插入、从尾部插入以及从指定位置插入。

头插法:

list *listAddNodeHead(list *list, void *value)
{
    listNode *node;

    // 给新插入的节点分贝内存空间并赋值
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;

    // 插入节点
    if (list->len == 0) {
        // 链表为空的时候要给首尾指针赋值
        list->head = list->tail = node;
        node->prev = node->next = NULL;
    } else {
        // 插入节点到链表
        node->prev = NULL;
        node->next = list->head;
        list->head->prev = node;
        list->head = node;
    }

    // 长度加1
    list->len++;

    return list;
}

尾插法,和头插法基本一致:

ist *listAddNodeTail(list *list, void *value)
{
    listNode *node;

    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;
    if (list->len == 0) {
        list->head = list->tail = node;
        node->prev = node->next = NULL;
    } else {
        node->prev = list->tail;
        node->next = NULL;
        list->tail->next = node;
        list->tail = node;
    }
    list->len++;
    return list;
}

插入节点到指定位置:

/*
 * 参数说明
 * @list 链表对象
 * @old_node 被插入的节点
 * @value 新插入节点的数据域
 * @after 是查到节点的后面还是前面
 * @return 插入成功后返回传入的链表对象,否则返回NULL
 */
list *listInsertNode(list *list, listNode *old_node, void *value, int after) {
    listNode *node;

    // 创建新节点并赋值
    if ((node = zmalloc(sizeof(*node))) == NULL)
        return NULL;
    node->value = value;

    // 插入节点
    if (after) {
        // 插入到old_node节点后
        node->prev = old_node;
        node->next = old_node->next;
        if (list->tail == old_node) {
            list->tail = node;
        }
    } else {
        // 插入到old_节点前
        node->next = old_node;
        node->prev = old_node->prev;
        if (list->head == old_node) {
            list->head = node;
        }
    }
    
    // 修改前一个节点的next指向
    if (node->prev != NULL) {
        node->prev->next = node;
    }
    // 修改后一个节点的prev指向
    if (node->next != NULL) {
        node->next->prev = node;
    }
    // 链表长度加1
    list->len++;

    return list;
}

2.3 删除节点

void listDelNode(list *list, listNode *node)
{
    if (node->prev) // 删除的不是头结点,修改前一个节点的next指针
        node->prev->next = node->next;
    else // 删除的是头结点,更新head指向
        list->head = node->next;

    if (node->next)
        node->next->prev = node->prev;
    else // 删除的是尾结点,更新next指向
        list->tail = node->prev;
    // 存在节点的free函数,调用函数释放节点内部资源
    if (list->free) list->free(node->value);
    zfree(node);
    list->len--;
}

三、链表遍历

redis在实现链表的时候,给链表的遍历单独创建了一套结构和方法。其中遍历的结构是listIter,它包含了一个listNode *的指针域和遍历方向direction

typedef struct listIter {
    listNode *next;
    int direction;
} listIter;

direction的作用是标记遍历方向是从头部往尾部遍历还是从尾部往头部遍历,获取一个listIter的方法:

listIter *listGetIterator(list *list, int direction)
{
    listIter *iter;

    if ((iter = zmalloc(sizeof(*iter))) == NULL)
        return NULL;

    if (direction == AL_START_HEAD)
        iter->next = list->head; // 从头往尾遍历,iter指向头结点
    else
        iter->next = list->tail; // 从尾往头遍历,iter指向尾结点
    iter->direction = direction;
    return iter;
}

配合listNode获取下一个节点的函数:

listNode *listNext(listIter *iter)
{
    // 保存当前指针
    listNode *current = iter->next;

    // 
    if (current != NULL) {
        if (iter->direction == AL_START_HEAD)
            iter->next = current->next; // 赋值下一个节点到iter
        else
            iter->next = current->prev; // 赋值前一个节点到iter
    }
    return current;
}

注意listNext函数只是获取当前iter指向的节点,并把iter指向下一个节点。为什么要把遍历操作单独抽离出来呢?

主要有以下几个目的:

  1. 外部遍历时不用重复编写遍历代码。前面已经说过,链表在redis很多场景都用到了,所有的结构共用一套遍历实现就可以了。
  2. 外部遍历的时候无需访问list的内部元素,充分做到了面向对象的理念。

四、其他

4.1 链表拷贝

list *listDup(list *orig)
{
    list *copy;
    listIter *iter;
    listNode *node;

    // 为新链表创建内存空间
    if ((copy = listCreate()) == NULL)
        return NULL;
    // 初始化
    copy->dup = orig->dup;
    copy->free = orig->free;
    copy->match = orig->match;
    // 遍历
    iter = listGetIterator(orig, AL_START_HEAD);
    while((node = listNext(iter)) != NULL) {
        void *value;

        if (copy->dup) {
            // 如果存在复制函数,使用自定的复制函数来复制每个节点
            value = copy->dup(node->value);
            if (value == NULL) {
                // 复制失败,释放掉前面已经拷贝成功的节点,避免内存泄漏
                listRelease(copy);
                listReleaseIterator(iter);
                return NULL;
            }
        } else
            value = node->value;

        // 新拷贝的节点插入到链表结尾
        if (listAddNodeTail(copy, value) == NULL) {
            listRelease(copy);
            listReleaseIterator(iter);
            return NULL;
        }
    }
    listReleaseIterator(iter);
    return copy;
}

4.2 链表查找

listNode *listSearchKey(list *list, void *key)
{
    listIter *iter;
    listNode *node;

    // 遍历链表
    iter = listGetIterator(list, AL_START_HEAD);
    while((node = listNext(iter)) != NULL) {
        if (list->match) {
            // 使用自定义的比较函数来比较每个节点和查找的key
            if (list->match(node->value, key)) {
                listReleaseIterator(iter);
                return node;
            }
        } else {
            // 使用默认的比较函数来比较节点和key
            if (key == node->value) {
                listReleaseIterator(iter);
                return node;
            }
        }
    }
    listReleaseIterator(iter);
    return NULL;
}

4.3 返回指定位置上的元素

传入索引,返回对应位置上的元素,支持从尾部索引:

listNode *listIndex(list *list, int index) {
    listNode *n;

    if (index < 0) {
        // index小于0,从尾部开始索引
        index = (-index)-1;
        n = list->tail;
        while(index-- && n) n = n->prev;
    } else {
        // index大于0,从头部开始索引
        n = list->head;
        while(index-- && n) n = n->next;
    }
    return n;
}

五、总结

收获很大,主要是两个地方感触很深:

  1. 链表的遍历
  2. 从尾部开始返回对应索引位置上面的元素

其实实现并不算难,只是实现的思路和想法很新颖,很值得学习!

一、发布和订阅

除了任务队列以外,redis还有一种基于“发布/订阅”模式的消息传递,使得客户端可以订阅某个频道,当频道有消息产生时,会把消息传递到所有的订阅者。和列表不一样的是,发布和订阅可以是一对多的关系,即同一个消息可以同时传递到多个客户端(订阅者)。而列表只能允许一个客户端接收一个消息。

订阅和退订的命令是PUBLISH/SUBSCRIBEPUBLISH是向频道发送消息,SUBSCRIBE是订阅频道。

1.1 发布

往指定频道发布消息的操作:

PUBLISH channel message

消息发送成功后,将会返回一个整数,表示收到这条消息的订阅者数量,如果没有任何客户端订阅频道,返回0。

一个要注意的问题是消息发布后不会被持久化,如果当前没有订阅者订阅频道,后续再订阅也不会收到订阅前的消息。

- 阅读剩余部分 -

一、expire和ttl命令

1.1 基本用法

Redis中的EXPIER 命令可以给键值设置过期时间,相关的命令及其格式为:

EXPIRE key seconds
PEXPIRE key milliseconds

两个命令分别表示设置秒级和毫秒级别的过期时间,到期之后系统会自动删除该键。

EXPIRE 命令返回1 表示设置过期时间成功,返回0 表示键不存在或者设置失败。

127.0.0.1:6379> set k 1
OK
127.0.0.1:6379> expire k 60
(integer) 1
127.0.0.1:6379> expire x 1
(integer) 0

- 阅读剩余部分 -

一、基本操作

有序集合也是集合的一种,顾名思义,它和集合的不同之处在于它是有序的 ,而集合无序。它通过给集合中每一个元素都关联一个分数来是的元素有序,同时提供了多个方式获取排序之后的元素。

1.1 增加和删除元素

增加元素:

ZADD key score member [score member ...]

当key不存在时自动创建,分数支持整数和双精度浮点数,同时还可以使用+inf-inf分别表示正无穷大和负无穷大。执行成功返回添加的元素个数。

删除元素:

ZREM key member [member ...]

移除元素,返回成功移除的个数。

- 阅读剩余部分 -

一、概念和原理

集合是redis中的基本数据类型之一,redis中的集合可以用来存储一堆无序的元素,集合内的元素不会重复,只有存在或者不存在两种属性。

redis集合内部是由intsethashtable实现的,当条件不满足时redis会自动转换内部编码。集合中使用intset的条件:

  1. 集合的所有的元素都是整数
  2. 集合中元素的个数不超过512个

一旦两者的任一条件不满足时,就会转换成hashtable编码。

- 阅读剩余部分 -

一、基本用法

任何一门程序语言都离不开位运算这个功能,redis虽然不是一门编程语言,但也是一个和编程密切关联的工具。因此位运算自然也是redis中不可或缺的功能。

redis中位运算相关的方法:

  • GETBIT key offset: 获取第offset位的bit,不存的的比特位返回0。
  • SETBIT key offset value: 给第offset位设置成value。
  • BITCOUNT key [start] [end]: 计算key中1的个数。
  • BITOP operation destkey key [key]: 执行位操作,位操作包含与(AND)或(OR)异或(XOR)以及 非(NOT)
  • BITPOS key value [start] [end]: 查询key中第一次出现value的位置,startend表示字符的开始和结束位置。

- 阅读剩余部分 -

一、基本用法

字符串是redis中的基本数据类型之一,能存储任何形式的字符串,包括二进制数据。同时,它还可以进行字符串运算数据运算位运算等操作。一个字符串最大能有512M。

字符串主要的操作命令有两个:

  • GET KEY: 如果KEY存在就返回对应的值,如果不存在则返回空值nil
  • SET KEY VALUE: 给KEY设置值为VALUE,如果KEY已经存在则更新值。

- 阅读剩余部分 -