目录
前言
Redis作为目前最火的缓存中间件,开发人员对Redis的要求也越来越高.熟练的掌握Redis的基础知识,是一个开发人员能开发高性能,高可用系统的基石.
Redis是什么
Redis是一个开源的内存数据存储系统,通常被称为缓存存储和键值存储,是一个高性能,轻量级数据库,支持丰富的数据类型和各种高级的功能而闻名.是最受欢迎的NoSQL数据库之一.因此被许多应用程序采用.
Redis的主要特点
- 内存存储
- 键值存储
- 数据类型
- 持久性
- 发布-订阅
- 事务
- 高可用性
Redis的使用场景
- 会话存储
- 缓存存储
- 实现分布式锁
会话存储
redis可以用于存储用户的会话数据.将会话存储到redis中,可以轻松的实现分布式,高可用性的会话管理,确保用户在多个服务器之间的会话一致.
缓存存储
缓存是redis最常见的使用场景之一,将频繁访问的数据存储到redis中.可以提高整个应用程序的效率,减少对后端数据库服务器的压力.
实现分布式锁
由于redis天生支持分布式系统,所以在redis上加锁操作就是加的分布式锁,可以确保多个客户端的互斥和共享资源.
Redis为什么这么快
Redis之所以快速的原因主要有以下几点:
- 基于内存操作
- 高效数据结构
- 多路I/O复用模型
- 单线程执行
基于内存操作
redis的所有操作都是基于内存的,而且redis的大多数操作都是简单的读取和写入功能,而存取和大部分操作都是消耗在了I/O上,而进行I/O最快的就是内存了.因为内存的存取操作要远远快于硬盘的读取操作的.
高效的数据结构
redis通过一系列的数据机构来保证redis的效率,其中主要使用动态字符串,哈希表,双向链表,压缩列表,跳跃表,集合,有序集合等高效的数据结构.这样就能将数据高效的存储和获取,这是保证redis性能高的基本条件
多路I/O复用模型
redis采用多路I/O复用模型,多路是指多个网络连接,复用是指多个I/O复用一个线程进行操作,这就使得redis在单线程的基础上能够进行处理并发的请求,多路复用I/O模型是内部使用epool代理实现的,epool会同时监测多个I/O流事件,当没有事件时,就会进行阻塞.
单线程执行
redis的单线程执行就完美的避免了在多线程中出现的问题,例如没有锁竞争和线程的创建和销毁以及线程的调度和切换带来的时间消耗.这就使得redis在单线程中的效率就远远高于多线程执行.
Redis常见的数据结构
redis常见的数据结构有5种:分别是String字符串,List列表,Hash哈希表,Set集合类型,Sorted set有序集合列表.
这5种类型的常用用途如下:
1. String字符串 ,常见的使用场景有存储用户的会话信息,存储缓存信息,存储整数信息,可以使用incr 实现整数+1,decr实现整数-1.
2. List列表类型,常见的使用场景有实现简单的消息队列,存储某项列表的数据.
3. hash哈希类型,常见的使用场景有存储 Session 信息、存储商品的购物车,购物车非 常适合用哈希字典表示,使用人员唯一编号作为字典的 key,value 值可以存储商品的 id 和数量等信息、存储详情页信息
4. Set集合类型,是一个无效且唯一的键值集合,它的使用场景有用户的关注功能,可以用集合存储,保证用户信息不重复.
5. Sorted Set集合类型,是一个有序且唯一的键值集合,常见的使用场景有用来存储排名信息,关注列表功能.
Redis有序列表的实现
有序列表是有ziplist压缩列表和skiplist跳跃表实现的.
- 压缩列表是字节数组实现的,是redis为了节省内存而设计的一种线性的数据结构,可以包含多个元素,每一个元素可以是一个字节数组或者整数.
- 跳跃表是一种有序的数据结构,它是通过在每个节点中维持指向多个节点的地址,从而能够快速的访问节点的目的.
注意:当数据比较少时,就使用压缩列表来存储,反之当数据比较多,就使用跳跃表来存储.使用压缩列表必须满足以下两个条件:
1. 有序集合的元素个数要小于128个
2. 有序集合中存储的每个元素成员长度必须小于64字节.
如果不满足上述的任意一个条件,就会使用跳跃表存储.
跳跃表的执行流程
跳跃表的底层使用多个链表实现的.
使用跳跃表的时候,查询效率就会很高.
跳跃表的查询:
我们以上述图中查询32为例:
先是从最上面的第2层链表开始找,1比32小,向后移动一位,7比32小,继续向后移动一位,发现为空,就以7为目标,移动到下一层第1层进行查找,18比32小,就继续向后移动一位,发现是77比32大,就会以18为目标,移动到下一层进行查询,再第0层对比18后面的数据,就找到了32.
从上面的流程可以看出,跳跃表的查询是先从最上面的一层开始查找,如果本层节点的值大于要查询节点的值,或者本层的节点为null之后,就会以上一个节点为目标,移动到下一层进行并循环的进行查询,直到知道该节点或者为null然后返回.
Redis分布式锁实现
redis天生就支持分布式系统,所以在redis里面加的锁就是分布式锁,所谓的分布式锁在redis这里是一个逻辑概念.
使用命令 setnx [key value] 加锁 执行结果为1表示成功
使用命令 del [key value] 释放锁
通过setnx多客户端1进行加锁
此时再次使用客户端2进行加锁,就会加锁失败.
我在客户端1中进行解锁,客户端2就能加锁成功了.
使用分布式锁存在的问题
产生死锁
会产生死锁:得到锁的线程下线了,锁没有被释放,得到锁的线程就会一只占用锁,导致死锁.
解决方案: 添加超时时间,(模仿MySQL 模式为10s) 使用expire命令.
通过设置超时时间来解决,如果超过超时时间没有释放锁,就会自动释放锁对象,也就不会存在死锁问题了.
锁误删
锁误删问题:某个线程的执行时间大于超时时间,就会产生锁误删问题.
当线程1 获取到lock锁,并设置超时时间为10s,线程2就会自旋等待,但是线程1实际执行需要15s,于是在第10秒的时候,由于超时时间到了,线程1就会把锁释放,线程2就会得到这把锁,当线程1全部15s执行完成之后,就会进行锁的删除操作,由于这会这把锁已经是线程2获取到了,所以线程1就会把线程2的锁删除掉.
解决方案:给锁增加标识,在进行删除的时候,判断这个锁是否属于当前线程,如果是当前线程,则删除,如果不是则不删除.
Redis数据不丢失策略
以为redis的数据是存储在内存中,在内存中的数据断电后就会丢失,而redis使用了持久化技术来解决这个问题.
redis持久化的三种方式:
- 快照方法(RDB) 将某一个时刻的的内存数据,以二进制的方式写入磁盘
- 文件追加方式(AOF) 记录所有的操作指令(set del),并于文本的形式追加到文件中,
- 混合持久化 结合了RDB和AOF的优点,在写入时,会把当前的数据以RDB的方式写入,在后续数据中,使用AOF的方式进行写入.
RDB和AOF的区别
RDB和AOF都是Redis持久化的两种策略
RDB
工作原理:记录某一时刻内存的数据,以二进制的方式写入硬盘
优点:
- 执行效率比较高,因为是二进制的数据,所以在写入硬盘的时候速度比较快.在恢复时,效率也是很高.
- 文件比较小,占用较小的硬盘空间.
缺点:
由于是记录某一时刻的数据,所以如果在redis快照期间发生了故障,可能会导致这一时刻的数据都丢失.
AOF
工作原理:将redis的日志文件以文本的形式追加到硬盘文件中去,当redis需要重启的时候,通过回放硬盘文件的内容来恢复数据.
优点:
- 数据完整性比较好.
- 由于文件是文本文件,所以在查看时会很方便.
缺点:
- 由于是追加的方式,硬盘文件会随着时间越来越大,当redis需要重启时,就会影响重启的效率.
- 对硬盘访问比较频繁,需要频繁的写入文件,可能会对硬盘造成压力.
缓存雪崩
短时间内,有大量的缓存同时过期,导致大量的请求全部访问数据库,从而对数据库服务器造成压力,严重情况下导致数据库服务器宕机.
缓存正常的访问:
缓存雪崩之后的访问
解决方案:
- 加锁排队:可以通过synchronized进行加锁排队,可以起到缓冲作用,防止大量用户同时访问数据库,但缺点就是增加了系统的响应时间.
- 随机化缓存的过期时间:为了避免大量缓存同时过期,可以设置缓存的随机过期时间. 这样就避免了大量的缓存同时过期.
- 采用二级缓存:除了redis本身,再去设置一个缓存(例如,hash表,),当redis失效后,先去查询二级缓存.
我们主要采用随机化缓存的过期时间
缓存击穿
缓存击穿是指某个热点缓存,在某一时刻真好失效了,然后此时正好有大量的并发请求,此时这些大量的并发请求将会直接访问数据库,会给数据库造成巨大的压力.这种情况就叫做缓存击穿.
解决方案
- 加锁排队: 类似于缓存雪崩的解决方案,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力。
- 设置永不过期:对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。
我们主要采用设置永不过期
缓存穿透
缓存穿透是指在查询数据库和缓存都没有数据,因为数据库没有数据,出于容错考虑,不会将结果保存到redis中,因此每次请求都会查询数据库,这种情况就是缓存穿透.
解决方案:
不管有没有查询到数据库,都将结果保存到redis中.
小结
本文为redis最常见的面试题.
如今我努力奔跑,不过是为了追上那个曾经被寄予厚望的自己.