Redis 的数据类型(数据结构)
- string (二进制安全,可以存储任意类型的数据)
- list(链表)
- 字典(就是hashmap)
- set(不重复无序的hashmap)
- zset(按照给定的 score 排序的 set)
- HyperLogLog(来做基数统计的算法,简介)
- Geo(支持地理位置的操作,使用简介)
- Pub/Sub
- BloomFilter
- RedisSearch
- Redis-ML
缓存雪崩(缓存击穿)
他们出现的原理都是访问缓存的时候,key 刚好失效,导致直接访问 DB,压垮后台。
解决办法就是让 key 的过期时间分散开,不要集中失效
分布式锁
使用 setnx 命令后为了防止死锁,需要对 key 施加 expire 命令,防止死锁,但是存在执行 expire 命令前宕机,造成死锁的发生。
解决办法就是使用复杂的 setnx 命令,他可以把 setnx 和 expire 一起原子执行
如何寻找有固定前缀的 key
使用 KEYS pattern 命令,如:KEYS alib*
但是因为 Redis 是单线程的,执行该命令后会导致 Redis 阻塞住。
解决办法就是使用 scan 命令,scan 命令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys指令长
scan 命令的特点:
Redis中的Scan命令的使用 - MSSQL123 - 博客园www.cnblogs.com如何用 Redis 做异步队列
使用 list 数据结构,在一遍加入,另一边取出,若取出来的是 null,则消费线程应该 sleep,或者消费线程不使用 lpop 或 rpop 命令,改为 blpop 或者 brpop 命令,若没有元素可取,它会阻塞列表直到等待超时或发现可弹出元素为止。
如何生产一次,消费多次
使用发布订阅模式
但是在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 rabbitmq
Redis 如何实现延时队列
使用 zset ,用时间戳作为 score,消息会按照时间顺序排序
然后使用 zrangebyscore key min max [WITHSCORES] [LIMIT offset count] 来取出比当前时间小的 key 的 value
持久化
Redis 4.0 时代以 RDB 为主,AOF 只记录上一次 RDB 到现在的更改记录
开启混合持久化:aof-use-rdb-preamble yes
工作原理:其实还是一种 AOF 机制,但是新增了 RDB 的特性,先看此模式下的 AOF 的数据结构图
- 看图就知道混合模式指的就是重写 AOF 的时候,将此刻内存里面的数据做成 RDB,在此过程中增量的数据写入到缓冲区,最终形成新的 aof 文件。接着删除旧的 AOF。
- 重启恢复时先恢复 RDB,再重放新增的 AOF 指令
持久化的意义在于故障恢复
- AOF:记录每一次的写操作到日志上,重启时重放日志以重建数据
- 每隔一段时间调用系统的 fsync 函数强制将 os cache 里面的数据刷新到磁盘上
- RDB:每隔一段时间保存一次当前时间点上的数据快照
- 快照就是一次又一次地从头开始创造一切,全量的
持久化如何工作的
关键词:写时复制和 fork 子进程
- 每当 Redis 需要转储数据集到磁盘时,会发生:
- Redis 调用 fork()。于是我们有了父子两个进程。
- 子进程开始将数据集写入一个临时 RDB / AOF 文件。
- 当子进程完成了新 RDB 文件,替换掉旧文件。
- AOF 的 fork(),与 RDB 不同的是父进程会在一个内存缓冲区中积累新的变更,同时将新的变更写入新的 AOF 文件,所以即使重写失败我们也安全。当子进程完成重写文件,父进程收到一个信号,追加内存缓冲区到子进程创建的文件末尾,接着自动重命名文件为新的,然后开始追加新数据到新文件
- 这个方法可以让 Redis 获益于写时复制(copy-on-write)机制。
AOF 为什么要重写
AOF 记录的是 Redis 的每一次变更,这个变更包含了大量的冗余操作,导致 AOF 体积变大,恢复缓慢。
通过重写这个体积大的 AOF 文件,可以实现新的 AOF 文件不会包含任何浪费空间的冗余命令,通常体积会较旧 AOF 文件小很多。
Pipeline
将多个指令一起发送,减少 IO,提高吞吐量
Redis 的同步机制
Redis 可以使用主从同步,从从同步。
第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 RDB 文件全量同步到复制节点,复制节点接受完成后将 RDB 镜像加载到内存。
加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过 AOF 日志同步即可,有点类似数据库的 binlog
Redis 集群
Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master,继续提供服务。
Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。
Redis 的通讯协议是什么
答案是文本协议
虽然文本协议耗费流量,但是解析性能很好
Redis 的事务
首先 Redis 支持事务,但是它的事务与MySQL这类传统的数据库的事务不同,不同点为:
- 通过 MULTI 开启事务(类似于MySQL的 STARTtransaction; 命令)
- 通过 EXEC 命令触发事务(类似于MySQL的 COMMIT; 命令)
- 执行事务的时候放入事务队列里面的命令都会被执行,不管是否有命令执行时出错
- Redis 的事务可以理解为打包的批量执行脚本,所以不支持原子性,失败了可以继续执行完,也不会回滚
- 但是单个的Redis命令是原子的
具备隔离性:Redis 因为是单线程操作,所以在隔离性上有天生的隔离机制,当 Redis 执行事务时,Redis 的服务端保证在执行事务期间不会对事务进行中断,所以,Redis 事务总是以串行的方式运行,事务也具备隔离性。
不具备一致性:虽然开启持久化之后可以在数据出现问题是恢复到之前的状态,但是因为Redis的事务不是原子性的,不会回滚数据,Redis设计时也没有考虑ACID特性,所以认为Redis不具备一致性
持久性:开启持久化就支持,不开启就不支持
Redis 的乐观锁 Watch 是怎么实现的
Watch 会在事务开始之前盯住 1 个或多个关键变量,如下图:
当事务执行时,也就是服务器收到了 exec 指令要顺序执行缓存的事务队列时, Redis 会检查关键变量自 Watch 之后,是否被修改了。
上图显示,watch abc 之后执行事务之前,执行了一次 incr 操作,所以在 exec 的时候失败,watch 的实现原理不是 CAS 中的 Cmpxchg 指令,而是借助 Redis 的单线程执行机制,采用了 watched_keys 的数据结构和串行流程实现了乐观锁,具体解释就是:
每一个被 watch 的 key 都会被构造成一个 watched_keys 数据类型,多个被 watch 的 key 构造成链表存储着假设客户端 A 和 B 都 watch abc
但是并发时 Redis Server 中只会有一个线程在执行,
当 A 修改了 watch 命令监视的 key 后,会改变 abc 的 watched_keys 的状态为 dirty,
客户端 B 会检查这个被 watch 的 abc,发现他的状态是 dirty 的时候就会终止事务
Redis 如何节省内存
关键词:ziplist、quicklist、对象共享
Ziplist 是一个紧凑的数据结构,每一个元素之间都是连续的内存,如果在 Redis 中,Redis 启用的数据结构数据量很小时,Redis 就会切换到使用紧凑存储的形式来进行压缩存储。
Quicklist 是 ziplist 的双向链表版本,可以在两端执行 push 和 pop 操作
对象共享:指的就是多个key的value是一样的话,就把多个key指向同一个value即可,如下图:
A和B都指向值100,则A、B 共用同一个100对象
Redis 的过期策略
- 定时删除:创建一个定时器,让定时器在键过期时来执行删除
- 对CPU不友好
- 影响性能
- 定期删除:每隔一段时间,程序都要对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,由算法而定。
- 要么对 CPU 不友好
- 要么对内存不友好
- 惰性删除:get Key 的时候才检查是否过期,过期了就删除返回 null
- 对内存不友好
- 可能导致内存溢出
Redis 同步策略
最简单的架构模式就是:一台 master 和多个 slave
仅 master 开启持久化策略,负责写入操作,slave 只负责读取操作
同步的目的就是为了【读写分离】和【容灾备份】
同步的过程:
- slave 发送 SYNC 给 master
- master 接收到命令后一边缓存继续写入的命令,一边 fork 子进程生成 RDB 文件
- 子进程写完 RDB 之后,父进程把 RDB 发送给 slave,slave 接收 RDB 并重现数据
- 父进程增量地将缓存的写命令发送给 slave
Redis 为什么是单进程单线程的
注意:这里的单线程指的是处理 I/O 事件是单线程的,并发的请求进入 Redis 后会排队,只有上一个处理完了,才会继续处理下一个。
注意:Redis 可不完全是单进程的,开启持久化的时候,会 fork 子进程完成 RDB/AOF 的创建
我们所谓的多线程操作是为了加快计算,所以开启多个线程同步操作,而这会耗费大量的 CPU 资源。
Redis 设计之初就是纯内存运行,计算速度够快了,若再使用多线程操作的话会因为线程管理问题以及上下文切换耗时,反而会降低性能。Redis 官方测试中一台普通的笔记本电脑没鸟的QPS可达十几万,所以目前来看没有必要使用多线程。
客户端获取 value 阻塞时,却不会影响后面的命令的执行
Redis 为什么这么快
- 纯内存运行
- 单线程(这个单线程指的是处理命令的时候只有一个线程执行,其他的命令加入队列阻塞)
- 数据结构简单,使用专门的数据结构存储数据
- 例如:ZSet 使用跳表存储
- 字符串使用 SDS(Simple Dynamic String)的结构体保存(该结构体可以存储字符串的长度,还能防止字符串溢出,实现二进制存储安全等特性)
- 哈希表用的是字典,且通过两个 ht 实现渐进式 rehash(有利于加快Redis响应效率)
- 还有通过 ZipList、QuickList 来压缩内存
- 非阻塞的 I/O 多路复用模型:用一个线程管理多个网络连接
- epoll
- React 线程模型
谈一下 Redis 的哈希槽
Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现分区(因为一致性哈希开销很大)
数据分片就是用到了16384个槽,槽里面可存多个key
集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽
也就是说整个集群里面不管有几台机器,只有 16384 个槽,集群把槽分配给全部的实例,每个实例管理一部分的槽,如图:
扩容、缩容都要涉及槽的迁移;扩容后要给新机器分配槽、缩容后要分配被释放的槽给其他节点。