redis是什么?

Redis是内存中的数据结构存储系统,也称nosql非关系型数据库。可以用作数据库,缓存、和消息中间件。

redis使用场景

缓存:会话缓存、全页缓存、数据缓存,作为缓存能大量减少后端的DB查询、web连接及IO操作的处理压力。

缓存热数据:对于那么频繁请求的数据,先存放到redis中减少DB压力。例子:系统中存有字典数据,数据不会经常变但是访问特别频繁,将这些数据存放到redis中加快访问速度减少DB压力。

削峰:秒杀活动中并发请求量会非常大,可以用redis做限流削峰。如前端为1000/s的并发请求,而后端只能处理100/s并发请求,那么将请求先扔到redis队列中缓存起来,超过100个就直接返回失败的提示。

计数器:通过redis提供的递增和递减命令实现计数器功能。如用户操作计数,访问计数限流。

分布式锁:分布式服务中需要对某个资源进行保护,保证不会产生并发,通过redis的分布式锁实现。

消息队列:适用于简单的消息队列(如即时聊天室)能接受部分数据丢失的场景,对于订单支付的消费消息还是用MQ会好一些,redis5版本的Stream类型也能实现mq一样的功能,可以尝试使用。

redis数据结构

主要包含了字符串string、散列hash、列表list、集合Set、有序集合sortedset等数据结构。

redis字符串的实现并没有使用C语言的char* 字符数组,而是设计了简单的动态 字符串SDS结构,SDS由《字符数组现有长度 len、分配给字符数组的空间长度 alloc,以及 SDS 类型 flags(分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64)、字符串实际数组buf[]》组成。

为什么 Redis 不用 char*?

1、不符合 Redis 希望能保存任意二进制数据的需求。

char是以\0为字符串结束的标识,如果数据本身就包含\0的二进制字符,那么就会将数据截断了。

2、不符合 Redis 对字符串高效操作的需求。

char字符串的长度查找、追加操作都需要遍历,而且在追加的过程中还可能存在空间不足够,还需要在程序上再动态分配空间的情况,从而增开了编程的复杂度。

Redis 的持久化机制

持久化就是数据落盘处理,将数据从内存保存到硬盘,以避免redis崩溃的时候丢失数据。

RDB持久化:就是定时将数据进行快照存储(全量数据),以rdb为后缀保存的文件(其实是挺像mysqldump备份sql数据库的操作)。RDB持久化的时候可以在配置文件里面设置定时自动处理,也可以手动执行save/bgsave要进行快照生成,相对于bgsave,save会阻塞进程(执行命令期间redis是没办法响应客户端请求),而bgsave是fork子进程的方式进行处理(也就是异步后台快照)。

rdb优点:保存的文件小,恢复速度快,对性能影响小。

rdb缺点:由于是定时快照,如果突然出现崩溃,那么在上次rdb之后的数据就有可能丢失。

AOF持久化:是记录每次对写的操作到AOF文件(其实也就是追加aof日志文件操作的方式),当服务器重启时会重新执行aof文件的命令来恢复原始数据。AOF持久化可以使用不同的fsync策略:无fsync,每秒fsync,每次写的时候fsync(使用默认的每秒fsync策略)

aof优点:支付秒级数据持久化,aof文件会自动重写,误操作也可通过修改aof文件后重启redis进行恢复;

aof缺点:文件大、恢复速度慢、对性能影响比较大。

REDIS persistence -- Redis中国用户组(CRUG)
redis
更多细节可见官方文档

Redis作为缓存使用时会遇到的问题

缓存穿透:当redis与DB数据库都不存在要查询的数据时,redis并不会写入这个查询的缓存,而导致每一次的请求都会转到DB去查询,严重的情况将会导致DB无法提供服务(例如恶意查询不存在的数据)

解决方案:

1、查询结果为空的时候,redis依然要进行缓存;

2、将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对 DB 的查询;

缓存击穿:当某一个缓存的key过期后,短时间内对此失效的key进行大量的请求,此时请求就会转到DB里面去,造成某一时刻请求量大,压力剧增,此时大并发的请求可能会瞬间把 DB 压垮。

解决方案:

1、使用互斥锁,不立即去 load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db 的操作并回设缓存,否则重试 get 缓存的方法

2、缓存设置为永不过期,由程序逻辑上做刷新处理

缓存雪崩:某一时刻缓存失效(如key设置了相同的失效时间、redis宕机),所有请求就有转到DB里面去,导致承受不住压力,过重雪崩、DB挂掉。

解决方案:

对于同时失效的情况,可设置缓存失效时间分散开,在原失效时间基础上加一个随机值(如1-5分钟)

对于宕机的情况:建议使用主从集群+哨兵 ,Redis Cluster 来避免 Redis 全盘崩溃的情况。开启redis持久化以保证宕机后能尽快恢复缓存集群。

redis实现消息队列的方案

1、基于List的 LPUSH+BRPOP 的实现:使用rpushlpush操作入队列,lpoprpop操作出队列。

优点:支持多个生产者和消费者并发进出消息,每个消费者拿到都是不同的列表元素

缺点:当队列为空时,lpop和rpop会一直空轮训,消耗资源;消息可能会丢失(宕机)、不能做广播、不能重复消费、不支持分组

2、PUB/SUB,订阅/发布模式:SUBSCRIBE,用于订阅信道;PUBLISH,向信道发送消息,生产组只生产一次消息,由中间件负责将消息复制到多个消息队列,每个消息队列由对应的消费组消费。

优点:广播模式,消息可以发布给多个消费者,消费者也可以订阅多个消息,消息是即时发送,不存在等待消费的情况 ;

缺点:只能做即时广播/即时反馈的业务,消息一旦发布,消费端若不在线将丢失 ,或者因为消费端因消息积压被强制断开后,也会导致消息丢失

3、基于Sorted-Set的实现:类似于java的SortedSet和HashMap的结合体,一方面她是一个set,保证内部value的唯一性,另一方面它可以给每个value赋予一个score

优点:就是可以自定义消息ID,在消息ID有意义时,比较重要。

缺点:不允许重复消息(因为是集合),同时消息ID确定有错误会导致消息的顺序出错。

4、基于Stream类型的实现

Stream为redis 5.0后新增的数据结构。支持多播的可持久化消息队列,实现借鉴了Kafka设计,弥补了Redis Pub/Sub不能持久化消息的缺陷。但是它又不同于kafka,kafka的消息可以分partition,而Stream不行。

Redis过期策略

定时过期:使用EXPIRE key time 的方式可以设置过期时间,超时会自动清除,会占用cpu资源处理数据,在一定程度上会影响缓存的响应时间及吞吐量;

惰性过期:当访问key的时候,才会判断key是否过期,可以最大化节省cpu资源,但对内存不友好,极端情况下,过期的key一直未补访问,从而导致占用内存未能释放;

主动清理:当已用内在超maxmemory限定时,触发主动清理策略;

Redis的内存淘汰策略

当内存不足时,需要写入新记录,新记录的空间就要先把旧的空间按一定的策略进行释放
  • volatile-lru:在那些设置了expire过期时间的缓存中,清除最少用的旧缓存,然后保存新的缓存
  • volatile-lfu:在那些设置了expire过期时间的缓存中,清除最长时间未用的旧缓存,然后保存新的缓存
  • volatile-ttl:在那些设置了expire过期时间的缓存中,删除即将过期的
  • volatile-random:在那些设置了expire过期时间的缓存中,随机删除缓存
  • allkeys-lru:清除最少用的旧缓存,然后保存新的缓存
  • allkeys-lfu:清除最长时间未用的旧缓存,然后保存新的缓存
  • allkeys-random:在所有的缓存中随机删除(不推荐)
  • noeviction:旧缓存永不过期,新缓存设置不了,返回错误

备注:过期策略都是在主库处理,redis并不会扫描从库,删除主库时,会有一条AOF日志的处理记录,从库是依据此记录进行清理工作,在3.2版本之前会存在读取到从库脏数据的情况(主库已删,而从库依然能读到数据)