redis

redis中存储的数据在内存中,取内存数据速度比去磁盘取数据快多了,能够减小数据库压力。内存存储开销比较大所以redis一般存一些比较常用的数据。
redis速度很快,官方数据每秒10w+QPS。(采用单线程,多路I/O复用模型)

持久化

redis数据存储在内存中,如果不进行持久化,重启系统后数据就丢失了

1. rdb

每隔一段时间对数据进行快照存储
redis先fork一个子进程,子进程将数据集写入临时rdb文件中,等全部写完以后用这个rdb文件替换掉之前的rdb文件并删除旧的rdb。

2. aof

AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾。

两者比较

RDB 方式可以保存过去一段时间内的数据,并且保存结果是一个单一的文件,可以将文件备份到其他服务器,并且在回复大量数据的时候,RDB 方式的速度会比 AOF 方式的回复速度要快。

AOF 方式默认每秒钟备份1次,频率很高,它的操作方式是以追加的方式记录日志而不是数据,并且它的重写过程是按顺序进行追加,所以它的文件内容非常容易读懂。可以在某些需要的时候打开 AOF 文件对其编辑,增加或删除某些记录,最后再执行恢复操作。保存的数据比较完整。

主从复制

主从复制写的详细的一篇文章

单节点就算进行了持久化但是遇到了磁盘故障也会导致丢失。主从复制可以避免单节点故障问题(全部节点都坏概率太小了),又可以Master/Slave读写分离,缓解Master读的压力。不过由于所有的写操作都是在 Master 节点上操作,然后同步到 Slave 节点,那么同步就会有一定的延时。

原理

同步和命令传播

  1. 从服务器发出SLAVEOF命令后,从服务器会向主节点发送SYNC命令。(新的命令叫PSYNC,可以解决断线重复制的问题)
  2. 主服务器执行 BGSAVE 命令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从开始执行的所有写命令。
  3. 主服务器的 BGSAVE 命令执行完毕后,主服务器会将 BGSAVE 命令生成的 RDB 文件发送给从服务器,从服务器接收此 RDB 文件,并将服务器状态更新为RDB文件记录的状态。
  4. 主服务器把缓冲区的写命令写发给从服务器,从服务器执行这些命令。
  5. 同步完成后,主服务器遇到新的写命令后,把这些写的命令传播给从服务器。然后从服务器执行后就能保持一致了。

哨兵

如果master服务器挂了那么整个系统就无法用了。哨兵模式监控redis系统的运行状态,当master挂了以后可以主动挑选出一个从节点担当主节点(自动通过投票机制),并建立和其他节点的关系。

redis集群

可以在多个 Redis 节点之间进行数据共享
集群+主从复制实现了redis高可用,又分布式服务又不容易挂掉。
redis集群不支持事务,rename(因为涉及了多个key)

Redis Cluster | 相关博客

去中心化,每个节点都是平等的并实现了redis的分布式存储

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。

所以,我们假设现在有3个节点已经组成了集群,分别是:A, B, C 三个节点,它们可以是一台机器上的三个端口,也可以是三台不同的服务器。那么,采用哈希槽 (hash slot)的方式来分配16384个slot 的话,它们三个节点分别承担的slot 区间是:
节点A覆盖0-5460;
节点B覆盖5461-10922;
节点C覆盖10923-16383.

这种哈希槽的分配方式有好也有坏,好处就是很清晰,比如我想新增一个节点D,redis cluster的这种做法是从各个节点的前面各拿取一部分slot到D上。

一致性哈希|相关博客

hash(服务器的IP地址) % 2^32
2^32 无符号整形的最大值
通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上。
接下来使用如下算法定位数据访问到相应服务器: 将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器!

redis缓存雪崩/缓存穿透

一致性哈希博客里面如果不使用一致性哈希,使用普通哈希函数,如果其中有一台服务器宕机了,哈希函数会大概率定位错服务器导致大多数缓存失效,这就是缓存雪崩。
正常使用缓存的流程是拿到key后先去redis查询,没有的话再去数据库查询,如果查到数据以后保存key和value在缓存。恶意攻击就会故意拿一些没有的key去查询,这会对数据库造成压力,这就是缓存穿透。(可以把key查不到的值稍微缓存个一分钟解决。)

redis的事务

redis事务是通过MULTI,EXEC,DISCARD和WATCH四个原语实现的。
先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的所有命令

不保证原子性:redis同一个事务中如果有一条命令执行失败(参考事务中的2类错误,执行失败是指第二类错误),其后的命令仍然会被执行,没有回滚,这也就是:Redis部分支持事务。


redis的数据类型(String、List、Set、Hash、ZSet)

redis的内部整体的存储结构就是一个大的hashmap

Redis中的一个对象的结构体包含:类型(上面五种),编码方式(底层实现),指向对象的值(*ptr)等。

typedef struct redisObject {
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    // 引用计数
    int refcount;
    // 指向实际值的指针
    void *ptr;
} robj;

底层数据结构

redis.png

1.String 相关博客

string针对不同的字符串会相应转换成三种不同的底层实现:

  • 整数,存储字符串长度小于21且能够转化为整数的字符串。(int)
  • EMBSTR,存储字符串长度小于等于44(以前是39)的字符串(REDIS_ENCODING_EMBSTR_SIZE_LIMIT)。
  • 剩余情况使用sds进行存储。(raw)

为什么以44为界限分配:上面redisObject对象头占据了16字节,然后sds对象头capacity,len,flags各占一个字节,字符串结尾\0还要占一个字节,redis的内存分配器jemalloc给embstr最多64字节,减去上面的就变成最长44字节了。

embstr和sds区别:embstr的创建只需要分配一次内存,而sds需要两次。同样释放内存也一样
不过embstr是只读的,如果需要修改他的值需要转换成raw后才能进行修改

embstr 存储形式
是将 RedisObject 对象头和 SDS 对象连续存在一起,使用 malloc 方法一次分配。而 raw 存储形式不一样,它需要两次 malloc,两个对象头在内存地址上一般是不连续的

SDS

struct sdshdr {
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 数据空间
    char buf[];
};

SDS本质上就是char *,不过多了sdshdr结构的存在,获取字符串长度的时间复杂度为O(1),直接读取len就可以了。有free的存在,可以修改字符串长度时候杜绝缓冲区溢出,并减少修改字符串时带来的内存重分配次数(C 字符串每次增长或者缩短, 都要对保存这个 C 字符串的数组进行一次内存重分配操作)。

2. List

压缩链表

相关博客1
相关博客2
列表元素少而且里面元素长度也短的时候用压缩链表比较好
压缩链表将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。

其实redis底层存储不是简单的链表而是快速链表quicksort,是由双链表+压缩链表组合而成。将多个压缩链表用双指针串起来。既满足了快读插入删除的性能,又不会出现太大的空间冗余

双向链表
// list 节点
typedef struct listNode {
    // 前驱节点
    struct listNode *prev;
    // 后继节点
    struct listNode *next;
    // 节点值
    void *value;
} listNode;

// redis 双链表实现
typedef struct list {
    listNode *head;                      // 表头指针
    listNode *tail;                      // 表尾指针
    void *(*dup)(void *ptr);             // 节点值复制函数
    void (*free)(void *ptr);             // 节点值释放函数(函数指针)
    int (*match)(void *ptr, void *key);  // 节点值对比函数
    unsigned long len;                   // 链表包含的节点数量
} list;

当每增加一个listNode的时候,就需要重新malloc一块内存

节点包含前驱指针和后继指针,可以使用迭代轻松遍历

header和tail,快速定位头部和尾部。对于在链表的头部或尾部进行插入节点的时间复杂度全部为O(1)

len:获取链表长度的时间复杂度为O(1)。

3.Hash

当对象数目不多且内容不大的时候也可以用压缩链表,哈希对象是按照key1,value1,key2,value2这样的顺序存放来存储的。

redis hashtable 解决哈希冲突使用链地址法(python的dict用的是开放寻址法)

typedef struct dictht {
    dictEntry **table;             // 哈希表数组
    unsigned long size;            // 哈希表数组的大小
    unsigned long sizemask;        // 用于映射位置的掩码,值永远等于(size-1)
    unsigned long used;            // 哈希表已有节点的数量
}dictht;

Redis的字典的值只能是字符串,另外它rehash用的是渐进式rehash策略(一次性rehash过于耗时)。
渐进式rehash在rehash的同时,保存新旧两个hash结构,查询时会同时查询两个hash结构,然后在后续的定时任务以及hash操作指令中,循序渐进地将旧hash内容迁移到新hash结构中。迁移完成后,就会使用新的hash结构和取而代之。

4.Set

集合对象的编码可以是intset或者hashtable。
intset是一个整数集合,里面存的为某种同一类型的整数

5.ZSet

类似于java中sortedset和hashmap的结合体,一方面它是一个set保证了value的唯一性,另一方面它可以为每个value赋值一个score,代表value的排序权重。它的内部实现:跳跃链表。
普通链表按照score进行排序。意味着如果新元素要进来时需要遍历链表才能找到特定位置的插入点。所以跳跃链表使用的是层级索引(最多32层)


跳跃链表

python中redis的基本操作

import redis
# StrictRedis默认参数host='localhost', port=6379, db=0
# python3 redis 默认get字符串的时候 返回的是byte,需要设置decode_responses为True
r = redis.StrictRedis(password='your password',decode_responses=True)

1.string

r.set('name', 'alan', ex=20) #ex为过期时间单位秒
r.mset(name1='peter', name2='ben') #批量设置

r.get('name1') #获取值,没有的话会返回None

2.hash

r.hset('student', 'alan', 12) # student = {'alan':12}
r.hget('student', 'alan') # 返回12 字符串类型
r.hgetall('student') # 返回{'alan': '12'} dict类型

dic={"ben":10, "peter":22}
r.hmset("student",dic) #批量设置

r.hlen('student') # 获取键值对个数
r.hkeys('student') # 获取所有keys 返回一个数组
r.hvals('student') # 获取所有vals
r.hexists('student', 'alan') #返回True
r.hdel('student_age', 'alan')  #删除指定key的键值对

3.list

blpop/brpop阻塞读 消息延迟几乎为0。阻塞读在队列没有数据的时候会进入休眠状态,一旦数据来临就会醒过来。

r.lpush('l_name', 1) #添加元素至列表最左边
r.lpush('l_name', 2, 3) # [3, 2, 1]
r.rpush('l_name', 4)  # 添加元素至列表最右边
r.llen('l_name') # 查看列表元素个数
r.lpop('l_name') # 弹出列表最左边元素
r.lrange('l',0, -1) # 获取范围内所有元素都是闭区间,-1代表最后一个元素

4.set

r.sadd('s_name', 'alan')
r.smembers('s_name') # 获取所有元素
r.scard('s_name') # 元素个数

r.sadd('s2_name','peter')
r.sdiff('s_name','s2_name') #获取在s_name但不在s2_name中的元素
r.sismember('s_name', '12') #检查元素是否在set中
r.spop('s_name') # 移除最右边的元素

5.zset

ZADD key score member [score member ...]

redis分布式锁

分布式锁的实现目标就是要在redis里面占坑,当别的进程也要来占坑时,发现坑已经被人占了,只能放弃或稍后再来占。
占坑一般使用setnx(set if not exists)指令,只允许一个客户端来占坑。先来先占,在调用del指令释放坑。
为了防止死锁我们要拿到坑位以后先设置个过期时间,保证锁自动释放。
redis2.8以后加入了set指令的扩展参数,使set和expire可以一起执行。

> set lock true ex 5 nx
...
> del lock
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349