Redis常见知识总结
文末有读书笔记脑图和书籍推荐顺序
数据结构与操作
-
1、Redis支持的数据类型有哪些
-
String 字符串
- 格式
-
set key-name value
* string类型是二进制安全的,因为底层使用的是字节数组来保存数据,这意味着string可以保存任何数据,包含jpg图片或者序列化的对象 * string是redis基本的数据类型,一个键最大能存储512MB * string的数据底层分为3种类型:int、embstr、raw(sds),这会根据value是否可以使用int直接表示,字符串是否大于32字节来决定
-
Hash(哈希)
- 格式
-
hset key-name key1 value1 key2 value2
* redis hash是一个键值对的集合。特别适用于存储对象 * 底层使用的是压缩列表或者字典,根据元素的个数和元素的大小,判断设置的阈值自动切换 * 可以通过使用压缩列表来进行内存的压缩 设置的参数分别为 * hash-max-ziplist-entries 512 哈希表中的键值对不超过512 * hash-max-ziplist-value 64 元素的大小不超过64字节
-
List(列表)
- 格式
-
lpush key-name value rpush key-name value2 lrem key-name value1 llen key-name
* Redis列表是简单的字符串列表,按照插入顺序进行排序。可以通过在列表左右插入元素 * 底层使用的是压缩列表或者linkedlist(双向无环链表),根据元素个数和元素大小,自动切换 * 可以通过使用压缩列表来进行内存的压缩,设置参数为 * list-max-ziplist-entries 512 列表中的键值对不超过512 * list-max-ziplist-value 64 元素的大小不超过64字节
-
Set(集合)
- 格式
-
sadd key-name value sismember key-name value2 smembers key-name srem key-name
* Redis集合是string类型的无序集合,数据的插入并不能保证顺序 * 底层使用的是intset或者哈希表,如果元素都可以用数字表示并且元素个数少于设置值那么使用整数集合,否则使用hashtable,自动切换 * 其中数字表示,指的是64位可以表示的整数,如果大于64位,那么也会使用hashtable * 可以通过使用压缩列表来进行内存的压缩,设置参数为 * set-max-intset-entries 512 集合中的元素个数不能超过512
-
zset(有序集合)
- 格式
-
zadd key-name score member1 zrange key-name start end zrank key-name member1 zcard key-name
* Redis 有序集合是键为string类型,和集合不同的是,每个成员会关联一个double类型的分数,有序集合是通过分数来进行排序,默认是从小到大排序 * 如果元素分数一样,那么按照成员进行排序,使用的是string排序 * 底层使用的是压缩列表或跳跃表,如果成员个数以及成员大小都低于给定值那么使用压缩列表,否则使用跳跃表(跳跃表对比链表,查询的时间复杂度为O(logN)),自动切换 * 可以通过使用压缩列表来进行内存的压缩,设置参数为 * zset-max-ziplist-entries 128 元素个数不超过128 * zset-max-ziplist-value 64 元素大小不大于64字节
数据持久化
1、什么是redis的持久化?有哪几种方式?优缺点是什么?
持久化就是将数据存储到系统媒介(例如磁盘中),目的是为了防止服务器宕机或者重启内存数据丢失
Redis提供了两种持久化的方式:RDB(默认)和AOF
- RDB Redis Database
- 功能的核心是生成rdb文件,并将rdb文件发送给从服务器,从服务器通过加载rdb文件来进行数据的备份恢复
- 在文件同步期间,主服务器会将期间获取到的key缓冲起来,等从服务器执行完毕后,将这些操作传播给从服务器
- 一般以创建子进程的方式处理备份文件,这样不会阻塞主进程处理相关请求
- AOF Append-only file
- 每当服务器执行定时任务或者函数(客户端请求),都会执行flushAppendOnlyFile函数,这个函数会做如下操作:
- Write:根据条件,将命令行写入aof文件
- Save:根据条件,调用fsync胡总和fdatasync函数,将AOF文件保存到磁盘中
- 几种处理策略
- always 每次执行有写入动作的命令都会写入aof文件并同步,cpu和磁盘占用率较高,也最安全,即使宕机也只是丢失宕机还未处理的一条请求
- everyseconds 每一秒钟执行一次,默认配置,机制宕机也只是丢失宕机时1s内的数据
- no 命令的写入由系统决定,最不安全,可能丢失上次写入后的所有数据
- 每当服务器执行定时任务或者函数(客户端请求),都会执行flushAppendOnlyFile函数,这个函数会做如下操作:
- 存储结构
- 内容时redis通通讯协议(RESP)格式的命令文本存储
- RESP 特点
- 简单的实现
- 快速的被计算机解析
- 简单的可以被人工解析
- 协议
For Simple Strings the first byte of the reply is "+"
简单字符串回复的第一个字节将是“+”For Errors the first byte of the reply is "-"
错误消息,回复的第一个字节将是“-”For Integers the first byte of the reply is ":"
整型数字,回复的第一个字节将是“:”For Bulk Strings the first byte of the reply is "$"
批量回复,回复的第一个字节将是“$”For Arrays the first byte of the reply is "*"
数组回复的第一个字节将是“*”
- 两种持久化方式的对比
- AOF文件比rdb更新频率高,优先使用aof还原数据
- aof比rdb更安全也更大,但是aof有重写的过程,也会进行一定的压缩,比如过次操作一个键可以转成一条指令
- rdb的性能比aof好
- 如果两个都配置了,优先加载aof文件
2、一般生产环境中,使用的持久化配置是什么?为什么要这么设置?
单机功能
1、Redis是采用的什么模型?
* redis是一种事件模型的服务器
* 采用的是Reactor多路复用io模型,自动选择底层的
2、为什么说Redis是单线程的以及为什么这么快?
* 完全基于内存,绝大部分请求是纯内存操作,非常快速
* 数据结构简单,对数据操作也简单。redis中的数据是专门设计的
* 采用单线程,避免了不必要的上下文切换和竞争条件。不存在多线程竞争而消耗cpu,不存在各种加锁问题
* 使用多路复用的io模型,非阻塞式io模型。Reactor模型下,自动选择底层系统的 select、epoll、poll等方法。
* 多路复用io模型
* 多路复用io模型是利用select、poll、epoll可以同时检查多个流的io事件能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有io事件时,就从阻塞态中唤醒,于是程序就会轮训一遍所有的流(epoll时只轮询哪些真正发出了事件的流),并且只是依次顺序处理就绪的流,这种做法就避免了大量的无用操作。
* 这里的多路指的是多个网络连接,多个socket(套接字,插座的概念),复用指的是复用同一个线程。采用多路复用io计数可以让单个线程高效的处理多个连接请求(尽量减少网络io的时间消耗),且redis在内存中操作数据的速度非常快,也就是说对内存的操作并不会成为redis的瓶颈。
3、使用过redis分布式锁么?他是怎么实现的?
- 有几种方式
- 自定义实现:一般是使用setnx设置一个唯一锁键,用于各个线程争抢,争抢到之后,使用expire给锁增加一个过期时间,用于锁的释放,避免客户端忘记解锁。
-
if (conn.setnx(lockKey, identifier) == 1) { //如果当前进程突然崩溃,那么则会造成无法解锁,后续的资源无法获取到当前锁 conn.expire(lockKey, lockExpire); return identifier; }
* 开源实现:可以使用Redisson实现,实现的原理是使用lua脚本,通过多次判断是否存在来进行加锁
4、分布式锁如何解决死锁或者宕机问题?解锁的过程是怎么样的?
-
如果使用setnx来实现,那么必然存在加锁和给锁添加超时的操作是非原子性的,所以当获取到锁资源之后,如果此时线程挂掉或者客户端宕机,那么还是会造成锁资源无法释放,造成死锁
-
解决方案:使用一条原子性的操作来获取并为锁增加超时时间
-
//使用一条原子性命令 conn.set(lockKey, identifier, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, lockExpire); ```
-
-
通常情况下,解锁的过程是这样的
-
if (identifier.equals(conn.get(lockKey))) { Transaction trans = conn.multi(); trans.del(lockKey);}
* 这种情况下会存在什么问题呢?如果获取到锁之后,在进行删除之前,此时锁超时过期,锁资源被其他线程争抢到,那么就会造成锁的误删。
-
-
改进方案1:
- 使用监视关键字 watch
-
while (true) { conn.watch(lockKey); if (identifier.equals(conn.get(lockKey))) { Transaction trans = conn.multi(); trans.del(lockKey); List<Object> results = trans.exec(); if (results == null) { continue; } return true; } conn.unwatch(); break; }
* 这种方案有什么问题呢?在高并发的情况下,锁资源争抢严重,使用watch会增加服务器对某个key的处理频率(当watch监视到某个key被改变时,会将这个key的dirty标识打开,此时客户端尝试修改这个key的操作便会失败),但是在这种情况下,会造成大量的资源争抢,效率较低
-
改进方案2:终极
- 使用lua脚本
-
String lockKey = "lock:" + lockName; String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifier));
* 在redis中,任何一个lua脚本都是原子性并且高效的。
5、使用过Redis做异步队列么?你是怎么用的?与什么缺点?
- 可以使用redis的列表作为队列,
rpush
向队列的右侧推入一个元素,lpop
从队列的左侧弹出一个元素,当推入速度大于弹出速度时,则会造成大量的数据堆积,当弹出速度大于推入速度时,则会造成大量的无用轮询。还是建议使用专业的消息队列 - 还有
blpop
和brpop
,阻塞式的从队列中弹出一个元素,可以设置过期时间
6、能不能生产一次消费多次呢?
- redis有发布/订阅模式。
- 当有redis 客户端订阅某个通道时,redis服务器会将该客户端存储在一个订阅通道的链表中,链表存储的是订阅这个通道的多个客户端。
- 当通道中有消息推入时,便会通知链表中的所有客户端,形成了一次生产多次消费
- redis还有订阅匹配模式,就是按照 pattern进行匹配,类似于 name.*,代表订阅了所有name.开头的通道
7、什么是缓存穿透?什么是缓存击穿?什么是缓存雪崩?都如何避免?
一般来说,在关系型数据库之前,都会放置一层缓存缓冲区,用于在大量请求下,为关系型数据库减轻压力。
- 缓存穿透
- 指的是有大量的 请求访问 到一个或者多个缓存中不存在,并且数据库中也不存在的key,这样请求就会穿过缓存直接访问到数据库,从而对数据库造成巨大压力
- 解决方案:
- 在请求到达服务器之前增加校验,例如身份检查,验证码等,减少恶意请求到达服务器
- 数据库中不存在的key也设置缓存(比如设置一个空值),并且将过期的时间设置的短些(单次短时,高频)避免影响正常数据
- 使用布隆过滤器,原理就是在访问缓存或者数据库之前先在布隆过滤器中判断数据是否存在(布隆过滤器可以判断是否一定不存在,不能判断一定存在)
- redis的bitmap自定义实现
- redisson内置的布隆过滤器
- guava的布隆过滤器,可以设置精度(通过多次hash实现)
- 缓存击穿
- 指的是当一个热点key承载着客户端的大量请求时,这个key突然过期,此时大量的请求将缓存击穿,到达数据库,从而对数据库造成巨大压力。
- 解决方案:
- 热点数据设置永不过期
- key过期时增加线程锁,单一时间只有一个线程可以获取到锁。,减轻服务器压力
- 缓存雪崩
- 指的是当缓存中的多个key同时过期,那么一瞬间的流量就会打到服务器上,造成雪崩效应,压垮服务器。
- 解决方案:
- 将热点数据的过期时间设置的尽可能的离散些
- 搭配离散过期时间,将热点数据通过分发策略,散布到多个服务器实例中
- 设置热点数据永不过期
- 将过期时间放到数据里,然后利用定时任务去查询删除,也有缺点,对服务器cpu消耗较大,可以在闲时进行处理,并判断好key的分布,避免大量务必要查询
- 缓存降级
- 是指当前的访问量剧增,服务器出现问题,或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据自动降级,也可以配置开关实现人工降级
- 降级的最终目的是保证核心服务是可用的即使是有损的。而且有些服务是无法降级的
- 案例列举:
- 订单中的收货地址,当大量请求打进来时,避免订单无收货地址可用,缓存降级成默认收货地址
- 用户查询历史中,当大量请求打进来,避免用户查询历史为空白页面,使用热点数据作为降级策略
- 缓存更新
- 除了缓存服务器自带的缓存失效策略之外,(redis中有6种策略可以选择,redis中同时存在惰性删除和定时删除策略),我们还可以根据业务需求自定义缓存淘汰策略
- 定时任务去清理过期的缓存(定时清理)
- 提供手动清理缓存的接口
- 当有用户请求过来是,再判断这个请求用到的缓存是否过期,过期的话,就去底层系统得到新的数据并更新缓存(惰性删除,可以使用代理的方式保证操作类)
- 除了缓存服务器自带的缓存失效策略之外,(redis中有6种策略可以选择,redis中同时存在惰性删除和定时删除策略),我们还可以根据业务需求自定义缓存淘汰策略
- 缓存预热
- 系统在上线前,先将热点数据(如果能判断出来的话),或者全部数据打到缓存层,避免服务一上线就被大量请求打到数据库
- 缓存并发
- 指的是多个客户端同时使用set key的问题
- 解决方案
- 将所有操作放到队列中,排队执行
- 使用锁,在操作之前获取锁,如果是单体系统可以使用线程锁,如果是分布式系统,需要用到分布式锁,保证在同一时刻只有一个客户端操作缓存
多机功能
1、多机有哪些模式?模式的原理是什么?优缺点有哪些?
-
主从复制
- redis的复制功能允许用户根据一个redis服务器来创建任意多个该服务器的复制品,其中被复制的主服务器(master),而通过复制出来的服务器复制品则为从服务器(slave)。
- 原理:
- 只要主从服务器之间网络连接正常,主从服务器两者会具有相同的数据,主服务器会一直将发生在自己身上的数据更新同步给从服务器,从而一致保证主从服务器的数据一致性
- 一个服务器通过客户端发送slaveof host port成为目标主服务器的从服务器
- 主服务器通过命令传播的方式,将所有涉及写的操作传播给从服务器
- 从服务器也会发送心跳监测给主服务器,心跳监测中包含自己当前的偏移量,从而保证命令不丢失
- 优点
- master/slave角色,数据备份
- master/slave 在网络连接正常的情况下,数据具有一致性
- slave 从库可以承担一部分读功能
- 缺点
- 无法保证高可用,主服务器一旦宕机,整个服务无法使用
- 只有主服务器能够进行写命令,没有缓解主服务器压力
-
哨兵模式(sentinel)
- redis sentinel是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移
- 原理:
- 监控(monitoring):Sentinel会不断地检查主服务器和从服务器之间是否连接正常
- 提醒(notification):当被监控的某个redis服务器出现问题时,sentinel可以通过api向管理员或者其他应用程序发送通知
- 自动故障转移(automatic failover):当一个主服务器无法正常工作时,sentinel会在一开始进在sentinel中使用raft算法在所有sentinel节点中选出领头sentinel负责主服务器的选择,并且进行故障转移工作
- 故障转移工作是sentinel通过向从服务器发送slaveof 命令来进行的
- 优点:
- 保证了高可用性
- sentinel选取主服务器原则包含:从服务器在线/5s内回复过sentinel的info命令/从服务器状态较新/从服务器优先级/从服务器偏移量/运行id较小(即存活时间最长)
- 监控了各个节点,节点故障发送通知,哪怕是从节点
- 自动故障迁移
- 保证了高可用性
- 缺点:
- 主从模式,切换需要时间,切换过程中需要stw(stop the world),服务不可用
- 没有缓解master的写压力
-
集群(proxy代理模式)
- Twemproxy是一个Twitter开源的一个redis和memcache快速/轻量的代理服务器.
- 原理:
- Twemproxy是一个Twitter开源的一个redis和memcache快速/轻量的代理服务器,Twemproxy是一个快速的单线程代理程序,支持Memcacheed ASCII协议和redis的RESP协议
- 本质上是客户端自定义分片方式,使用hash算法来进行分片分派
- 特点:
- 支持多种算法,MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
- 支持失败节点自动删除
- 后端Sharding分片逻辑对业务同名,也无妨的读写方式和操作单个redis实例是一致的
- 缺点:
- 增加了新的proxy,需要维护其高可用
- failover需要自己实现,并且由于存在代理,扩容需要自己处理
-
集群模式
- redis集群是一种同时集成高可用,高性能的无中心架构集群,每个节点负责保存不同槽点的数据以及整个集群的状态,每个节点都会和其他节点相连接
- 原理
- 最少需要三个master、三个slave节点
- 通过CRC32算法将16384个槽点分配到集群中的各个主节点当中
- 使用Gossip协议进行广播选举,超过半数的master赞同则晋升为主节点,并继续向集群中广播成为主节点的消息,更新整个集群状态
- 优点:
- 无中心架构,哪个节点影响性能瓶颈,少了proxy层
- 数据按照slot存储分布在多个节点,节点间的数据共享,同时共享整个集群状态,可以动态调整数据分布
- 可扩展,可线性扩展到1000个节点,节点可动态添加或删除
- 高可用性,存在主从模式,主节点不可用时,从节点自动升级为主节点,并向集群中进行广播自己升级为主节点,更新集群状态
- 实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到master的角色提升
- 缺点:
- 资源隔离性较差,容易出现相互影响的情况
- 数据通过异步复制,不能保证数据的强一致性,也就是在某一时刻可能出现数据不一致的情况,因为主从复制期间可能存在数据不一致,只能保证数据最终一致性
2、集群是为了解决什么问题而存在的?
- 解决线性可扩展问题,横向扩展问题,通过增加节点,来增加数据处理能力
3、redis诞生以前怎么解决这个问题?
- 通过客户端分片、代理分片、查询路由、预分片、一致性哈希、客户端代理/转发等
4、Redis集群化面临的问题是什么?
- Redis集群本身要解决的是可伸缩问题,同时数据一致性、集群可用性等一些列问题。前者涉及到了节点的哈希槽的分配(重分配),节点的增删,主从关系与变更等。后者则是故障发现,故障转移,选举过程等。
5、Redis集群实现的核心思想和思路是什么?
- 通过消息的交互(Gossip)实现去中心化,通过hash槽的分配和重分配,实现线性可扩展
使用案例及启发
- 使用redis的bitmap做用户签到
- 原理:将用户的id(一般为自增id,如果不是自增id,那么做映射)
- 将日期作为key的bitmap上用户自增的偏移量位置存放1代表已经签到,存放0代表未签到
- 单机模式下可以使用客户端分片,用于内存的压缩
- 原理:将一个key中存放多个数据,通过使用crc16或者crc32方法计算出他的分片索引和偏移量
- 需要预先定义分片容量和总分片数量,这个需要与实际生产进行搭配
读书笔记脑图
redis in action
redis 设计与实现
推荐阅读顺序
如果是对redis有一定基础的,推荐先看《redis 设计与实现》了解原理后,再回头去看《redis in action》可以更深刻的理解作者的应用场景和思想
如果是redis基础很少,可以先看《redis in action》了解redis的基础数据结构,再回头深入了解《redis 设计与实现》。