Redis
1.Redis基础
1.1 Redis简介及应用场景
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
Redis作者Salvatore Sanfilippo,来自意大利的西西里岛,现在居住在卡塔尼亚。目前供职于Pivotal公司。
Redis做为内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件,在计时器,消息队列,排行榜,社交网络等也有广泛的应用场景。
1.2 Redis特性
-
简单稳定
源码少、单线程模型
-
速度快
单节点读 110000次/s,写81000次/s -
数据存放内存中
-
用 C 语言实现,离操作系统更近
-
单线程架构,6.0 开始支持多线程(CPU、IO 读写负荷)
-
持久化
数据的更新将异步地保存到硬盘(RDB 和 AOF)
RDB:Redis DataBase,把某一时刻的状态以文件的形式进行全量备份到磁盘,这个快照文件就称为RDB文件。
AOF:Append Only File(追加文件),保存Redis服务器所执行的写命令来记录数据库状态。
-
多种数据结构 - 不仅仅支持简单的 key-value 类型数据,还支持:字符串、hash、列表、集合、有序集合,
-
支持多种编程语言
-
功能丰富
- HyperLogLog、GEO、发布订阅、Lua脚本、事务、Pipeline、Bitmaps,key 过期 -
主从复制
-
Redis 支持数据的备份(master-slave)与集群(分片存储),以及拥有哨兵监控机制。
-
Redis 的所有操作都是原子性的,同时 Redis 还支持对几个操作合并后的原子性执行。
1.3 Redis高并发原理
- Redis 是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在 IO 上,所以读取速度快
- Redis 使用的是非阻塞 IO,IO 多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。
- Redis 采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。
- Redis 存储结构多样化,不同的数据结构对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
- Redis 采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大
2.Redis数据结构
Redis有五种数据结构,string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合);其中list、set、hash、zset这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
- create if not exists:容器不存在则创建
- drop if no elements:如果容器中没有元素,则立即删除容器,释放内存
2.1 string(字符串)
2.1.1 string内部结构
string(字符串)是Redis最简单也是使用最广泛的数据结构,它的内部是一个字符数组。Redis中string(字符串)是动态字符串,允许修改;它在结构上的实现类似于Java中的ArrayList(默认构造一个大小为10的初始数组),这是冗余分配内存的思想,也称为预分配;这种思想可以减少扩容带来的性能消耗。
2.1.2 String扩充
当string(字符串)的大小达到扩容阈值时,将会对string(字符串)进行扩容,string(字符串)的扩容主要有以下几个点:
- 长度小于1MB,扩容后为原先的两倍; length = length * 2
- 长度大于1MB,扩容后增加1MB;length = length + 1MB
- 字符串的长度最大值为 512MB
2.2 list(列表)
2.1.1 list内部结构
Redis的列表相当于Java语言中的LinkedList,它是一个双向链表数据结构,支持前后顺序遍历。链表结构插入和删除操作快,时间复杂度O(1),查询慢,时间复杂度O(n)。
Redis底层存储list(列表)不是一个简单的LinkedList,而是quicklist —“快速列表”。quicklist是多个ziplist(压缩列表)组成的双向列表;而这个ziplist(压缩列表)又是什么呢?ziplist指的是一块连续的内存存储空间,Redis底层对于list(列表)的存储,当元素个数少的时候,它会使用一块连续的内存空间来存储,这样可以减少每个元素增加prev和next指针带来的内存消耗,最重要的是可以减少内存碎片化问题。
2.1.1 list应用场景
根据Redis双向列表的特性,因此其也被用于异步队列的使用。实际开发中将需要延后处理的任务结构体序列化成字符串,放入Redis的队列中,另一个线程从这个列表中获取数据进行后续处理。
2.3 hash(字典)
2.3.1 hash(字典)的内部结构
Redis的hash(字典)相当于Java语言中的HashMap,它是根据散列值分布的无序字典,内部的元素是通过键值对的方式存储。hash(字典)的实现与Java中的HashMap(JDK1.7)的结构也是一致的,它的数据结构也是数组+链表组成的二维结构,节点元素散列在数组上,如果发生hash碰撞则使用链表串联在数组节点上。
2.3.2 hash(字典)扩容
Redis中的hash(字典)存储的value只能是字符串值,此外扩容与Java中的HashMap也不同。Java中的HashMap在扩容的时候是一次性完成的,而Redis考虑到其核心存取是单线程的性能问题,为了追求高性能,因而采取了渐进式rehash策略。
渐进式rehash指的是并非一次性完成,它是多次完成的,因此需要保理旧的hash结构,所以Redis中的hash(字典)会存在新旧两个hash结构,在rehash结束后也就是旧hash的值全部搬迁到新hash之后,新的hash在功能上才会完全替代以前的hash。
2.3.3 hash(字典)的相关使用场景
hash(字典)可以用来存储对象的相关信息,一个hash(字典)代表一个对象,hash的一个key代表对象的一个属性,key的值代表属性的值。hash(字典)结构相比字符串来说,它无需将整个对象进行序列化后进行存储。这样在获取的时候可以进行部分获取。所以相比之下hash(字典)具有如下的优缺点:1) 读取可以部分读取,2)节省网络流量存储消耗的高于单个字符串的存储;
2.4 set(集合)
2.4.1 set(集合)的内部结构
Redis的set(集合)相当于Java语言里的HashSet,它内部的键值对是无序的、唯一的。它的内部实现了一个所有value为null的特殊字典。集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。
2.4.2 set(集合)的使用场景
set(集合)由于其特殊去重复的功能,可在唯一ID场景使用或校验。
2.5 zset(有序集合)
2.5.1 zset(有序集合)的内部结构
zset(有序集合)是Redis中最常问的数据结构。它类似于Java语言中的SortedSet和HashMap的结合体,它一方面通过set来保证内部value值的唯一性,另一方面通过value的score(权重)来进行排序。这个排序的功能是通过Skip List(跳跃列表)来实现的。
zset(有序集合)的最后一个元素value被移除后,数据结构被自动删除,内存被回收。
2.5.2 zset(有序集合)的相关使用场景
利用zset的去重和有序的效果可以由很多使用场景,如用户ID的去重,用户属性的排名。
3.Redis进阶
3.1 持久化
Redis 支持 RDB 和 AOF 两种持久化机制,持久化功能有效地避免因进程 退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。
- RDB 是一次全量备份,AOF 日志是连续的增量备份, RDB 是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。
- AOF 以独立日志的方式记录每次写命令, 重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用 是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
3.2 主从同步
Redis 通过主从同步功能实现主节点的多个副本。从节点可灵活地通过 slaveof 命令建立或断开同步流程。同步复制分为:全量复制和部分增量复制主从节点之间维护心跳和偏移量检查机制,保证主从节点通信正常和数据一致。
Redis 为了保证高性能复制过程是异步的,写命令处理完后直接返回给客户端,不等待从节点复制完成。因此从节点数据集会有延迟情况。即当使用从节点用于读写分离时会存在数据延迟、过期数据、从节点可用性等问题,需要根据自身业务提前作出规避。
3.3 Redis Sentinel(哨兵)
Sentinel 负责持续监控主从节点的健康,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址, 然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节点切换。
3.4 消息丢失
Redis 主从采用异步复制,意味着当主节点挂掉时,从节点可能没有收到全部的同步消息,这部分未同步的消息就丢失了。如果主从延迟特别大,那么丢失的数据就可能会特别 多。Sentinel 无法保证消息完全不丢失,但是也尽可能保证消息少丢失。它有两个选项可以 限制主从延迟过大:
- min-slaves-to-write 1
- min-slaves-max-lag 10
第一个参数表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性。
何为正常复制,何为异常复制?这个就是由第二个参数控制的,它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈。
3.5 Redis 最终一致
Redis 的主从数据是异步同步的,所以分布式的 Redis 系统并不满足「一致性」要求。当客户端在 Redis 的主节点修改了数据后,立即返回,即使在主从网络断开的情况下,主节 点依旧可以正常对外提供修改服务,所以 Redis 满足「可用性」。
Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点 的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢 复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。
3.6 缓存
3.6.1 缓存更新策略
- LRU:Least Recently Used,最近最少使用。
- LFU:Least Frequently Used,最不经常使用。
- FIFO:First In First Out,先进先出。
使用场景:剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如 Redis 使用 maxmemory-policy 这个配置作为内存最大值后对于数据的剔除策略。
一致性:要清理哪些数据是由具体算法决定,开发人员只能决定使用哪种算法,所以数据的一致性是最差的。
维护成本:算法不需要开发人员自己来实现,通常只需要配置最大 maxmemory 和对应的策略即可。
缓存更新策略—超时剔除
使用场景:超时剔除通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如 Redis 提供的 expire 命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不一致,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。
一致性:一段时间窗口内(取决于过期时间长短)存在一致性问题,即缓存数据和真实数据源的数据不一致。
维护成本:维护成本不是很高,只需设置 expire 过期时间即可,当然前提是应用方允许这段时间可能发生的数据不一致。
缓存更新策略—主动更新
使用场景:应用方对于数据的一致性要求高,需要在真实数据更新后, 立即更新缓存数据。例如可以利用消息系统或者其他方式通知缓存更新。
一致性:一致性最高,但如果主动更新发生了问题,那么这条数据很可能很长时间不会更新,所以建议结合超时剔除一起使用效果会更好。
维护成本:维护成本会比较高,开发者需要自己来完成更新,并保证更新操作的正确性。
3.6.2 缓存击穿
缓存穿透:指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方法:
- 布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被 这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。
- 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
3.6.3 缓存雪崩
指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。解决方法:我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3.6.4 缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多 key。缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。解决方法:互斥锁、永远不过期设置、资源保护等等。
3.6.5 缓存无底洞
该问题是由 facebook 的工作人员提出的,facebook 在 2010 年左右,memcached 节点就已经达 3000 个,缓存数千 G 内容。他们发现了一个问题 - memcached 连接频率,效率下降了,于是加 memcached 节点,添加了后,发现因为连接频率导致的问题仍然存在,并没有好转,称之为 “无底洞现象”
目前主流的数据库、缓存、Nosql、搜索中间件等技术栈中,都支持 “分片” 技术,来满足 “高性能、高并发、高可用、高扩展” 等要求。有些是在 client 端通过 Hash 取模(或一致性 Hash)将值映射到不同的实例上,有些是在 client 端通过取值范围的方式映射的。当然,也有些是在服务器端进行的。但是,每一次操作都可能需要和不同节点进行网络通信来完成,实例节点越多,则开销会越大,对性能影响就越大。
主要可以从如下几个方面避免和优化:
1. 数据分布方式
有些业务数据可能适合 Hash 分布,而有些业务适合采用范围分布,这样能够从一定程度避免网络 IO 的开销。
2. IO 优化
可以充分利用连接池,NIO 等技术来尽可能降低连接开销,增强并发连接能力。
3. 数据访问方式
一次性获取大的数据集会比分多次去获取小数据集的网络 IO 开销更小。
4.Redis高级
4.1 缓存与数据库同步策略
如何保证缓存(Redis)与数据库的一致性:
对于热点数据(经常被查询,但不经常被修改的数据),我们一般会将其放入 Redis 缓存中,以增加查询效率,但需要保证从 Redis 中读取的数据与数据库中存储的数据最终是一致的。
4.2 集群
在大数据高并发场景下,单个 Redis 实例往往会显得捉襟见肘。首先体现在内存上,单个 Redis 的内存不宜过大,内存太大会导致 rdb 文件过大,进一步导致主从同步时全量同步时间过长,在实例重启恢复时也会消耗很长的数据加载时间,特别是在云环境下,单个实例内存往往都是受限的。其次体现在 CPU 的利用率上,单个 Redis 实例只能利用单个核心,这单个核心要完成海量数据的存取和管理工作压力会非常大。所以孕育而生了 Redis 集群,集群方案主要有以下几种:
- Sentinel:Sentinel(哨兵)模式,基于主从复制模式,只是引入了哨兵来监控与自动处理故障
- Codis:Codis 是 Redis 集群方案之一。