总起
在公司内,一开始就可以方便地使用中间件Tair,对 Redis 也无了解要求,但这方面的了解,应该是进一步拓展技术视野的要求,所以购买了书籍进行了了解,本文也是对应的简单摘抄记录,可作为了解的一个索引。
书本链接: http://product.dangdang.com/25859315.html
Redis 是 “Remote Dictionary Service”(远程字典服务)的首字母缩写。
基础篇:数据结构
Redis 有5种基础数据结构
- string 字符串
- list 列表
- hash 字典
- set 集合
- zset 有序集合
其中 list、hash、set、zset 共享2条通用原则:
- create if not exists : 如果不存在,那就创建一个,再进行操作。
- drop if no elements:如果容器里面的元素没有了,那么立即删除容器,释放内存。
Redis 的所有数据结构都可以设置过期时间。需要注意的是:
- 过期是以对象为单位,而不是其中的某个子key。
- 如果一个字符串设置了过期时间,修改它后,过期时间会消失。
string 字符串
string 字符串的特点
- 是动态字符串,是可以修改的,结构类似 java 的 ArrayList。
- 采用预分配冗余空间的方式减少内存的频繁分配。
list 列表
Redis 的列表相当于 Java 语言里面的 LinkedList。
- 它是链表而不是数组,插入、删除快,索引定位慢。
- 常用来做异步队列使用。
- 底层是“快速列表”(quicklist):将多个采用连续内存的压缩列表(ziplist),用双向指针串起来使用。
hash 字典
Redis 的字典相当于Java 语言里面的 HashMap。
- 是无序字典。
- 实现结构:“数组+链表”二维结构。
- Redis 为了性能,不阻塞服务,采用了渐进式 rehash 策略(后续的定时任务以及hash操作指令)。
set 集合
Redis 的集合相当于 Java 语言里面的 HashSet。
- 内部的键值对是无序的、唯一的。
- 内部实现相当于一个特殊的字典,字典的 value 都是一个值 NULL。
zset 有序列表
类似于Java 的 SortedSet 和 HashMap 的结合体。
- 是一个set,保证内部value的唯一性。
- 可以给每个 value 赋予一个 score, 代表这个 value 的排序权重。
- 内部使用 “跳跃列表” 的数据结构。
- 要支持随机的插入和删除:需要用链表,不宜用数组。
- 有元素要插入的时候,要定位到特定位置的插入,期望二分法查找,所以会有多层次索引。
应用篇
分布式锁
基础逻辑
占坑:一般使用 setnx (set if not exists)指令,只允许被一个客户端占坑。
释放坑:调用 del 指令。
超时:为了避免中间异常,del指令没有调用,会通过 exipire 指令设置超时时间。
原子指令:set 增加了扩展参数支持 setnx 和 expire 组合在一起的原子指令。
超时问题
Redis 的分布式锁不能解决超时问题(执行的时间超过锁超时时间)。
因此:建议不要用于长时任务,真的超市化后可能需要人工介入。
可重入性
可重入性是指一个锁支持同一个线程的多次加锁,那么锁就是可重入的。
Redis 支持锁的可重入,一般需要结合使用 Threadlocal 记录锁。
锁冲突处理
3种处理策略
- 直接抛出异常,让用户重试。
- sleep 一会,然后再重试。
- 将请求转移至延时队列,过一会再试。
延时队列
队列局限
Redis 的消息队列不是专业的消息队列,没有 ack 保证,对可靠性有极高要求的,不适合使用。
队列使用
- 结合使用 list 列表的 rpush、lpop(lpush、rpop)操作。
- 避免队列空造成频繁拉数据造成的 cpu 高。
- 线程睡一会。
- 采用阻塞读命令:blpop/ brpop。
- 空闲链接自动断开:需要捕获 blpop/ brpop 抛出的异常。
延时队列
延时队列可以通过 Redis 的 zset(有序列表)来实现。
- 字符串作为 zset 的 value。
- 到期处理时间作为 score。
- 用多个线程轮询 zset 获取到期的任务。
- zrem 命令是多线程争抢任务的关键,决定唯一的属主。
- 优化竞争:通过 lua scripting 将 zrangebyscore 和 zrem 一同挪到服务端进行原子化操作。
位图
使用场景
一些 bool 型数据的存取,比如用户一年的签到记录(签了1,没签是0,记录365天)。
基本操作
- 可以使用普通的 get/set 直接获取和设置整个位图的内容。
- 可以使用位图操作 getbit/setbit 将 byte 数组看成 “位数组” 来处理。
- 可以通过 bitcount 统计范围内的总数。
- 可以通过 bitpos 查找开始的位置。
- 可以通过 bitfield 一次进行多个位的操作。
HyperLogLog
使用场景
统计页面UV,用户量很大,需要进行去重,但是可以容忍一定概率误差。
基本概念
Redis 的 HyperLogLog 提供了不精确的去重计数方案,标准误差是 0.81%。
- pfadd 增加计数。
- pfcount 获取计数。
- pfmerge 将多个 pf 计数值累加在一起形成一个新的 pf 值。
实现原理
抽象问题:给定一系列的随机整数,记录下低位连续零位的最大长度 K,通过这个 K 值可以估算出随机数的数量N。
数据占用
Q:pf 的内存占用为什么是12KB?
A:Redis 使用了 16384 个分桶, 也就是 2^14,每个桶需的maxbits 需要6个 bit 来存储,算出来是 12 KB。
布隆过滤器
使用场景
- 推荐场景,判断是否推荐过,可以粗粒度处理,并且节约存储成本。
- URL爬虫,初步判断页面是否查询过。
- NOSQL 查询中,过滤掉大量不存在的 row 请求。
- 邮箱系统的垃圾邮件过滤。
功能特点
- 判断存在会有误判:当布隆过滤器说某个值存在时,这个值可能不存在;
- 判断不存在很准确:当它说某个值不存在时,那就肯定不存在。
基本用法
- bf.add 添加元素;
- bf.exists 查询元素是否存在。
限流
使用场景
控制流量、控制用户行为等场景。
解决方案
1)简单限流 ,使用 zset 模拟,时间戳作为 score(排序依据),不适合量大的限流,计数会占用较多存储。
2)漏斗限流,提供了 Redis-Cell 模块,使用了漏斗算法(考虑容量、流速、剩余空间、上一次操作时间)。
GeoHash
使用场景
寻找附近的车辆、附近的餐馆等。
算法原理
GeoHash 算法将二维的经纬度数据映射到一维的整数,这样所有的元素都将挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。
基本用法
- geoadd 增加位置
- geodist 计算两个元素之间的距离
- geopos 获取元素的经纬度坐标
- geohash 获取元素的经纬度编码字符串
寻找key
使用场景
需要从 Redis 实例的成千上万个 key 中找出特定前缀的 key 列表。
相关指令
- keys 用来列出所有满足特定正则字符串规则的 key,缺点:1)没有分页参数,2)遍历算法,复杂度是O(n),容易造成服务卡顿。
- scan 提供了三个参数:第一个参数 cursor 是指针,第二个 key 是正则模式,第三个是 limit hint。
原理篇
线程 IO 模型
Redis 是个单线程程序,关键特点有:
- 非阻塞 IO:读写 IO 不必阻塞,可以异步执行。
- 事件轮询(多路复用):非阻塞场景下,解决数据到来的通知问题。如:select、epoll、kqueue、macosx。
- 指令队列:客户端套接字关联到指令队列,先到先服务。
- 响应队列:将返回结果放回关联的响应队列中。
- 定时任务:定时任务存放在“最小堆”数据结构中,结合事件轮询的超时时间,确保能够及时执行。
通信协议
RESP 是 Redis 序列化协议(Redis Serialization Protocol)的简写。它是一种直观的文本协议,优势在于实现过程异常简单,解析性能极好。
Redis 协议将传输的数据结构数据分为 5 种最小单元类型,单元结束时统一加上回车换行符合 \r\n。
- 单行字符串以“+”符号开头,如“+hello world\r\n”。
- 多行字符串以“$”符合开头,后跟字符串长度,如“$11\r\nhello world\r\n”。
- 整数值以“:”符号开头,后面跟整数的字符串形式,如“:1024\r\n”。
- 错误消息以“-”符合开头,如“-WRONGTYPE Operation\r\n”。
- 数组以“*”符合开头,后跟数组的长度,如数组[1,2,3] “*3\r\n:1\r\n:2\r\n:3\r\n”。
持久化
技术背景
Redis 的数据全部存在内存里,如果突然宕机,数据就会全部丢失,因此必须有一种机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的持久化机制。有2种机制,一种是快照,一种是AOF日志。
快照
快照是一次全量备份,是内存的二进制序列化形式,在存储上非常紧凑。
- 使用操作系统的多进程 COW(Copy On Write)机制来实现快照持久化机制。
- Redis 在持久化时会 fork 产生一个子进程,快照持久化完全交给子进程来处理。
AOF日志
AOF日志是连续的增量备份,记录的是内存数据修改的指令记录文本。需要定期进行AOF重放,进行日志瘦身。
- 先执行指令,才将日志存盘。
- AOF 重写:开辟子进程,对内存进行遍历,转换为一些列操作指令,序列化到一个新的AOF日志文件中,然后将操作期间的增量 AOF 日志追加到这个新的文件中。
- 写数据时实际写到了内存缓存中,内核会异步将数据刷回到磁盘,在宕机时可能会丢。Redis 通常每隔一定时间执行一次 fsync 指令,强制刷到磁盘。
运维
文件操作是一个比较费时的事情,所以 Redis 的主节点不会进行持久化操作,持久化操作注意在从节点进行。从节点是备份节点,没有来自客户端的请求的压力,它的操作系统资源往往比较充沛。
混合持久化
问题:重启Redis时,很少使用快照恢复内存状态,因为会丢失大量数据;通常使用AOF日志重放,但是慢很多。
方案:Redis 4.0 带来了新的持久化特性:混合持久化。将快照的内容和增量的AOF日志存在一起,AOF不再是全量日志,是增量日志。在重启时,先加载快照,再重放增量AOF日志,效率大幅提升。
管道
Redis 管道(Pipeline)本身不是 Redis 服务器直接提供的技术,这个技术本质上是由客户端提供的。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省IO时间。
事务
基本用法
包含指令 multi、exec、discard。
- multi 指示事务的开始;
- exec 指示事务的执行;
- discard 指示事务的丢弃。
因为 Redis 的单线程特性,不用担心在执行队列的时候被其他指令打搅,可以保证“原子性”执行。
“原子性”
当指令执行失败之后,后面的指令还会继续执行,所以Redis的事务根本不具备“原子性”,而仅仅满足了实物的“隔离性”中的串行化——当前执行的事务有着不被其他事务打断的权利。
watch 机制
出现并发问题的时候,一种是采用分布式锁的悲观锁方案,Redis 提供了一种了 watch 的乐观锁机制。
watch 会在事务开始之前盯住变量,当事务执行时,也就是服务端收到了 exec 指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后是否被修改了。
Redis 禁止在 multi 和 exec 之间执行 watch 命令,必须在 multi 之前盯住关键变量。
PubSub
为了支持消息多播,Redis 单独提供了 PubSub 模块来支持消息多播,PubSub 也就是 PublisherSubscriber(发布者 / 订阅者模式)。
内存相关
Redis 是一个非常耗费内存的数据库,它的所有数据都放在内存里。为了优化数据结构的内存占用,增加了非常多的优化点。
- 32bit or 64bit : 如果使用32bit进行编译,数据结果所使用的指针空间占用会减少一半。
- 小对象压缩存储 ziplist : 如果 Redis 内部管理的集合数据很小,它会使用经凑存储形式压缩存储。
- 内存回收机制:Redis 并不是将空闲内存立即归还给操作系统,它会重新使用那些尚未回收的空闲内存,原因是操作系统以页为单位来回收内存的。
- 内存分配算法:Redis 将内存分配的细节丢给了第三方内存分配库去实现。可以使用 jemalloc(facebook)库,也可以使用 tcmalloc(google)库。
集群篇
主从同步
CAP 原理
- C:Consistent,一致性
- A:Availability,可用性
- P:Partition tolerance,分区容忍性
网络断开的场景叫做网络分区。当网络分区发生事,一致性和可用性两难全。
最终一致性
- Redis 的主从数据是异步同步的,所以并不满足一致性要求。
- 即使在主从网络断开的情况,主节点依旧可以对外提供服务,满足可用性。
- Redis 保证最终一致性,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态保持一致。
Redis 同步支持 主从同步与同同同步。
增量同步
主节点会将指令记录在本地内存的buffer中,然后异步将指令同步到从节点。当网络情况不好的时候,buffer可能被后续的指令覆盖而造成丢失。
快照同步
快照同步是一份耗费资源的操作:在主节点上将内存数据全部快照到磁盘中,然后再将快照文件全部传达到从节点。从节点接受完毕后,立即执行一次全量加载,加载完毕后通知主节点继续进行增量同步。
增加从节点
增加从节点时,需先进行一次快照同步,然后再继续进行增量同步。
无盘复制
指主服务器直接通过套接字将快照内容发送到从节点,不需要落盘之后再发送。从节点还是和之前一样。
wait指令
Redis 的复制是异步进行的,wait 指令可以让异步复制变身同步复制,确保系统的强一致性(不严格)。
Sentinel
背景
需要有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行主从切换,程序可以不用重启,Redis 官方提供了方案 Redis Sentinel(Sentinel 的含义是哨兵)。
工作原理
Sentinel 负责持续监控主从节点的健康,当住节点挂掉时,自动选择一个最优的从节点切换成为主节点。客户端来连接集群时,会首先连接 Sentinel,通过 Sentinel 来查询主节点地址。
消息丢失
Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分为同步的消息就会丢失了。
Sentinel 无法保证消息完全不丢失,但是尽量保证消息少丢失,如:可以定义一定场景下,停止对外写服务,丧失可用性。
Codis
产生背景
在大数据并发场景下,单个 Redis 实例往往会捉襟见肘:
- 内存不宜过大,过大会导致快照文件过大,主从同步时时间过长。
- 云环境下单个实例内存大小往往都是受限的。
- CPU 利用率上,单个 Redis 实例只能利用单个核心。
在这样的大数据高并发的需求下,Redis 集群方案应运而生。它可以将众多小内存的 Redis 实例整合起来,将分布在多台机器上的众多 CPU 核心的计算能力聚集到一起,完成海量数据存储和高并发读写操作。
基本介绍
Codis 是 Redis 集群方案之一,来自前豌豆荚中间件团队。使用 Go 语言开发,它是一个代理中间件,和 Redis 一样使用 Redis 协议对外提供服务。当客户端向 Codis 发送指令时,Codis 负责将指令转发到后面的 Redis 实例来执行。
- 分片原理:默认将 key 划分为 1024 个槽位(slot)。
- 槽位同步:需要一个分布式配置存储数据库专门用来持久化槽位关系,使用 zookeeper、etcd。
- 当扩容时,Codis 可使用 SLOTSSCAN 指令扫描出待迁移槽位的所有 key。
- 代价:key 分散在不同的 Redis 实例中,不能再支持事务了,事务只能在单个 Redis 实例中完成。
- 优点:将分布式问题交给三方(zookeeper等)负责,相比 Redis Cluster 官方集群方案要简单很多。
Cluster
Redis Cluster 是 Redis 的“亲儿子”,是 Redis 作者提供的 Redis 集群化方案。
- 是去中心化的,节点间通过特殊的二进制协议交互集群信息;
- 将所有数据划分为 16384 个槽位;
- 客户端需要缓存槽位关系,并有纠正机制,可直接定位到目标节点。
拓展篇、源码篇
略
总结
Redis 单线程的设计、特殊的数据结构令人映象深刻,在内存管理、网络处理、分布式服务等方案设计上,又体现出对通用理论的深刻理解,让人再次产生共鸣,体会到计算机体系结构和基础知识的力量。