数据库

Redis笔记

2018-09-06  本文已影响182人  alivs

须知:个人读书笔记,很多点没有具体描述,只是大体罗列下,立个flag后面完善。

文章罗列了基本知识(类型,小功能),高可靠(持久化,LRU,复制,哨兵,集群),实现原理

基本知识

redis有5中类型,小功能也很惊喜。

类型

字符串(string)

> set mykey somevalue
OK
> get mykey
"somevalue"

> set mykey newval nx
(nil)
> set mykey newval xx
OK

> set counter 100
OK
> incr counter
(integer) 101
> incr counter
(integer) 102
> incrby counter 50
(integer) 152

> mset a 10 b 20 c 30
OK
> mget a b c
1) "10"
2) "20"
3) "30"

> set mykey x
> type mykey
string
> exists mykey
(integer) 1
> del mykey
(integer) 1
> type mykey
none

> set key 100 ex 10
OK
> ttl key
(integer) 9

哈希(hash)

由field和关联的value组成的map。field和value都是字符串的。

> hmset user:1000 username antirez birthyear 1977 verified 1
OK
> hget user:1000 username
"antirez"
> hget user:1000 birthyear
"1977"
> hgetall user:1000
1) "username"
2) "antirez"
3) "birthyear"
4) "1977"
5) "verified"
6) "1"

也有一些指令能够对单独的域执行操作,比如 HINCRBY:

> hincrby user:1000 birthyear 10
(integer) 1987
> hincrby user:1000 birthyear 10
(integer) 1997

列表(list)

按插入顺序排序的字符串元素的集合。他们基本上就是链表(linked lists)。

> rpush mylist A
(integer) 1
> lpush mylist B
(integer) 2
> rpush mylist C
(integer) 3
> lrange mylist 0 -1
1) "first"
2) "A"
3) "B"
> rpop mylist
"C"

redis> BRPOP list1 list2 0
1) "list1"
2) "c"

集合(set)

不重复且无序的字符串元素的集合。

> sadd myset 1 2 3
(integer) 3
> smembers myset
1. 3
2. 1
3. 2

Redis 有检测成员的指令。一个特定的元素是否存在?
> sismember myset 3
(integer) 1
> sismember myset 30
(integer) 0

交集
> sinter tag:1:news tag:2:news tag:10:news tag:27:new

通过 SUNIONSTORE 实现的,它通常用于对多个集合取并集
> sunionstore game:1:deck deck
(integer) 52

有序集合 (zset)

类似Sets,但是每个字符串元素都关联到一个叫score浮动数值(floating number value)。里面的元素总是通过score进行着排序,所以不同的是,它是可以检索的一系列元素。

添加 zadd key score member
> zadd user:ranking 251 tom 250 kimi

计算个数 zcard key
>zcard user:ranking 

计算排名
>zrank user:ranking kimi
>1

删除成员 zrem key member
>zrem  user:ranking kimi
增加成员分数
>zincrby key increment 【num】 member
查找排名前,withscores会返回分数
>zrang key start end [withscores]

小功能

bitmaps

通过特殊的命令,你可以将 String 值当作一系列 bits 处理:可以设置和清除单独的 bits,数出所有设为 1 的 bits 的数量,找到最前的被设为 1 或 0 的 bit,等等。

常用命令:

setbit key offset value
getbit key offset
(不推荐,较慢)bitcount key [start end]

bitmaps常用来做布隆过滤器。

hyperloglogs

被用于估计一个 set 中元素数量的概率性的数据结构。

地理空间(geospatial)

消息发布/订阅

image.png
d:\Redis>redis-cli.exe
127.0.0.1:6379> publish channel1 "hello world"
(integer) 2
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> publish channel1 "publish message 'hello,world'"
(integer) 2
127.0.0.1:6379>

d:\Redis>redis-cli
127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello world"
1) "message"
2) "channel1"
3) "publish message 'hello,world'"


127.0.0.1:6379> SUBSCRIBE channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "hello world"
1) "message"
2) "channel1"
3) "publish message 'hello,world'"
image.png

LUA脚本

Lua 数据类型和 Redis 数据类型之间转换
原子性:Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。

事务

MULTIEXECDISCARDWATCH 是 Redis 事务相关的命令。
1.MULTI命令用于开启一个事务,它总是返回 OK。户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中。

2.当 EXEC命令被调用时, 所有队列中的命令才会被执行;
当DISCARD , 客户端可以清空事务队列, 并放弃执行事务

3.WATCH命令可以为 Redis 事务提供 check-and-set (CAS)行为。

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用上面的代码, 如果在 WATCH执行之后, EXEC执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。

高可靠

LRU驱动事件

Redis的maxmemory指令用于将可用内存限制成一个固定大小

回收进程如何工作

  1. 一个客户端运行了新的命令,添加了新的数据。
  2. Redi检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
  3. 一个新的命令被执行,等等。

所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。

持久化

RDB vs AOF

RDB

. RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储.

流程

当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:

  1. Redis 调用forks. 同时拥有父进程和子进程。
    子进程将数据集写入到一个临时 RDB 文件中。
  2. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。
    这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

“ N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集,调用 SAVE或者 BGSAVE , 手动让 Redis 进行数据集保存操作。

save 60 1000

RDB的优点

1.非常紧凑的文件,它保存了某个时间点得数据集,非常适用于数据集的备份,比如你可以在每个小时报保存一下过去24小时内的数据,同时每天保存过去30天的数据,这样即使出了问题你也可以根据需求恢复到不同版本的数据集.

  1. RDB是一个紧凑的单一文件,很方便传送到另一个远端数据中心或者亚马逊的S3(可能加密),非常适用于灾难恢复.
  2. RDB在保存RDB文件时父进程唯一需要做的就是fork出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他IO操作,所以RDB持久化方式可以最大化redis的性能.
    与AOF相比,在恢复大的数据集的时候,RDB方式会更快一些.

RDB的缺点

  1. fork子进程来保存数据集到硬盘上,当数据集比较大的时候,fork的过程是非常耗时的,可能会导致Redis在一些毫秒级内不能响应客户端的请求.如果数据集巨大并且CPU性能不是很好的情况下,这种情况会持续1秒,AOF也需要fork,但是你可以调节重写日志文件的频率来提高数据集的耐久度.
    2.意外停止工作,你可能会丢失部分数据

AOF

RDB快照功能并不是非常耐久, 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。

配置文件中打开AOF方式:

appendonly yes

有三种方式:

  1. 每次有新命令追加到 AOF 文件时就执行一次 fsync :非常慢,也非常安全
  2. 每秒 fsync 一次:足够快(和使用 RDB 持久化差不多),并且在故障时只会丢失 1 秒钟的数据。
  3. 从不 fsync :将数据交给操作系统来处理。更快,也更不安全的选择。

. 推荐(并且也是默认)的措施为每秒 fsync 一次, 这种 fsync 策略可以兼顾速度和安全性。

流程

写时复制机制:

  1. Redis 执行 fork() ,现在同时拥有父进程和子进程。
    子进程开始将新 AOF 文件的内容写入到临时文件。
  2. 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
  3. 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
    搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。

AOF优点

  1. 使用AOF 会让你的Redis更加耐久: 你可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync.使用默认的每秒fsync策略,Redis的性能依然很好(fsync是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,你最多丢失1秒的数据.
  2. AOF文件是一个只进行追加的日志文件,所以不需要写入seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用redis-check-aof工具修复这些问题.
  3. Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
    4.AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF缺点

  1. 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
  2. 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

AOF重写

写入命令的不断增加, AOF 文件的体积也会变得越来越大,AOF 文件会进行重建(rebuild)。执行 BGREWRITEAOF 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。

高可用

复制

分为全量复制和增量复制:

当从Redis服务器启动时会向主Redis服务器发送SYNC命令,主Redis服务器接收到SYNC命令后开始进行RDB持久化,并将这期间接收到的写入操作命令都缓存起来,等RDB持久化完成后,将快照和缓存起来的命令一并发送给从Redis服务器,从Redis服务器接收到后开始载入快照和命令,这一过程称之为复制初始化。

复制初始化完成后,每当主Redis接收到写入命令后,就会将命令同步给从Redis服务器,保证主从数据一致。

image.png

主从断开重连后会根据断开之前最新的命令偏移量进行增量复制

Redis哨兵(Sentinel)

image.png

配置了监控主节点,内部会自动发现其他Sentinel节点和从节点。

// 当前Sentinel节点监控 127.0.0.1:6379 这个主节点
// 2代表判断主节点失败至少需要2个Sentinel节点节点同意
// mymaster是主节点的别名
sentinel monitor mymaster 127.0.0.1 6379 2

参考资料:https://blog.csdn.net/men_wen/article/details/72724406

Cluster

Redis Cluster 是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。
Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念., 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。
每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点:

节点 A 包含 0 到 5500号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16383号哈希槽.

微信图片_20180828192952.jpg

优点:

缺点:

参考资料:https://www.cnblogs.com/cjsblog/p/9048545.html
http://www.redis.cn/topics/cluster-tutorial.html
https://www.toutiao.com/a6593195936774619656

原理

VM理解,key如何映射成value。内部主要数据结构有哪些。

架构

单线程,事件驱动。接收到请求会进入队列中,逐个处理。
特点:
1.纯内存访问,相应时间大约在100纳秒
2.非阻塞IO,基于epoll作为IO多路复用技术,不在网络IO上浪费过多时间;
3.单线程避免了线程切换和竞态产生的消耗

但同时缺点也很明显:多核CPU利用率低,阻塞造成噩梦。
阻塞:

  1. 单线程,不合理使用API或数据结构,比如获取大对象。
  1. CPU饱和
  2. 持久化阻塞:fork阻塞,AOF刷盘阻塞
  3. CPU竞争,内存交换,网络问题:链接拒绝,链接拒绝,链接溢出,网络延迟

key映射 VS 数据结构

参考:https://redis.io/topics/internals-vm

每个Redis客户端都有自己的目标数据库,每当客户端执行数据库的读写命令时,目标数据库就会成为这些命令的操作对象。在服务器内部记录客户端连接的目标数据库,这个属性是一个指向redisDb结构的指针。

typedef struct redisClient {
    //..
    // 客户端当前正在使用的数据库
    redisDb *db;
    //..
} redisClient;

redisClient中redisDb的指向redisServer.db数组中的某个元素,即是当前客户端的目标数据库。通过修改redisClient指针,让他指向服务器中的不同数据库,从而实现切换数据库的功能。

int selectDb(redisClient *c, int id) {
    // 确保 id 在正确范围内
    if (id < 0 || id >= server.dbnum)
        return REDIS_ERR;
    // 切换数据库(更新指针)
    c->db = &server.db[id];
    return REDIS_OK;
}

在看redisDb的数据结构

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */
    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    dict *expires;              /* Timeout of keys with a timeout set */
    // 数据库号码
    int id;                     /* Database ID */
    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */
    //..
} redisDb

dict 展开的数据结构如下:

image.png

lookupKey函数,根据key找到value,value对象是redisObject

robj *lookupKey(redisDb *db, robj *key) {
    // 查找键空间
    dictEntry *de = dictFind(db->dict,key->ptr);
    // 节点存在
    if (de) {
        // 取出该键对应的值
        robj *val = dictGetVal(de);
        // 更新时间信息
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
            val->lru = LRU_CLOCK();
        // 返回值
        return val;
    } else {
        // 节点不存在
        return NULL;
    }
}

redisObject数据结构如下:


image.png

在redis-cli命令中,通过如下[debug object key]可以看到其数据结构

d:\Redis>redis-cli
127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> debug object foo
Value at:00007FEC370DA260 refcount:1 encoding:embstr serializedlength:4 lru:8743156 lru_seconds_idle:11
127.0.0.1:6379>

详细解读:

编码方式

struct sdshdr {  
    // buf 中已用长度  
    int len;  
    // buf 中可用长度  
    int free;  
    // 数据空间  
    char buf[];  
};
编码 描述
int 8个字节长整型
embstr 小于44个字节的字符串,(redis3.2版本之前是39字节,之后是44字节)
raw 大于44个字典的字符串

int 编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存短字符串。raw和raw可以用下图来分别:

image.png

上图看出使用均使用redisObject和sds保存数据,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

编码 描述
ziplist 压缩列表,当数据个数小于hash-max-ziplist-enrtries(默认512),单个值均小于hash-max-ziplist-vaue(64)
hashtable 哈希字典表,ziplist不满足时使用
  1. ziplist压缩列表,实现为无指针的数组,内部数据结构:
image.png

特点是紧凑连续数组,还是双向列表,新增删除涉及内存分配/释放有点复杂,适合小长度对象。

  1. hashtable实现为如下,数组[桶]+元素是链表实现。
image.png
编码 描述
ziplist 压缩列表,当数据个数小于list-max-ziplist-enrtries(默认512),单个值均小于list-max-ziplist-vaue(默认64)
linkedlist ziplist无法满足时使用

ziplist不在说明,
linkedlist是双向链表如下,比较好理解,不说明

image.png
编码 描述
intset 整数集合,当元素小于set-max-intset-entries(默认512)时使用
hashtable intset不满足时使用

intset数据结构如下,内部实现是连续空间的数组:

typedef struct intset{
    uint32_t enconding;
   // 元素数量
    uint32_t length;
    //元素的数组    
    int8_t contents[];
}  intset;
编码 描述
ziplist 压缩列表,元素个数小于zset-max-ziplist-entries(默认128),单个值小于zset-max-ziplist-value(默认64)
skiplist ziplist不满足时使用

skiplist结构

typedef struct zskiplistNode{
   //层
     struct zskiplistLevel{
     //前进指针
        struct zskiplistNode *forward;
    //跨度
        unsigned int span;
    } level[];
  //后退指针
    struct zskiplistNode *backward;
  //分值
    double score;
  //成员对象
    robj *obj;
}

1、层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
2、前进指针:用于指向表尾方向的前进指针
3、跨度:用于记录两个节点之间的距离
4、后退指针:用于从表尾向表头方向访问节点
5、分值和成员:跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值.。

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

}zskiplist;
image.png

从结构图中我们可以清晰的看到,header,tail分别指向跳跃表的头结点和尾节点。level 用于记录最大的层数,length 用于记录我们的节点数量。

参考资料:
https://redis.io/topics/internals-vm
https://blog.csdn.net/asd1126163471/article/details/60162221
https://www.cnblogs.com/jaycekon/p/6277653.html

上一篇下一篇

猜你喜欢

热点阅读