1. 简介
- 本文是自己学习中收集的一些资料,有摘抄有原创,目的是希望对相关知识有一个系统并全面的了解,不足之处还望指正。写于2019年8月。
- Redis(REmote DIctionary Server)是一个由Salvatore Sanfilippo(大神介绍)写的一个开源的使用key-value形式存储在内存中存储系统。它可以用作:数据库、缓存和消息中间件。
- Redis是一个开源的使用ANSI C语言编写、遵守BSD协议(BSD,Apache,GPL,LGPL,MIT五大开源协议)、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
- Redis使用了单线程的架构,利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销也预防了多线程可能产生的竞争问题。
- Redis 内置了复制(Replication),LUA脚本(Lua scripting),LRU驱动事件(LRU eviction),事务(Transactions)和不同级别的磁盘持久化(Persistence)。
2. 支持数据类型
基本数据类型
- String: 字符串
- Hash: 散列
- List: 列表
- Set: 集合
- Sorted Set: 有序集合
此处不对命令做介绍了,资料很多自行查阅(Redis 命令)
特殊类型
- Bitmap:位图 2.2.0版新增,不要误会不是说可以存图片。可以把它想象成一个最大2^32(512M)的数组,数组的每一个元素(bit)只能存放二进制数据(0或1)。主要用来做一些日活、签到等统计业务。详见参考文章。
- HyperLogLog:2.8.9版新增,这个类型的作用是计算一个集合中基数(值不相同)的数量。但是实际上这个计算虽然压缩了空间复杂度但结果并不一定准确。具体原理请见参考文章。
- GEO:GENE EXPRESSION OMNIBUS(基因表达数据库) 3.2版新增,这个功能可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作。GEO 的数据结构总共有六个命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash。
3. 持久化
- RDB:快照(snapshots)
缺省情况情况下,Redis把数据快照存放在磁盘上的二进制文件中,文件名为dump.rdb。你可以配置Redis的持久化策略,例如数据集中每N秒钟有超过M次更新,就将数据写入磁盘;或者你可以手工调用命令SAVE或BGSAVE。
工作原理
. Redis forks.
. 子进程开始将数据写到临时RDB文件中。
. 当子进程完成写RDB文件,用新文件替换老文件。
. 这种方式可以使Redis使用copy-on-write技术。 - AOF:Append Only File 文件日志
快照模式并不十分健壮,当系统停止,或者无意中Redis被kill掉,最后写入Redis的数据就会丢失。这对某些应用也许不是大问题,但对于要求高可靠性的应用来说,Redis就不是一个合适的选择。此时Append-only文件模式是另一种选择。你可以在配置文件中打开AOF模式。
AOF持久化策略(默认每秒):
appendfsync always (同步持久化,每次发生数据变更会被立即记录到磁盘,性能差但数据完整性比较好)
appendfsync everysec (异步操作,每秒记录,如果一秒钟内宕机,有数据丢失)
appendfsync no (将缓存回写的策略交给系统,linux 默认是30秒将缓冲区的数据回写硬盘的) - 4.0版本开始支持RDB和AOF混用的方式进行持久化(默认关闭)。如果把混合持久化打开,aof rewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。这样做的好处是可以结合 rdb 和 aof 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, aof 里面的 rdb 部分就是压缩格式不再是 aof 格式,可读性差。
- 虚拟内存方式
指定Redis最大内存限制,Redis在启动时会把数据加载到内存中,达到最大内存后,Redis会先尝试清除已到期或即将到期的Key,当此方法处理后,仍然到达最大内存设置,将无法再进行写入操作,但仍然可以进行读取操作。Redis新的vm机制,会把Key存放内存,Value会存放在swap区。注意这个swap区是Redis自己实现的不是OS(系统)的。
- OS是基于page(4K)来做的,它的粒度对于Redis来说太大。而redis的大多数对象都远小于4k,所以一个OS页面上可能有多个redis对象。另外redis的集合对象类型如list,set可能存在与多个OS页面上。最终可能造成只有10%的key被经常访问,但是所有OS页面都会被OS认为是活跃的,这样只有内存真正耗尽时OS才会交换页面。
- 相比于OS的交换方式。redis可以将被交换到磁盘的对象进行压缩,保存到磁盘的对象可以去除指针和对象元数据信息。一般压缩后对象会比内存中对象小10倍。这样redis的vm会比OS vm能少做很多io操作。
- OS交换的时候,是会阻塞线程的,而Redis可以设置让工作线程来完成,主线程仍可以继续接收client的请求。
4. 部署方式
- 单机运行
这个就不用介绍了。 - 读写分离:主从复制
这个也很好理解,为了缓解服务读的压力将读和写的操作分开。 - 故障转移:Redis Sentinel(哨兵模式)V2.8+
哨兵是Redis为了解决主从模式下master宕机后主从服务切换问题的一个功能,它是一个独立的进程。 redis主从复制和哨兵 - 分布式:Redis cluster(集群模式)V3.0+
要理解这个模式首先要知道一个概念就是(一致性Hash)。笔者对它的理解是利用hash产生对数据分配的规则,但不知道这个一致性是怎么体现的,因为节点上的数据显然是不一致的。废话不多说,再来看看Redis中的体现。首先Redis的"圆"没有这么大,它只有16384个槽(slot),而且是均匀分布的,规则是16384/n。比如有3个节点,节点1就是05460,节点2是564110922,节点3就是10923~16383。当有一个节点挂掉后如果没有备用切换,这部分数据就丢失了。而且半数以上的节点挂掉后集群也会停止服务。记一次 Redis Cluster 宕机引发的事故,这个文章中有验证。
至于为什么是16384(2^14)这个数字作者给出了解释:
The reason is:
Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
有点渣的翻译:
- 正常的心跳包携带节点的完整配置,可以用幂等方式替换旧节点以更新旧配置。 这意味着它们包含原始形式的节点的插槽配置,它使用带有16k Slots的2k空间,但使用65k Slots时将使用高达8k的空间。说人话就是在发送心跳包的时候可以发送更少的数据。
- 同时,由于其他设计权衡,Redis Cluster不太可能扩展到超过1000个主节点。
因此,16k处于正确的范围内,以确保每个主站有足够的插槽,最多1000个主站,但足够小的数字可以轻松地将插槽配置传播为原始位图。 请注意,在小型集群中,位图难以压缩,因为当N很小时,位图将设置 Slots/ N位,这是设置的大部分位。
5. Redis常见性能问题和解决方案
- Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。
- Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
6. 淘汰策略
MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
相关知识:redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略(回收策略)
Redis 提供 6种数据淘汰策略:
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据
在 redis 中,允许用户设置最大使用内存大小(server.maxmemory)默认为0,没有指定最大内存,如果有新的数据添加,超过最大内存,则会使redis崩溃,所以一定要设置。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
7.事务
和众多其它数据库一样,Redis作为NoSQL数据库也同样提供了事务机制。在Redis中,MULTI/EXEC/DISCARD/WATCH这四个命令是我们实现事务的基石。相信对有关系型数据库开发经验的开发者而言这一概念并不陌生,即便如此,我们还是会简要的列出Redis中事务的实现特征:
- 在事务中的所有命令都将会被串行化的顺序执行,事务执行期间,Redis不会再为其它客户端的请求提供任何服务,从而保证了事物中的所有命令被原子的执行。
- 和关系型数据库中的事务相比,在Redis事务中如果有某一条命令执行失败,其后的命令仍然会被继续执行。
- 我们可以通过MULTI命令开启一个事务,有关系型数据库开发经验的人可以将其理解为"BEGIN TRANSACTION"语句。在该语句之后执行的命令都将被视为事务之内的操作,最后我们可以通过执行EXEC/DISCARD命令来提交/回滚该事务内的所有操作。这两个Redis命令可被视为等同于关系型数据库中的COMMIT/ROLLBACK语句。
- 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
- 当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。
Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。
WATCH命令和基于CAS的乐观锁:
在Redis的事务中,WATCH命令可用于提供CAS(check-and-set)功能。假设我们通过WATCH命令在事务执行之前监控了多个Keys,倘若在WATCH之后有任何Key的值发生了变化,EXEC命令执行的事务都将被放弃,同时返回Null multi-bulk应答以通知调用者事务执行失败。例如,我们再次假设Redis中并未提供incr命令来完成键值的原子性递增,如果要实现该功能,我们只能自行编写相应的代码。其伪码如下:
val = GET mykey
val = val + 1
SET mykey $val
以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景--竞态争用(race condition)。比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是我们认为的12。为了解决类似的问题,我们需要借助WATCH命令的帮助,见如下代码:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
和此前代码不同的是,新代码在获取mykey的值之前先通过WATCH命令监控了该键,此后又将set命令包围在事务中,这样就可以有效的保证每个连接在执行EXEC之前,如果当前连接获取的mykey的值被其它连接的客户端修改,那么当前连接的EXEC命令将执行失败。这样调用者在判断返回值后就可以获悉val是否被重新设置成功。
8. PipeLine
Redis在2.6中增加了PipeLine(管道)。
简述一下就是,Redis如何从客户端一次发送多个命令,服务端到客户端如何一次性响应多个命令。就是说pipeline模式首先是一种客户端行为,它将多条指令合并到一次请求中执行,减少了通信次数。
一次 pipeline
- 客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲send buffer。
- 客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到服务器的网卡。
- 服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
- 服务器进程调用read从接收缓冲中取出消息进行处理。
- 服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer。
- 服务器操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到客户端的网卡。
- 客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
- 客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理。
结束。
其中步骤 5~8 和 1~4 是一样的,只不过方向是反过来的,一个是请求,一个是响应。
注意:
pipeline不代表原子操作。
cluster模式下使用pipeline要注意多个key是否能落在同一节点。具体原因见上面的cluster原理。
适用场景
- 不合适一次操作强依赖结果的值
- 支持批量操作,有一两个失败也不影响那种
9. 过期键的删除策略
如果一个键是过期的, 那它什么时候会被删除?
这里有三种不同的删除策略:
- 定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。
- 惰性删除:放任键过期不管,但是在每次从 dict 字典中取出键值时,要检查键是否过期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。
- 定期删除:每隔一段时间,对 expires 字典进行检查,删除里面的过期键。
第一种和第三种为主动删除策略,第二种为被动删除策略
定时删除
定时删除策略对内存是最友好的: 因为它保证过期键会在第一时间被删除, 过期键所消耗的内存会立即被释放。
这种策略的缺点是, 它对 CPU 时间是最不友好的: 因为删除操作可能会占用大量的 CPU 时间 —— 在内存不紧张、但是 CPU 时间非常紧张的时候 (比如说,进行交集计算或排序的时候), 将 CPU 时间花在删除那些和当前任务无关的过期键上, 这种做法毫无疑问会是低效的。
除此之外, 目前 Redis 事件处理器对时间事件的实现方式 —— 无序链表, 查找一个时间复杂度为 O(N)O(N) —— 并不适合用来处理大量时间事件。
惰性删除
惰性删除对 CPU 时间来说是最友好的: 它只会在取出键时进行检查, 这可以保证删除操作只会在非做不可的情况下进行 —— 并且删除的目标仅限于当前处理的键, 这个策略不会在删除其他无关的过期键上花费任何 CPU 时间。
惰性删除的缺点是, 它对内存是最不友好的: 如果一个键已经过期, 而这个键又仍然保留在数据库中, 那么 dict 字典和 expires 字典都需要继续保存这个键的信息, 只要这个过期键不被删除, 它占用的内存就不会被释放。
在使用惰性删除策略时, 如果数据库中有非常多的过期键, 但这些过期键又正好没有被访问的话, 那么它们就永远也不会被删除(除非用户手动执行), 这对于性能非常依赖于内存大小的 Redis 来说, 肯定不是一个好消息。
举个例子, 对于一些按时间点来更新的数据, 比如日志(log), 在某个时间点之后, 对它们的访问就会大大减少, 如果大量的这些过期数据积压在数据库里面, 用户以为它们已经过期了(已经被删除了), 但实际上这些键却没有真正的被删除(内存也没有被释放), 那结果肯定是非常糟糕。
定期删除
从上面对定时删除和惰性删除的讨论来看, 这两种删除方式在单一使用时都有明显的缺陷: 定时删除占用太多 CPU 时间, 惰性删除浪费太多内存。
定期删除是这两种策略的一种折中:
它每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,籍此来减少删除操作对 CPU 时间的影响。
另一方面,通过定期删除过期键,它有效地减少了因惰性删除而带来的内存浪费。
10. 客户端
因为本人是做Java开发的,所以这里只列举一下java版本的客户端。
- Jedis:老牌的Redis的Java实现客户端,使用最广泛的一种,提供了比较全面的Redis命令的支持,Springboot1.x默认使用。
- Redisson:实现了分布式和可扩展的Java数据结构。
- Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。Springboot2.x默认使用。
优点: - Jedis:比较全面的提供了Redis的操作特性
- Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。最重要的作用是实现了分布式锁(redlock)。
- Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。
可伸缩: - Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
- Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作
- Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作。lettuce能够支持redis4,需要java8及以上。
lettuce和jedis比较:
jedis使直接连接redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个jedis实例增加物理连接 ;
lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,StatefulRedisConnection是线程安全的,所以一个连接实例可以满足多线程环境下的并发访问,当然这也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
总结:
优先使用Lettuce,如果需要分布式锁,分布式集合等分布式的高级特性,添加Redisson结合使用,因为Redisson本身对字符串的操作支持很差。
11.数据同步
主从复制时分全量同步和增量同步方式。有兴趣的看一下参考文章就可以了,这里不再重复。
Redis 的主从同步,及两种高可用方式
Redis在2.8以后对复制方式又优化了一下。Redis复制
11. 布隆过滤器
这是Redis到了4.0提供了插件功能之后出现的一个插件。它是作用是起到一定的判断是否存在功能,但是和HyperLogLog一样也是牺牲了准确性换取效率的算法。它的宗旨是过滤到的不一定存在,但没过滤到的一定不存在。详解布隆过滤器的原理、使用场景和注意事项
参考文章
为什么说Redis是单线程的以及Redis为什么这么快!
Redis常见面试题
redis-pipeline
Redis BitMap 总结
探索HyperLogLog算法(含Java实现)
Redis入门详解
Redis VM使用
redis三个连接客户端框架的选择:Jedis,Redisson,Lettuce