(转) Redis 基础知识笔记记录

ReZero lol

文章笔记参考链接来自作者 敖丙

开源项目:https://github.com/AobingJava/JavaFamily

以及 Redis 设计与实现

以及 钱大的 Redis 深度历险

Redis 常见应用

  • 记录帖子的点赞数、评论数和点击数 (hash)。
  • 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。
  • 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。
  • 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。
  • 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。
  • 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。
  • 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。
  • 收藏集和帖子之间的关系 (zset)。
  • 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。
  • 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。
  • 数据推送去重Bloom filter pv,uv统计

小菜

delay queue

Redis 可以做单消费组的消息对列,但没有 ack 保证的原因意味着可靠性也没有保证。

redis 队列可能出现空队列时 pop 的空轮询,这种情况下 一般采用sleep(1000) 让其他线程获取可执行的机会

bitmap

签到,用户日活

可以设 key 为前缀:用户id:年月, setbit sign:123:1909 0 1 代表用户 123 19年9月 第 1(就是0的时候)天签到 bitcount来统计签到天数

HyperLogLog

Page visit,简单的 incrby 就可
User visit 就无法简单处理,如果每个页面一个 set 就很爆炸
HyperLoglog的 pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是,pfcount 和 scard 用法是一样的,直接获取计数值。

bloom 过滤器

bf.add bf.exists 比如新闻推荐,url爬取等的去重操作

布隆过滤器的initial_size估计的过大,会浪费存储空间,估计的过小,就会影响准确率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避免实际元素可能会意外高出估计值很多。

布隆过滤器的error_rate越小,需要的存储空间就越大,对于不需要过于精确的场合,error_rate设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。

大致说下原理:

初始化 n 个元素长度的的数组,提供 k 个hash 函数,从数组的角度来看,当有元素hash对数组的某个索引命中时,那么这个地方就置为1,注意的是数组的位置只有第一次设置为1后后面的设置就不再起效

这样当我们对 给定值应用了 k 次 hash后发现全部落在数组值为 1 的地方,那么我们就判定这个元素在数组中是存在的。

tips: 可以看出来这东西并不绝对,有一定的误差,至于怎么把这个误差减到最少,需要了解下相关的推论。

Redis 数据结构

基本五个:

String、Hash、List、Set、SortedSet

然后还有:HyperLogLog、Geo、Pub/Sub

以及 Redis Module,比如:BloomFilter,RedisSearch,Redis-ML

  1. String: 常见缓存,共享session等,强烈不建议复杂结构做 jsonToString 之类的操作。

  2. hash, map一般不常用,难见场景

  3. list:常用来存储列表之类的数据,比如lrange 命令,读取某个闭区间内的元素,可以基于 List 实现下拉那种分页查询,再就是阻塞队列了。

  4. set,一般用来运算并交差集之类的,比如取两个人的共同好友之类的

  5. zset:根据 score 作为权重的排序


String: 类似一个ArrayList<Byte|Char>, 加了长度限制字段,目的是尽可能高效且不浪费利用空间并且防范缓冲区溢出等问题。


list:类似 linked list,双向链表, 可实现队列,栈效果


hash:类似 hashMap 仅通过数组加链表解决hash冲突,实际上字典结构的内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的,但是在字典扩容缩容时,需要分配新的 hashtable,然后进行 渐进式搬迁 。

渐进式 rehash:就是持有两个hashtable,查地时候去两个里面查,这样避免一次性大扩容单线程的 redis 顶不住。

扩容条件:正常情况下,当 hash表中 元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新数组是 原数组大小的 2倍。不过如果 Redis正在做 bgsave(持久化命令),为了减少内存也得过多分离,Redis尽量不去扩容,但是如果 hash 表非常满了,达到了第一维数组长度的 5 倍了,这个时候就会 强制扩容。
当 hash 表因为元素逐渐被删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少 hash 表的第一维数组空间占用。所用的条件是 元素个数低于数组长度的 10%,缩容不会考虑 Redis 是否在做 bgsave。


集合 set:Redis 的集合相当于 Java 语言中的 HashSet,它内部的键值对是无序、唯一的。它的内部实现相当于一个特殊的字典,字典中所有的value 都是一个值 NULL。

1
2
3
4
5
6
7
8
9
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;

  1. intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。因此使用时需同时满足元素皆是整数且数量不超过max-intset-entries

  2. hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet
    集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null。


zset:跳表
基本结构类似于一个多层二分链表,但是为了避免插入引起的 O(N) 效率问题,采用了这样的方案:不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是 3
,那么就把它链入到第 1 层到第 3 层这三层链表中。


基础知识

  1. 大量的key设置相同的过期时间不可取,尽量分散随机防止同时失效时涌入大量的请求造成缓存雪崩。或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

  2. scan 取代keys 带来的影响:Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key
    列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。不过,增量式迭代命令也不是没有缺点的:举个例子,使用 SMEMBERS命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。

  3. 异步队列,rpush生产消息,lpop消费消息,如果不sleep的话blpop可以阻塞消息的到来。使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列,但该模式在消费者下线的情况下,生产的消息会丢失。

  4. 延时队列的实现:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

  5. 持久化:RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。其实就类似与全表与日志恢复的补偿措施。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
RDB: 

优点:
他会生成多个数据文件,每个数据文件分别都代表了某一时刻Redis
里面的数据,这种方式,有没有觉得很适合做冷备,完整的数据运维设置定时任务,定时同步到远端的服务器,比如阿里的云服务,这样一旦线上挂了,你想恢复多少分钟之前的数据,就去远端拷贝一份之前的数据就好了。

RDB对Redis的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的,而且他在数据恢复的时候速度比AOF来的快。

缺点:
RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。AOF则最多丢一秒的数据,数据完整性上高下立判。

还有就是RDB在生成数据快照的时候,如果文件很大,客户端可能会暂停几毫秒甚至几秒,你公司在做秒杀的时候他刚好在这个时候fork了一个子进程去生成一个大快照,哦豁,出大问题。

AOF

优点:
上面提到了,RDB五分钟一次生成快照,但是AOF是一秒一次去通过一个后台的线程fsync操作,那最多丢这一秒的数据。

AOF在对日志文件进行操作的时候是以append-only的方式去写的,他只是追加的方式写数据,自然就少了很多磁盘寻址的开销了,写入性能惊人,文件也不容易破损。

AOF的日志是通过一个叫非常可读的方式记录的,这样的特性就适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。


缺点:
一样的数据,AOF文件比RDB还要大。

AOF开启后,Redis支持写的QPS会比RDB支持写的要低,他不是每秒都要去异步刷新一次日志嘛fsync,当然即使这样性能还是很高,我记得ElasticSearch也是这样的,异步刷新缓存区的数据去持久化,为啥这么做呢,不直接来一条怼一条呢,那我会告诉你这样性能可能低到没办法用的,大家可以思考下为啥哟。

综上:出事第一时间用RDB恢复,然后AOF做数据补全
  1. sync:持久化时的断点可能造成影响,所以需要定时 sync,可以 1 s 执行一次,这样最多丢失 1s 的数据。

  2. RDB 原理: fork(Linux 子进程的那个 fork)& copy on write

  3. pipeline : 可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline
    批次指令的数目。

  4. 同步机制 : Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB
    镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

  5. 集群:

    • Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
    • Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
  6. 缓存穿透:构建不存在于缓存和数据库的 key 发起攻击。解决方案是用参数校验比如分页大小不要没个限制之类的,或者 bloom 过滤,不存在就设置空对象到缓存去,同时给这个key一个较短的过期时间。

  7. 缓存击穿:极度热点的 key 失效的瞬间。 解决方案:不要过期或者加个互斥锁。


事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。

事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免MySQL被打死。

事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。


集群实例

  1. 我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。
    这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。

  2. 数据持久化,持久化的话是Redis高可用中比较重要的一个环节,因为Redis数据在内存的特性,持久化必须得有,我了解到的持久化是有两种方式的。

    • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。
    • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。
    • 两种方式都可以把Redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备,AOF
      更适合做热备,比如我杭州的某电商公司有这两个数据,我备份一份到我杭州的节点,再备份一个到上海的,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧,这灾备也就是异地容灾,地球毁灭他没办法。

      tip:两种机制全部开启的时候,Redis在重启的时候会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。

  3. 哨兵组件的功能

    • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
    • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
    • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
    • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
  4. 读写分离的数据同步方案:启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。

  5. Linked hash map 实现 LRU 缓存,accessOrder 的实现就是 move node to the last

  6. 关于 key 的失效: 定期删除和惰性删除,如果不能满足这两种那就只有缓存淘汰了。淘汰的策略有这些:Cache invalidation

分布式问题

  1. 多个系统同时操作(并发)Redis带来的数据问题:

    1. 某个时刻,多个系统实例都去更新某个 key。可以基于 Zookeeper 实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。

    2. 你要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。

    3. 每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。 感觉有点像cas

  2. 缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,这个时候一般是取舍,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。串行的问题不用说自然就是效率问题,拉垮性能。

线程模型

Redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 Socket,根据 Socket 上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含 4 个部分:

  • 多个 Socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

秒杀系统设计

预览

  1. 秒杀也建个微服务,对应的秒杀库

  2. 高可用: Redis集群,主从同步、读写分离,我们还搞点哨兵,开启持久化

  3. Nginx 负载均衡打 tomcat 群

  4. 资源静态化,能放 CDN 就放

  5. Button loading or disabled

  6. 后端限流

    • 阿里的Sentinel、Hystrix
    • 库存预热:加载到 redis 中,配合 lua
  7. 限流&降级&熔断&隔离。限流,顶不住就挡一部分出去但是不能说不行,降级,降级了还是被打挂了,熔断,至少不要影响别的系统,隔离,你本身就独立的,但是你会调用其他的系统嘛,你快不行了你别拖累兄弟们啊。

  8. 削峰填谷: 上 mq

全栈

  1. Node js

    • Master-Worker 模式,Node 提供了 child_process 模块,或者说 cluster 模块也行,可以利用 child_process 模块直接创建子进程。fork的目的是为了利用 cpu 资源

    • 句柄传递去掉主进程代理,Egg.js 将它们放到了一个单独的进程上去执行,这个进程就叫 Agent Worker,简称 Agent,专门用来处理一些公共事务

Redis 命令学习

  1. 大key检查: redis-cli --bigkeys -i 0.01

  2. 模块采样,查看高频访问 redis-cli --host 192.168.x.x --port 6379 monitor

  3. 时延查询 redis-cli --host 192.168.x.x --port 6379 --latency不仅是物理网络的时延,还和当前的 Redis 主线程是否忙碌有关。如果你发现 Unix 的 ping 指令时延很小,而 Redis 的时延很大,那说明 Redis 服务器在执行指令时有微弱卡顿。

  4. 将远程的 Redis 实例备份到本地机器,远程服务器会执行一次bgsave操作,然后将 rdb 文件传输到客户端。./redis-cli --host 192.168.x.x --port 6379 --rdb ./user.rdb

  5. 观察主从服务器之间都同步了那些数据,可以使用 redis-cli 模拟从库 ./redis-cli --host 192.168.x.x --port 6379 --slave 从库连上主库的第一件事是全量同步,所以看到上面的指令卡顿这很正常,待首次全量同步完成后,就会输出增量的 aof 日志。

  6. 更新缓存时建议直接删缓存,如果没有那么从数据库取

    • 如果先走数据库,后操作缓存(CacheAsidePattern)存在缓存删除失败的情况,处理方案:在高并发下表现优异,在原子性被破坏时表现不如意
      • 将需要删除的key发送到消息队列中
      • 自己消费消息,获得需要删除的key
      • 不断重试删除操作,直到成功
    • 先删除缓存,再更新数据库:在高并发下表现不如意,在原子性被破坏时表现优异
      • 将操作积压到队列里进行串行化的操作,避免并发带来的问题

Redis 实现消息队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public interface MessageListener {
/**
* Callback for processing received objects through Redis.
*
* @param message message must not be {@literal null}.
* @param pattern pattern matching the channel (if specified) - can be {@literal null}.
*/
void onMessage(Message message, @Nullable byte[] pattern);

}

/**
* Class encapsulating a Redis message body and its properties.
*
* @author Costin Leau
* @author Christoph Strobl
*/
public interface Message extends Serializable {

/**
* Returns the body (or the payload) of the message.
*
* @return message body. Never {@literal null}.
*/
byte[] getBody();

/**
* Returns the channel associated with the message.
*
* @return message channel. Never {@literal null}.
*/
byte[] getChannel();
}

RedisTemplate.convertAndSend(chanenl, message)
  1. redis队列监听器的监听机制是:使用一个线程监听队列,队列有未消费的消息则取出消息并生成一个新的线程来消费消息。如果你还记得,我开头说的是由于redis单线程特性,因此我们用它来做消息队列,但是如果监听器每次接受一个消息就生成新的线程来消费信息的话,这样就完全没有使用到redis的单线程特性,同时还会产生线程安全问题。

  2. 一个通道只有一个消费者的解决办法:最简单的办法莫过于为onMessage
    ()方法加锁,这样简单粗暴却很有用,不过这种方式无法控制队列监听的速率,且无限制的创造线程最终会导致系统资源被占光。解决方案:RedisMessageListenerContainer类中有一个方法setTaskExecutor
    (Executor taskExecutor)可以为监听容器配置线程池。配置线程池以后,所有的线程都会由该线程池产生,由此,我们可以通过调节线程池来控制队列监听的速率。

  3. 上锁样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory factory) {

return new RedisLockRegistry(factory, "lock-name", timeForRelease);

}

@Component
public class RedisListener implement MessageListener{

@Autowrite
private RedisLockRegistry redisLockRegistry;

private static final Logger LOGGER = LoggerFactory.getLogger(RedisListener.class);

@Override
public void onMessage(Message message,byte[] pattern){
Lock lock=redisLockRegistry.obtain("lock");
try{
lock.lock(); //上锁
LOGGER.debug("从消息通道={}监听到消息",new String(pattern));
LOGGER.debug("从消息通道={}监听到消息",new String(message.getChannel()));
LOGGER.debug("元消息={}",new String(message.getBody()));
// 新建一个用于反序列化的对象,注意这里的对象要和前面配置的一样
// 因为我前面设置的默认序列化方式为GenericJackson2JsonRedisSerializer
// 所以这里的实现方式为GenericJackson2JsonRedisSerializer
RedisSerializer serializer=new GenericJackson2JsonRedisSerializer();
LOGGER.debug("反序列化后的消息={}",serializer.deserialize(message.getBody()));
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock(); //解锁
}
}
}

分布式锁

常见锁

  1. 基于 MySQL 中的锁:MySQL 本身有自带的悲观锁 for update 关键字,也可以自己实现悲观/乐观锁来达到目的;但是要
  2. 基于 Zookeeper 有序节点:Zookeeper 允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁;
  3. 基于 Redis 的单线程:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像 SETNX(set if not exists) 这样的指令,本身具有互斥性;

Redis 锁的释放

设置时间怕临界区还没结束就放了,所以不要长任务。同时因为这个A超时释放了那期间就会有别的B捡起锁,这样可能A执行完了事务把B的锁给释放了,所以还有种方案就是值设置为随机数,这样删key的时候先看看是不是自己的那个锁,这个方案需要Lua
的支持来保证 check 和 delete 为一个原子操作。

Redlock

服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从从机那里获取到这把锁,为了解决这个问题,Redis 作者提出了一种 RedLock 红锁 的算法 (Redission 同 Jedis):

分布式锁

官方文档也在 SETNX 文档中提到了这样一种思路:把 SETNX 对应 key 的 value 设置为 <current Unix time + lock timeout + 1>,这样在其他客户端访问时就能够自己判断是否能够获取下一个 value 为上述格式的锁了。

持久化

  1. redis 库

    1
    2
    3
    4
    5
    6
    7
    typedef struct redisDb { 
    int id; // 数据库ID标识
    dict *dict; // 键空间,存放着所有的键值对
    dict *expires; // 过期哈希表,保存着键的过期时间
    dict *watched_keys; // 被watch命令监控的key和相应client
    long long avg_ttl; // 数据库内所有键的平均TTL(生存时间)
    } redisDb;
  2. RDB: 根据修改次数和时间来决定是否调用 bgsave 生成 RDB 文件。RDB不会存储过期键,创建时会进行相应的检查。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct redisServer{
    // 修改计数器
    long long dirty;

    // 上一次执行保存的时间
    time_t lastsave;

    // 参数的配置
    struct saveparam *saveparams;
    };
  3. AOF: 以 redis 序列化协议的方式记录写命令,为了压缩指令集的大小,比如将多个push合为一个push,BGREWRITEAOF 提供类似该命令效果的重写功能配置,重写不依赖于现有的aof
    文件,而是基于现有库分析拿到重写的指令集数据。类似bgsave fork 子进程重写时会出现不一致的问题,是通过加个临时缓冲区暂存后执行命令解决的。混合模式下 AOF指的是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志

    1. 命令追加:命令写入aof_buf缓冲区
    2. 文件写入:调用flushAppendOnlyFile函数,考虑是否要将aof_buf缓冲区写入AOF文件中
    3. 文件同步:考虑是否将内存缓冲区的数据真正写入到硬盘
  4. RDB 执行优先级低于 AOF,此外关于持久化相关的配置:

    script
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    redis持久化,两种方式
    1、rdb快照方式
    2、aof日志方式

    ----------rdb快照------------
    save 900 1
    save 300 10
    save 60 10000

    stop-writes-on-bgsave-error yes
    rdbcompression yes
    rdbchecksum yes
    dbfilename dump.rdb
    dir /var/rdb/

    -----------Aof的配置-----------
    appendonly no # 是否打开 aof日志功能

    appendfsync always #每一个命令都立即同步到aof,安全速度慢
    appendfsync everysec
    appendfsync no 写入工作交给操作系统,由操作系统判断缓冲区大小,统一写入到aof 同步频率低,速度快


    no-appendfsync-on-rewrite yes 正在导出rdb快照的时候不要写aof
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
  5. 内核缓冲区成功写入认为已经写入,但到磁盘缓存后再到落盘这中间两步仍然无法避免停电的问题,而Linux 默认30 秒将缓冲区做真正的写提交从而落盘,除此之外 fsync 命令也有相应的效果。

Redis stream

  1. 消息用不同的 ID 来区分

    • Consumer Group:消费者组,可以简单看成记录流状态的一种数据结构。消费者既可以选择使用 XREAD命令进行独立消费,也可以多个消费者同时加入一个消费者组进行 组内消费。同一个消费者组内的消费者共享所有的 Stream 信息,同一条消息只会有一个消费者消费到,这样就可以应用在分布式的应用场景中来保证消息的唯一性。
    • last_delivered_id:用来表示消费者组消费在 Stream 上 消费位置 的游标信息。每个消费者组都有一个 Stream 内 唯一的名称,消费者组不会自动创建,需要使用 XGROUP CREATE指令来显式创建,并且需要指定从哪一个消息 ID开始消费,用来初始化 last_delivered_id 这个变量。
    • pending_ids:每个消费者内部都有的一个状态变量,用来表示 已经 被客户端 获取,但是 还没有 ack的消息。记录的目的是为了保证客户端至少消费了消息一次,而不会在网络传输的中途丢失而没有对消息进行处理。如果客户端没有 ack,那么这个变量里面的消息 ID 就会越来越多,一旦某个消息被 ack,它就会对应开始减少。这个变量也被 Redis 官方称为 PEL (Pending Entries List)。
  2. Stream 消息太多怎么办?

设置限定消息长度,对老消息进行淘汰

  1. PEL 丢失问题:在客户端消费者读取 Stream 消息时,Redis 服务器将消息回复给客户端的过程中,客户端突然断开了连接,消息就丢失了。但是 PEL 里已经保存了发出去的消息 ID,待客户端重新连上之后,可以再次收到 PEL中的消息 ID列表。不过此时 xreadgroup 的起始消息 ID 不能为参数 > ,而必须是任意有效的消息 ID,一般将参数设为 0-0,表示读取所有的 PEL 消息以及自 last_delivered_id 之后的新消息。

集群

[集群参考](https://mp.weixin.qq.com/s?__biz=MzUyMTg0NDA2Ng==&mid=2247484047&idx=1&sn=9b8a62d204ed82805a997878500eef16&chksm
=f9d5a682cea22f94a10b3e1d302a2b98079a0c4e72049283b3e9a34e541b4a4aa79bc0a0970d&scene=21#wechat_redirect)

主从复制

  1. 指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis主从复制支持 主从同步 和 从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。

  2. 三个阶段:准备阶段-数据同步阶段-命令传播阶段。

哨兵

  1. 主服务器的选择
    • 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被 淘汰。
    • 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被 淘汰。
    • 在经历了以上两轮淘汰之后 剩下来的从服务器中, 我们选出 复制偏移量(replication offset
      )最大 的那个(越大说明数据越比较新) 从服务器 作为新的主服务器;如果复制偏移量不可用,或者从服务器的复制偏移量相同,那么 带有最小运行 ID 的那个从服务器成为新的主服务器。

Redis 集群

维护一个hash 环,服务器作为Hash 环上的节点,求 hash 时会落到节点附近,这个时候按顺时针找到第一个节点即为存放节点。按顺时针会有一个问题就是如果环上的点较为集中会使得hash
全部打在顺向的的同一个节点上,因此引入了虚拟节点的概念,让节点本身承担虚拟节点,这样就会得到一个均匀的环,当然虚拟的节点最终会打在实际的服务节点上。

  • Post title:(转) Redis 基础知识笔记记录
  • Post author:ReZero
  • Create time:2020-07-19 22:00:00
  • Post link:https://rezeros.github.io/2020/07/19/redis/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.
 Comments