Redis
是最受欢迎的NoSQL数据库之一.
-
基于内存运行,性能高效
-
支持分布式,理论上可以无限扩展
-
key-value存储系统
-
开源的使用ANSI C语言编写 , 遵守BSD协议 , 支持网络 , 可基于内存亦可持久化的日志型 , Key-value数据库
-
特点:
- C/S通讯模型
- 单进程单线程模型
- 丰富的数据类型
- 操作具有原子性
- 持久化
- 高并发读写
- 支持lua脚本
-
应用场景:
- 缓存系统('热点'数据:高频读,低频写)
- 计数器
- 消息队列系统
- 排行榜
- 社交网络
- 实时系统
Redis的数据类型:
String类型 ,哈希类型,列表类型,集合类型和顺序集合类型
String类型:
- 它是一个二进制安全的字符串,不仅能存储字符串,还能存储图片,视频等多种类型,最大长度支持512M
常用命令:
- GET/MGET
- SET/SETEX/MSET/MSETNX
- INCR/DECR
- GETSET
- DEL
哈希类型:
由field和关联的value组成的map。其中field和value都是字符串类型的。
常用命令:
- HGET/HMGET/HGETALL
- HSET/HMSET/HSETNX
- HEXISTS/HLEM
- HKEYS/HDEL
- HVALS
列表类型:
是一个插入顺序的字符串元素集合,基于双链表实现
常用命令:
- LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
- LINDEX/LRANGE
- LLEN/LTRIM
集合类型:
Set类型是一种无顺序集合,它和List类型最大的区别是:集合中的元素没有顺序,且元素是唯一的。
Set类型的底层是通过哈希表实现的,其操作命令为:
- SADD/SPOP/SMOVE/SCARD
- SINTER/SDIFF/SDIFFSTORE/SUNION
应用场景:
- 在某些场景中,通过交集,并集和差集运算,通过Set类型可以非常方便地查找共同好友,共同关注和共同偏好等社交关系。
顺序集合类型:
ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从大到小的排序。与Set类型一样,其底层也是通过哈希表实现的。
常用命令:
- ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT
- ZINTER/ZDIFF/ZDIFFSTORE/ZUNION
redis数据结构如下:
1.压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数,要么就是长度比较短的字符串,Redis就会使用压缩列表来做列表键的底层实现。
2.整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素不多时,Redis就会使用整数集合作为集合键的底层实现
简单动态字符串SDS
基于C语言中传统字符串的缺陷,Redis自己构建了一种名为简单动态字符串的抽象类型,简称SDS其结构如下:
struct sdshdr{
//buf数组已经使用字节数量,既SDS字符串长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于博爱村二进制数据
char buf[];
}
SDS几乎贯穿了Redis的所有数据结构,应用十分广泛。
SDS的特点:
- 常数复杂度获取字符串长度
- Redis中利用SDS字符串的len属性可以直接获取到所保存的字符串的长度,直接将获取字符串长度所需要的复杂度从C字符串的O(N)降低到了O(1)
- 减少修改字符串时导致的内存重新分配次数
- 通过C字符串的特性,我们知道对于一个包含了N个字符的C字符串来说,其底层实现总是N+1个字符长的数组(额外一个空字符结尾)
- 那么如果这个时候需要队字符串进行修改,程序就需要提前对这个C字符串数组进行一次内存重分配(可能是扩展或者释放)
- 而内存重分配就意味着是一个耗时的操作。
Redis巧妙的使用了SDS避免了C字符串的缺陷。在SDS中,buf数组的长度不一定就是字符串的字符数量加一,buf数组里面可以包含未使用的字节,而这些未使用的字节由free属性记录。
与此同时,SDS采用了空间预分配的策略,避免C字符串每一次修改时都需要进行内存重分配的耗时操作,将内存重分配从原来的每次修改N次就分配Nc---》降低到了修改N次最多分配N次。
Redis特性:
1.事务
- 命令序列化,按顺序执行
- 原子性:
- 要么同时成功,要么同时失败
- 三阶段:开始事务-》命令入队-》执行事务
- 命令:
- MULTI/EXEC/DISCARD
2.发布订阅
-
Pub/sub是一种消息通讯模式
-
Pub发送消息,Sub接受消息
-
Redis客户端可以订阅任意数量的频道
-
‘fire and forgot’ 发送即遗忘
-
命令:
- Publish/Subscribe/Psubscribe/UnSub
-
类似MQ中的广播通知
3.Stream
- Redis 5.0新增
- 等待消费
- 消费组(组内竞争)
- 消费历史数据
- FIFO
Redis常见问题:
缓存击穿:
概念:在Redis获取某一key时,由于Key不存在,而必须向DB发起一次请求的行为,称为‘Redis击穿’
同一时间访问大量不存在的key,就会造成缓存击穿问题。
原因:
- 第一次访问
- 恶意访问不存在的key
- key过期
规避方案:
- 服务器启动时,提前写入
- 规范Key的命名,通过中间件拦截
- 对某些高频率访问的key,设置合理的过期时间TTL或永不过期
缓存雪崩:
概念:Redis缓存层由于某种原因宕机后,所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。
规避方案:
- 使用Redis集群,主从复制,哨兵模式,某个服务器宕机以后其他服务器接上来。
- 限流,限制端口的访问次数
缺点:
1.缓存和数据库双写一致性问题
一致性问题很常见,因为加入缓存之后,请求时先从Redis中查询,如果redis存在数据就不会走数据库了,如果不能保证缓存和数据库的一致性就会导致请求获取到的数据不是最新的数据:
解决方案:
- 编写删除缓存的接口,在更新数据库的同时,调用删除缓存的接口删除缓存中的数据。这么做会有耦合高以及接口调用失败的情况
- 消息队列:ActiveMq,消息通知
2.缓存的并发竞争问题
并发竞争,指的是同时有多个子系统去set同一个key值。
解决方案:
- 最简单的方式就是做一个分布式锁,大家去抢锁,抢到锁就做set操作即可
3.缓存雪崩问题
缓存雪崩:既缓存同一时间大面积的失效,这时候又来了一波请求,结果请求都怼到数据库上,从而导致数据库连接异常。
解决方案:
- 1.给缓存的失效时间,加上一个随机值,避免集体失效
- 2.使用互斥锁,但该方案吞吐量明显下降了
- 3.搭建redis集群
4.缓存击穿问题
缓存穿透:即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据库上,从而数据库连接异常。
解决方案:
- 利用互斥锁,缓存失效的时候,先去获得所,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
- 采用异步更新策略,无论key是否取到值,都直接返回,value值中维护一个缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。
面试题:
1.都说Redis速度快,那Redis为什么这么快呢?
- 基于内存:Redis是使用内存存储,没有磁盘IO上的开销。数据存在内存中,读写速度快。
- 单线程实现(Redis6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。
- IO多路复用模型:Redis采用IO多路复用技术。Redis使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间
- 高效的数据结构:Redis每种数据类型都做了优化,目的就是为了追求更快的速度。
2.Redis为何选择单线程呢?
- 单线程实现可以避免过多的上下文切换开销。程序始终运行在进程中单个线程内,没有多线程切换的场景。
- 避免同步机制的开销。如果Redis选择使用多线程模型,需要考虑数据同步的问题,则必然会引入某些数据库过程中带来更多的开销,增加程序复杂度的同时还会降低性能。
- 实现简单,方便维护。如果Redis使用多线程模式,那么所有的底层数据结构的设计都必须考虑线程安全的问题,那么Redis的实现将会变得更加复杂。
3.Redis 的应用场景有哪些?
- 缓存热点数据,缓解数据库的压力
- 利用Redis原子性的自增操作,可以实现计数器的功能,比如用户点赞数、用户访问数等。
- 作为简单的消息队列,实现异步操作
- 限速器,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速带年纪带来的不必要的压力
- 好友关系,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好值类的功能。
4.Redis怎么实现消息队列
- 有三种方式可以实现
- 使用列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。
- 发布订阅模式。类似于MQ的主题模式。只能消费订阅之后发布的消息,一个消息可以被多个订阅者消费。
- 延时队列。使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用‘zrangebyscore’指令获取N秒之前的数据轮询进行处理
5.讲讲Redis主从复制的原理?
- Redis的复制功能是支持多个数据库之间的数组同步。主数据库可以进行读写操作,当主数据库的数据发生变化时会自动将数据同步到从数据库。从数据库一般是只读的,它会接受主数据库同步过来的数据
- 原理:
- 当启动一个从节点时,它会发送要给‘PSYNC’命令给主节点
- 如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动要给后台线程,开始生产一份RDB快照文件
- 同时还会将从客户端client新收到的所有写命令缓存在内存中。RDB文件生成完毕后,主节点会将RDB文件发送给从节点,从节点会先将RDB文件写入本地磁盘,然后再从本地磁盘加载到内存中;
- 接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据;
- 如果从节点根主节点之间网络出现故障,连接断开了,会自动重连,连接之后主节点仅会将部分确实的数据同步给从节点
6.了解过期键的删除策略吗?
- 被动删除。在访问key时,如果发现key已经过期,那么会将key删除。
- 主动删除。定时清理key,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。
- 内存不够时清理。Redis有最大内存的限制,通过maxmemory参数可以设置最大内存,当使用的内存超过了设置的最大内存,就要进行内存释放,在进行内存释放的时候,会按照配置的淘汰策略清理内存。