Redis学习笔记

Redis读书笔记

redis

1.为什么要有nosql(redis)?

原始架构:

APP->dao->Mysql

数据库存在瓶颈:1、数据量总大小,一个机器放不下 2、数据的索引,一个机器放不下 3、访问量,一个实例不能承受

架构变化:缓存+mysql+垂直拆分

app->dao->cache->mysql*n(垂直拆分,不同服务对应不同的mysql服务器,userinfo一个mysql服务器,bussness1一个服务器,bussiness2一个服务器)

架构变化:mysql读写分离

app->dao->cache->mysql(master,写userinfo) + mysql(salver*n,读userinfo)

架构变化:分库分表+水平拆分+mysql集群

原因:主库的写压力出现瓶颈

app->dao->cache->mysql cluster(数据库集群)(热点放在一个库,其他的放弃其他库,这叫分库。将大表分成小表。)

Redis都可以干什么事儿

缓存,毫无疑问这是Redis当今最为人熟知的使用场景。再提升服务器性能方面非常有效;

ZSet排行榜,如果使用传统的关系型数据库来做这个事儿,非常的麻烦,而利用Redis的SortSet数据结构能够非常方便搞定;

计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;

Set,好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;

简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;

Session共享,以PHP为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

数据量太大、数据访问频率非常低的业务都不适合使用Redis

为什么要用Redis
https://cloud.tencent.com/developer/article/1336755

分布式为什么一定要有redis
https://zhuanlan.zhihu.com/p/50392209
缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 JVM 的销毁而结束。
并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用 Redis 或 Memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。

为什么redis快?1、纯内存操作。2、单线程操作,避免频繁上下文切换。3、数据结构简单。4、采用非阻塞IO多路复用机制。

redis的过期策略和内存淘汰机制

定期删除+惰性删除。即,get到过期值就失效并删除,并定期100ms检查,有过期的key即删除,这种主动删除的方式还是基于概率监检测,即如果随机测试了100个设置了过期时间的key,发现有大于阈值的key是过期的,则重复运行主动删除操作。如果这样还是内存占用还是高,那就进行内存淘汰机制,比如采用LRU的策略。

最终一致性:这是弱一致性的特殊形式;存储系统保证如果没有对某个对象的新更新操作,最终所有的访问将返回这个对象的最后更新的值。

缓存穿透——黑客故意去请求缓存上不存在的数据,导致所有请求都要记录在数据库上,从而造成数据库连接异常。

解决方案:1、布隆过滤器先去布隆过滤器中查询数据库是否有这个key,如果没有,返回null,不用查数据库了。2、缓存中依然缓存这个key,value为null,失效时间5分钟,这样就不会穿透mysql了。

缓存雪崩——缓存同一时间大面积的失效。

解决方案:1、失效时间加一个随机值,便面计提时效。

缓存击穿——热点数据过期后,数据库被击穿。
解决方案:1、采用互斥锁,即其他无法对这个key进行操作,然后load db,然后同步到缓存,解除锁,这样后面的所有请求都不用访问数据库。2、提前采用互斥锁,在value设置一个timeout值,当timeout值接近过期的时候,立刻重新加载。3、不设置过期时间。

并发获取key:如果不要求顺序:加分布式锁,谁拿到锁谁set。要求顺序,给时间戳,时间戳之前的不需要set了。利用Redis的setnx命令。此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。

NoSQL的应用场景,以购物网站为例(多个数据源)——

  • 商品基本信息:关系型数据库mysql
  • 商品描述、详情等多文字类:mongoDB
  • 商品图片:分布式文件系统中
  • 商品的关键字:ISearch,搜索引擎
  • 商品的波段性的热点高频信息:NoSQL数据库,内存数据库,redis

插入知识:mysql三范式
1.各列原子性

2.属性完全依赖于主键,消除部分依赖。

对于上图,容易发现,主键为(学号、课名),此时姓名、系名部分依赖于主键,分数完全依赖于主键。这就不满足二范式。怎么判断是否不满足二范式呢?对每个字段进行检查,如果一个字段是由多个字段决定的,那么多个字段整体应该为一个主键。然后找到被依赖最多的那个,那个就是真正的主键,其他的就存在部分依赖,此时就可以根据这种情况分离这张表。

3.属性不依赖于除主键外的其他属性,消除传递依赖

对于上图,已经满足2范式,但是显然系主任依赖于系名,因此可以拆出。

http://www.blogjava.net/xzclog/archive/2009/01/04/249711.html
不满足二范式会出现的问题,数据冗余,更新异常,删除异常。

NoSQL:

  • KV键值 redis
  • 文档型数据库 mongoDB
  • 列存储数据库 HBase
  • 图关系数据库 Neo4j,InfoGrid

分布式数据库中CAP原理CAP+BASE

  • C Consistency 强一致性 一致性指“all nodes see the same data at the same time”,即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。
  • A Availability 可用性 即服务一直可用,而且是正常响应时间。
  • P Partition tolerance 分区容错性 分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
  • BA Basically Available 基本可用 分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。
    电商大促时,为了应对访问量激增,部分用户可能会被引导到降级页面,服务层也可能只提供降级服务。这就是损失部分可用性的体现。
  • S Soft state 软状态 状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据至少会有三个副本,允许不同节点间副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
  • Eventual Consistency 最终一致性 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。

Redis的五种基本类型及底层实现

底层数据结构

  • SDS 简单动态字符串

数据结构:

len标记长度,alloc记录总分配内存大小,flags记录字节数组属性,buf记录字符串真正的值,char[]型。
知识点:flag分为5,8,16,32,64,其中5没有len,针对不同长度有不同数据结构,。冗余分配,小于1m两倍,大于1m扩展1m。惰性空间释放,甚至不清零,因为采用len标记长度,不需要用/0来标识。优势:二进制安全,获取长度时间复杂度O(1),动态化,惰性释放和冗余分配降低内存分配次数。

  • ADList(A generic doubly linked list) 双向链表

      typedef struct listNode {   // 双向节点
      	struct listNode *prev;
      	struct listNode *next;
      	void *value;    // 空指针,可以被指向任何类型
      } listNode;
      
      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长度
      } list;
    

void*实现多态,len记录长度。迭代操作由专门的迭代器实现。

  • dict 字典

      typedef struct dictEntry {
          void *key;  // 键
          union {
              void *val;
              uint64_t u64;
              int64_t s64;
              double d;
          } v;    // 值
          struct dictEntry *next; // 拉链法解决冲突,下一个节点
      } dictEntry;
      
      typedef struct dictht { // hash表
          dictEntry **table;  // 节点数组
          unsigned long size; // hash表大小
          unsigned long sizemask; // hash表掩码,等于size-1,用于计算hash值
          unsigned long used; // 已有节点数量
      } dictht;
      
      typedef struct dict {   // 字典
          dictType *type; // 各种字典操作方法
          void *privdata; // 私有数据,用于传给操作函数
          dictht ht[2];   // 两个hash表,一个用来存储当前使用的,一个用来rehash
          long rehashidx; // rehash标志位,用于判断是否在rehash和记录rehash进度
          int iterators;  // 迭代器的运行数量
      } dict;
    

如果hash表很大,但是键值对太少,也就是hash表的负载(dictht->used/dictht->size)太小,就会有大量的内存浪费;如果hash表的负载太大,就会影响字典的查找效率。这时候就需要进行rehash将hash表的负载控制在一个合理的范围。

采用惰性refresh的方式,一点一点把老的hashtable中的数据迁移到新的中,所以有两个ht。为避免有子进程持久化时(BGSAVE/BGREWRITEAOF)进行rehash,因此把负载值阈值提升到了5否贼是1.

渐进式rehash
http://redisbook.com/preview/dict/incremental_rehashing.html
简而言之,h[0]存放数据,h[1]是rehash时用的暂存,rehashidx用于标识当前rehash到了哪一个下标。rehashidx表示rehash完毕,rehashidx=0表示开始rehash操作。凡是新增数据都在新表中进行。当h[0]中数据减少为空后,交换h[0]与h[1]指针即完成一次rehash。

  • intset 整数集合

地址在内存中连续,增删改查操作通过地址偏移完成。只能储存整数,通过二分法查询。
重点是升级——

对新元素进行检测,看保存这个新元素需要什么类型的编码;
将集合 encoding 属性的值设置为新编码类型,并根据新编码类型,对整个 contents 数组进行内存重分配。
调整 contents 数组内原有元素在内存中的排列方式,从旧编码调整为新编码。
将新元素添加到集合中。即:集合中的所有元素的编码方式必须和最大或最小的一致。

  • ziplist 压缩表

previous_entry_ength的长度可能是1个字节或者是5个字节,如果上一个节点的长度小于254,则该节点只需要一个字节就可以表示前一个节点的长度了,如果前一个节点的长度大于等于254,则previous length的第一个字节为254,后面用四个字节表示当前节点前一个节点的长度。利用此原理即当前节点位置减去上一个节点的长度即得到上一个节点的起始位置,压缩列表可以从尾部向头部遍历。这么做很有效地减少了内存的浪费。

注意,encoding其实 是encoding+length

  • quicklist 快速列表

在3.2之前,list是根据元素数量的多少采用ziplist或者adlist作为基础数据结构,3.2之后统一改用quicklist,从数据结构的角度来说quicklist结合了两种数据结构的优缺点,复杂但是实用:
链表在插入,删除节点的时间复杂度很低;但是内存利用率低,且由于内存不连续容易产生内存碎片
压缩表内存连续,存储效率高;但是插入和删除的成本太高,需要频繁的进行数据搬移、释放或申请内存
而quicklist通过将每个压缩表用双向链表的方式连接起来,来寻求一种收益最大化。

  • skiplist 跳表

基本数据结构

  • String 1.int 2.embstr 3.sds
  • Hash 1.ziplist 2.dict
  • List 1.quicklist
  • Set 1.inset 2.dict
  • Sorted set 1.ziplist 2.skiplist + dict(skiplist范围操作,dict用于查找)

在自定义的基础数据结构的基础上,redis 通过 redisObject 封装整合成了对外暴露的5中数据结构。 首先看看 redisObject 的定义:

#define LRU_BITS 24
typedef struct redisObject {    // redis对象
    unsigned type:4;    // 类型,4bit
    unsigned encoding:4;    // 编码,4bit
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ // 24bit
    int refcount;   // 引用计数
    void *ptr;  // 指向各种基础类型的指针
} robj;

redis详解系列博客
http://czrzchao.com/redisSourceSds

Redis持久化

一文看懂redis的持久化原理
https://juejin.im/post/5b70dfcf518825610f1f5c16

  • RDB Redis DataBase

    配置文件

      # 时间策略
      save 900 1    900s内如果有1条是写入文件,就触发产生一次快照,即进行备份
      save 300 10   300s内。。10条
      save 60 10000 60s内。。。10000条
      
      # 文件名称
      dbfilename dump.rdb   保存的文件名
      
      # 文件保存路径
      dir /home/work/app/redis/data/  保存的路径
      
      # 如果持久化出错,主进程是否停止写入
      stop-writes-on-bgsave-error yes
      
      # 是否压缩
      rdbcompression yes  如果有完善的监控系统可以禁止
      
      # 导入时是否检查
      rdbchecksum yes   会额外消耗CPU资源
    

    触发方式

  1. save 阻塞当前redis服务器,直到持久化完成。

  2. bgsave 该触发方式会fork一个和主线程一样的子线程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程中。

  3. 本质上都是调用rdbSave方法,区别在于是在主进程还是在子进程。

优势与劣势

  1. 优势:RDB数据结构紧凑,非常适合备份和恢复。生成RDB文件采用fork子线程,不干扰主线程。RDB恢复比AOF更快。

  2. 劣势:RDB数据结构保存了版本,可能出现不兼容。RDB的备份不是实时的,并且bgsave会fork一个子线程导致内存占用大。redis意外宕机会导致最后一次快照的所有修改全部丢失。比如900秒1次,899秒的时候宕机,那这一次就不会写进去。

    底层数据结构

     struct redisService{
          //1、记录保存save条件的数组
          struct saveparam *saveparams;
          //2、修改计数器
          long long dirty;
          //3、上一次执行保存的时间
          time_t lastsave;
      
     }
     
     struct saveparam{
          //秒数
          time_t seconds;
          //修改数
          int changes;
     };
    
  • AOF Append Only File

    配置文件

      # 是否开启aof
      appendonly yes
      
      # 文件名称
      appendfilename "appendonly.aof"
      
      # 同步方式
      appendfsync always/everysec/no
      
      # aof重写期间是否同步
      no-appendfsync-on-rewrite no
      
      # 重写触发配置
      auto-aof-rewrite-percentage 100 当前AOF文件是上次的200%
      auto-aof-rewrite-min-size 64mb  当前的AOF文件超过64mb
      
      # 加载aof时如果有错如何处理
      aof-load-truncated yes
      
      # 文件重写策略
      aof-rewrite-incremental-fsync yes
    

    对于上图有四个关键点补充一下:在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;因此它依然会写入旧的AOF file中,如果重写失败,能够保证数据不丢失。为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个buf,防止新写的file丢失数据。重写是直接把当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。AOF文件直接采用的文本协议,主要是兼容性好、追加方便、可读性高可认为修改修复。

    优势与劣势

  1. 优势——最多丢失2s数据。redis-check-aof可以轻松修正aof文件,即删除掉不正常的命令。aof的可读性比较器强
  2. 劣势——AOF体积大。AOF因为每秒都要写,因此对性能有影响。RDB储存方式更加健壮。

RDB-AOF 混合持久化: 这种持久化能够通过 AOF 重写操作创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态: 至于那些在重写操作执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。

事务

  • MULTI:事务开启的标志
  • DISCARD:放弃事务的标志
  • EXEC:提交事务
  • WATCH:对某个key上锁

WATCH命令的实现

所有被进行写操作的key都会调用multi.c/touchWatchKey函数。这个方法将检查watched_keys这个dict,看看是否有watch标记,如果有,就会被标记为REDIS_DIRTY_CAS,当事务执行的相关key有这个标记的话,就说明在执行事务的同时该数据已经被篡改,因此事务执行失败。

  • 原子性 redis单个执行是原子性,但是不是原子性。即,即使失败也不会回滚。redis官方表示支持原子性,但是实际上由于特殊情况是不会回滚的。事务中的命令要么全部被执行,要么全部都不执行。
  • 一致性 我对于一致性的理解为:从A到B状态,均满足数据库约束。 入队错误:带有不正确入队命令的事务不会执行,符合一致性。执行错误:执行的过程中,有错误的不执行,其他照样执行,符合一致性。Redis进程被终结:内存模式数据总是一致,因为没有持久化,都是空白。RDB模式,会缺少一次数据库快照,因此不一定是一致的。AOF也不一定是一致的。
  • 隔离性,通过乐观锁CAS实现。而且redis是单进程的,显然是隔离的。

事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。所以watch解决的本质上是申明事务的时候可能被打断。

  • 持久性 通过AOF\RDB有部分持久性,但是不够持久。

我的知乎回答:https://www.zhihu.com/question/60189169/answer/626990833

消息订阅发布

每个redis服务器进程都维护一个redis.h/redisServer结构,pubsub_channles是一个字典,字典的key是正在被订阅的频道,value是订阅这个频道的客户端们list,用于保存订阅的频道:

struct redisServer {
    // ...
    dict *pubsub_channels;
    // ...
};

redisServer/pubsub_patterns,是一个list,list中保存着所有和模式相关的信息,所谓模式就是通配符,比如tweet.shop.*。这个就是一个模式。

struct redisServer {
    // ...
    list *pubsub_patterns;
    // ...
};

typedef struct pubsubPattern {
    redisClient *client;
    robj *pattern;
} pubsubPattern;

主从复制

主从复制,主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,master以写为主,slave以读为主。用处为读写分离与容灾恢复。info replication

用法:

  1. 配从库不配主库

  2. 从库配置:salveof 主库ip 主库端口,每次与master断开之后,都需要重新连接,除非配置进redis.conf。

  3. 修改配置文件细节操作

  4. 一主二从 主挂了从不会代替,从挂了上线就失去了从的身份

  5. 薪火相连 去中心化,每个都是中间节点

  6. 反客为主 SALVEOF no one

主从复制的实现原理

  1. 从节点中保存主节点信息,包括masterhost与masterport。slaveof是异步指令,实际的复制过程在slaveof之后进行。
  2. 建立socket连接,从节点连接主节点,并建立一个专门处理复制工作的文件事件处理器,负责后序复制工作。
  3. 发送ping命令。从节点ping主节点,看看是否连接成功。
  4. 身份验证,从节点想主节点进行身份验证。
  5. 发送从节点端口信息。从节点向主节点发送监听的端口号。执行info replication可以显示。
  6. 数据同步阶段——从节点向主节点发送psync命令,开始同步。此阶段之前,主节点不是从节点的客户端,到了这个阶段后,互为客户端。
  7. 命令传播阶段。主节点将自己执行的写命令发送给从节点,从节点接受命令并执行,从而保证主从一致性。主从还维持心跳机制ping和replconf ack。repl-disable-tcp-nodela=no的时候TCP马上发送主节点数据包,带宽增加延迟变小,否则会对包进行合并从而减少带宽,降低频率,增加延迟。

全量复制与增量复制

  • 全量复制 非常重量的操作,主节点fork子进程RDB、从节点接收RDB并读取、从节点清空老数据等都是阻塞的、从节点bgrewriteaof有额外消耗
  1. 主节点无法进行部分复制/子节点要求全量复制。
  2. 主节点收到全量复制命令后,执行bgsave,在后台生成RDB文件,使用复制缓冲区记录此时刻开始的写命令。
  3. 发送RDB给从,从先删除自己的旧数据,然后载入RDB
  4. 从载入主的缓冲区命令
  5. 如果从开启AOF,会触发bgrewritebg,从而保证AOF文件更新至主节点最新状态。
  • 增量复制
  1. 复制偏移量,代表主节点向从节点传递的字节数。如果主是A,从是B,即要复制的增量就是A-B.
  2. 主节点维护一个复制积压缓冲区,固定长度,先进先出,作用是备份主节点最近发送给从节点的数据。这个本质上是为了防止从节点断线之后,再上线的时候可以通过增量复制的形式恢复同步。(这个功能默认是被注释掉的,repl-backlog-size)
  3. runid,如果从节点断线重连后发现主机runid没变,那就尝试增量,否则全量。

心跳机制

  • 心跳包

主-从 ping,官方文档中是从向主发送ping命令,默认10s。

replconf ack,1s发送一次,命令格式为REPLCONF ACK{offset}。作用:1.实时监测主节点状态。2.比较offset,如果不一致则从复制挤压缓冲区中拿。3.保证从节点的数量和延迟,如果lag值很高,就说明从节点过多,主机会拒绝执行写命令。

sentinel 哨兵模式

作用:在复制的基础上,哨兵实现了自动化的故障恢复。核心:主节点的自动故障转移。

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

https://www.cnblogs.com/kismetv/p/9609938.html

  1. 主观下线
  2. 客观下线
  3. 投票选举哨兵领导者
  4. 哨兵领导者进行故障转移,从节点选举为主节点的原则:先过滤掉不健康的从节点,选择优先级最高的,选择复制偏移量最大的,选择runid最小的。
  5. 通过slaveof no one使得从节点变为主节点,并通过slaveof命令使其他节点成为从节点。之前的主节点变成新的主节点的从节点。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值