1. 为什么要用Redis缓存
随着互联网应用的高速发展,用户对服务器端的请求压力日益增大,尤其是存在大量的读请求,需要借用缓存来服务性能和并发处理。缓存分为本地缓存和分布式缓存。
- 本地缓存:在Java应用体系,本地缓存会随着JVM的销毁结束,并且多实例情况下,每个实例都需要维护一份缓存,一方面浪费内存资源,另一方面页存在缓存不一致情况。
- 分布式缓存:在多实例情况下,各实例公用一份缓存,具有缓存一致性。如Redis或memcache等。
2. 什么是Redis
Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的(BSD许可)高性能非关系型(NoSQL)的键值对数据库。Redis支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Introduction to Redis | Redishttps://redis.io/docs/about/
Redis的优缺点。
优点
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子性 – Redis所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
- 支持数据持久化,支持AOF和RDB两种持久化方式。
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
缺点
- 受物理内存限制,不能海量缓存数据;
- 不具备自动容错和恢复功能。主机从机的宕机都会导致部分读写请求失败,如果主机未将数据及时同步从机,切换从机后还会引入数据不一致的问题,降低了系统的可用性。
- 集群在线扩缩容难度大,在集群容量达到上限时在线扩容会变得很复杂;
3. Redis为什么会很快
- 纯内存操作,没有磁盘io
- 单线程处理请求,没有线程切换开销和竞争条件,也不存在加锁问题
- 多路复用模型epoll,非阻塞io(多路:多个网络连接;复用:复用同一个线程) 多路复用技术可以让单个线程高效的处理多个连接请求
- 数据结构简单,对数据操作也简单。还做了自己的数据结构优化
3.1 redis的单线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
- 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
4. redis 五种对对象类型和底层数据结构
类型常量 | 对象的名称 |
| 字符串对象 |
| 列表对象 |
| 哈希对象 |
| 集合对象 |
| 有序集合对象 |
对应的底层数据结构有8种:
编码常量 | 编码所对应的底层数据结构 |
|
|
|
|
| 简单动态字符串 |
| 字典 |
| 双端链表 |
| 压缩列表 |
| 整数集合 |
| 跳跃表和字典 |
I. 字符串对象可以编码成long, raw和embStr。如果字符串可以转换成long,就用long存储;如果字符串的长度小于39,就用embStr,其比raw的优势:1)embstr的创建只需分配一次内存,而raw为两次,同样释放内存也是如此;2)embstr的objet和sds放在一起,更好地利用缓存带来的优势。
II. list对象有ziplist和linkedlist,ziplist是内存中的一块连续区域,只需要分配一次,但为了保证内存的连续性和插入复杂度O(N),所以每次插入都会重分配一次内存。linkedlist是一种双向链表,节点中存放pre和next两个节点。
III. hash对象,底层是ziplist和hashtable.ziplist是key1,value1,key2,value2这样的顺序存放来存储的,当对象数目不太多的时候,这种存储结构效率比较高。dict是一个字典,其中有长度为2的数组,一个用来存放真正的数据,一个用来rehash时中转数据。
IV. set集合对象,集合对象的编码可以是intset或者hashtable,intset是一个有序的整数集合,查找效率是O(logN),但插入可能由O(logN)升级到O(N)。
V. zset有序集合,编码可能两种,一种是ziplist,另一种是skiplist与dict的结合。ziplist中存放的是score和members,跳跃表实现里有序集合的快速查找,大多数情况下与平衡术相当。每一列都代表一个节点,保存了member和score。
redis的rehash过程
对哈希表进行扩展或收缩,以使哈希表的负载因子维持在一个合理范围之内。rehash的步骤包括:为字典的ht[1]哈希表分配空间,大小取决于要执行的操作以及ht[0]当前包含的键值对数量(都是以2的幂次)。然后将保存在ht[0]的所有键值对rehash到ht[1]上面,最后释放ht[0],将ht[1]置为ht[0],并新建一个hash作为ht[1]。
渐进式hash:ht[0]数据重新索引到ht[1]不是一次性集中完成的,而是多次渐进式完成(避免hash表过大时导致性能问题)。
- 字典中rehashidx置为0,表示开始执行rehash(默认值为-1)
- rehash期间,每次对字典执行操作时,顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]
- 全部rehash完毕时,rehashidx设为-1
- 新增加的值一律放入ht[1],保证数据只会减少不会增加
5. Redis持久化机制
持久化就是把内存的数据写到磁盘中去,Redis提供RDB和AOF两种持久化机制,默认是RDB。
5.1 RDB持久化
RDB方式是把数据快照存放在磁盘上的二进制文件中,文件名为dump.rdb。主要步骤有:
- Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
- 父进程继续处理来自客户端的请求,子进程开始将内存中的数据写入硬盘中的临时文件;
- 当子进程写完所有的数据后,用该临时文件替换旧的RDB文件,至此,一次快照操作完成。
RDB的优点:
- RDB 是一个compact的文件,保存了 Redis 在某个时间点上的数据集。遇上问题的时候,可以随时将数据集还原到不同的版本。所以RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- RDB 非常适用于灾难恢复(disaster recovery)可以(在加密后)将它传送到别的数据中心。
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时只需要 fork 出一个子进程,这个子进程会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
RDB的缺点:
- RDB有时间间隔。 如果在RDB未生成的时候出现故障,可能导致这个阶段的数据丢失。
- 每次保存 RDB 的时候,Redis 都要 fork() 一个子进程,由子进程来进行实际的持久化工作。 在数据集比较大时, fork 可能会非常耗时,造成服务器在一定时间内停止处理客户端;
5.2 AOF持久化
AOF(APPEND ONLY MODE)是对数据的每一条修改命令追加到aof文件。
AOF的优点:
- 数据安全,可以设置不同的 fsync刷盘策略,比如无 fsync,每秒钟一次 fsync或者每次执行写入命令时 fsync 。
- 日志Append模式,AOF文件是一个只进行追加操作的日志文件,写入不需要进行 seek ,即便因为某些原因包含了未写入的完整命令,也能轻易修复这种问题。
- 数据量过大时,能自动重写AOF文件。
- AOF rewrite机制。AOF文件以Redis协议格式有序地保存Redis执行的所有写入操作,文件已于读懂,导出也非常简单: 如果不小心执行了 FLUSHALL 命令, 只要 AOF 文件未被重写, 停止服务器移除 AOF 文件末尾的 FLUSHALL 命令, 重启 Redis就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF的缺点:
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 使用 fsync 策略,AOF 的速度可能会慢于 RDB 。
5.3 如何选择持久化机制
根据对数据不同的需求可以选择不同的持久化模式,或者两种模式组合。对性能要求好,允许部分数据丢失,可以选择RDB模式;对数据要求高,则选择AOF模式;兼容性更好选择组合模式,在时间节点用RDB保存,时间节点之间用AOF,既能解决AOF文件过大,恢复导入慢,也能保证数据及时性。
6. redis数据的淘汰策略
redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略:( LRU 数据淘汰机制:在数据集中随机挑选几个键值对,取出其中 lru 最大的键值对淘汰)
全局的键空间选择性移除
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
7. redis的过期策略
常见的过期策略有三种:
1)定时删除,在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除
- 优点:保证内存被尽快释放
- 缺点:若过期key很多,删除这些key会占用很多的CPU时间;定时器的创建耗时,性能影响严重。
2)惰性删除,key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
- 优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的。
- 缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
3)定期删除,每隔一段时间执行一次删除过期key操作
- 优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用--处理"定时删除"的缺点,并且定期删除过期key--处理"惰性删除"的缺点
- 缺点:在内存友好方面,不如"定时删除",在CPU时间友好方面,不如"惰性删除"
redis采用的是惰性删除+定期删除策略。过期的key对RDB和AOF没有任何影响,在RDB和AOF之前,会对key先进行过期检查,恢复的时候,仍会对key进行过期检查,过期的key不会写入数据库。
8. Redis的事务机制
redis的事务是指:一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
Redis 事务的本质是通过MULTI、EXEC、WATCH等一组命令的集合。
- MULTI:用于标记事务的开始,其后执行的命令都将被存入命令队列,直到执行EXEC时,这些命令才会被原子执行。
- EXEC: 执行事务,内部包装执行了WATCH命令,只有当WATCH所监控的keys没有被修改的前提下,EXEC命令才能执行事务队列中的所有命令,否则EXEC将放弃当前事务中的所有命令。(有点相似于CAS)
- DISCARD:回滚事务队列中的所有命令,同时再将当前连接的状态恢复为正常状态,即非事务状态。如 果WATCH命令被使用,该命令将UNWATCH所有的keys。
- WATCH:在MULTI命令之前,可以指定监控的keys,提供监控(乐观锁)的功能,如果key的值发生了变化,就会放弃事务的执行。
- UNWATCH: 取消当前事务中指定监控的keys,如果执行了EXEC或DISCARD命令,事务中所有的keys都将自动取消WATCH。
8.1 Redis事务底层实现
每个客户端都有自己的事务状态,保存在客户端的mstate属性中。事务状态包含一个事务队列,结构有:
typedef struct redisClient {
//事务状态
multiState mstate;
} redisClient;
type struct multiState {
//事务队列,FIFO
multiCmd *commands;
//已入队命令计数器
int count;
} multiState;
事务队列是一个multiCmd类型的数组,数组中每个multiCmd结构都保存了一个已入队命令的相关信息,包括指向命令实现函数的指针、命令的参数,以及参数数量:
typedef struct multiCmd {
//参数
robj **argv;
//参数数量
int argc;
//命令指针
struct redisCommand *cmd;
} multiCmd;
执行事务:当客户端发送exec命令时候,服务器会遍历客户端的事务队列,执行队列中所有的命令,并把执行结果一次性全部返回给客户端。
放弃事务:执行discard命令后,会清空事务队列,并且客户端会从事务状态总退出。
WATCH的乐观锁实现:WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。被WATCH的键会被监控,如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了键的值, 那么当前客户端的事务就会失败。
8.2 redis的ACID
A:原子性。Redis的原子性是通过事务队列执行的,EXEC 命令负责触发并执行事务中的所有命令。如果开启了事务MULTI,但是未执行EXEC,事务队列中的命令不会被执行。
C:一致性。在Redis中,一致性不能很好的保证,不支持回滚。
I:隔离性。因为Redis单线程串行化命令的特点,可以保证事务的隔离性
D:持久性。Redis有RDB和AOF两种模式做持久化处理。
非常注意一点:在执行事务的过程中,如果命令出现了错误,事务中的其他命令将会继续执行,对于错误的命令,返回结果是nil。
Redis 在事务失败时不进行回滚,而是继续执行余下的命令。主要的原因是:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这属于编程错误,生产环境中不应该出现。
- 不支持回滚,可以保持Redis内部简单快捷
9. Redis的高级用法
Bitmap :位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter);
HyperLogLog: 供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV;
Geospatial:可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等。
pub/sub:功能是订阅发布功能,可以用作简单的消息队列。
Pipeline:可以批量执行一组指令,一次性返回全部结果,可以减少频繁的请求应答。
Lua:Redis 支持提交 Lua 脚本来执行一系列的功能。
参考文献:《进大厂系列》系列-Redis常见面试题(带答案) - 知乎