前言
这里简单记录下
redis常见问题
的总结
为什么要使用redis
主要原因:性能和并发,redis
还可以实现分布式锁,但是分布式锁可以使用中间件ZooKeeper
或数据库等代替
- 性能
当需要执行耗时比较久,且结果不会发生频繁变动的SQL,就特别适合将运行结果放入缓存,后面的请求去缓存中读取,使得请求能够迅速响应
- 并发
在高并发的情况下,所有的请求直接访问数据库,会导致数据库出现连接异常,这时就需要使用redis做缓存,让请求先访问redis缓存,而不是直接访问数据库
使用redis
有什么缺点
- 缓存和数据库双写一致性问题
- 缓存击穿问题
- 缓存雪崩问题
- 缓存的并发竞争问题
具体解决方案,详见下文
单线程的redis
为什么这么快
redis6.0
针对性能优化引入多线程网络IO
- 纯内存操作
- 单线程,避免频繁的上下文切换
- 采用非阻塞的I/O多路复用机制(
epoll
)redis
还提供了select
、epoll
、evport
、kqueue
等多路复用函数库
redis
的数据类型以及应用场景
- String(字符串)
常用命令:GET、SET、INCR、DECR
应用场景:简单的key-value
类型,value不仅是string,也可以是数字;INCR/INCRBY
可以实现计数;SETEX
可以实现分布式锁 - Hash(哈希)
常用命令:HGET、HSET、HGETALL
应用场景:一个对象多个属性,value是map,可以存储用户信息,比如:用户包含 姓名,性别,年龄等 - List(列表)
常用命令:LPUSH、RPUSH、LPOP、RPOP、LRANGE
应用场景:LPUSH+RPOP、RPUSH+LPOP
可以实现消息队列,也可以实现数据结构栈
和队列
的功能 - Set(集合)
常用命令:SADD、SPOP、SDIFF、SINTER、SUNION、SMEMBERS
应用场景:SDIFF、SINTER、SUNION
可以求集合的差集、交集、并集,比如:计算自己独有的喜好,共同喜好,全部的喜好等 - Zset(sorted set 有序集合)
常用命令:ZADD、ZREM、ZRANGE、ZCARD
应用场景:ZREVRANK
可以实现排行榜,查询获取topK - HyperLogLog(算法)
常用命令:PFADD、PFCOUNT、PFMERGE
应用场景:可以实现基数统计,但不存储元素本身 - Pub/Sub(发布/订阅)
消息发布及消息订阅,可以实现实时聊天系统 - Transactions(事务)
提供基本的命令打包执行功能,可以保证多条命令顺序执行 - pipeline(管道技术)
参考博客
redis执行一条命令分为4个过程:这个过程称为RTT(Round Trip Time,往返时间),1
发送命令-->命令排队-->命令执行-->返回结果
MGET、MSET
有效节省了RTT,提高效率,但是不支持批量操作(如MDEL
),需要N次RTT,必然会消耗大量网络IO,同样redis需要调用多次read()和write()系统方法,系统方法会将数据从用户态转移到内核态,这样对进程上下文切换也有比较大的影响。
为了提高redis的读写能力,就有了pipeline的出现,我们对于多个命令执行,不再同步等待每个命令的返回结果,最终一次性读取所有服务端的响应,这样就解决多次RTT和read()与write()的问题,因为redis命令执行非常快,但是网络交互很慢,所以pipeline就是控制网络时间和上下文切换,提高了执行效率
使用管道需要注意的问题:- redis原生批命令是原子性的,但是pipeline不保证执行的原子性,如有命令执行失败(不会回滚,也不会影响未执行的),将会丢失执行结果,所以使用pipeline时,需要自己保证执行命令的数据安全性;
- pipeline只适用不需要同步获取执行结果的场景,比如
HINCR、HSET
等更新操作,不适用于GET、HGET
等查询操作; - pipeline打包命令不能没有节制,如果pipeline打包命令数据过多,会导致一次pipeline同步等待时间过长,影响客户端体验甚至会造成网络阻塞;
redis
的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略
- 为什么不是用定时删除
定时删除,用一个定时器来负责监视key,过期则自动删除,虽然内存可以及时释放,但是十分消耗CPU资源。在高并发的请求下,CPU要将时间应用在处理请求上,而不是删除key,因此没有采用该策略 - 定期删除+惰性删除的工作原理
定期删除,redis默认每隔100ms检查是否有过期的key,过期则删除。注意:redis不是每隔100ms将所有key检查一次,而是随机抽取检查(如果每隔100ms全部key进行检查,可能导致redis卡死,造成灾难性的后果),因此,只采用定期删除策略,可能会导致很多key到期没有被删除。于是,就需要配合惰性删除策略,在获取key时,redis会先进行检查,对key进行过期删除操作
定期删除+惰性删除存在的问题:定期删除是随机抽取检查删除过期key,如果没有删除过期key,并且没有及时去请求key,所以也没有触发惰性删除策略,这样,redis内存持续增长,那么就应该采用内存淘汰机制
- 内存淘汰机制
在redis.conf
中有一行配置,配置内存淘汰的策略1
# maxmemory-policy volatile-lru
noeviction:当内存不足时,新写入操作会报错(
应该不会用
)
allkeys-lru:当内存不足时,在键空间中,移除最近最少使用的key,一定不能做持久化存储(推荐使用
)
allkeys-random:当内存不足时,在键空间中,随机移除某个key(应该不会用
),至少应该删除最少使用key而不是随机删
volatile-lru:当内存不足时,在设置了过期时间的键空间中,移除最近最少使用的key,适合把redis当缓存,又做持久化存储(不推荐使用
)
volatile-random:当内存不足时,在设置了过期时间的键空间中,随机移除某个key(不推荐使用
)
volatile-ttl:当内存不足时,在设置了过期时间的键空间中,有更早过期时间的key优先移除(不推荐使用
)
redis
和数据库双写一致性问题
数据库和缓存双写,必然存在数据一致性
- 强一致性,不能使用数据库缓存
- 最终一致性,采取正确更新策略,先更新数据库,再更新缓存
- 数据库更新失败,则更新返回失败;
- 数据库更新成功,缓存更新失败,将数据发到MQ,监控MQ继续更新缓存;
- 数据库更新成功,缓存更新功能,则更新返回成功
如何应对redis
缓存击穿和缓存雪崩问题
这两个问题一般中小型项目基本很难碰到,只有高并发的项目,流量达到百万以上,需要考虑的问题
缓存击穿,即大量请求缓存中不存在的数据,结果所有请求都打到数据库上,从而导致数据库连接异常
解决方案:- 采用互斥锁,缓存失效时,先获取锁,得到锁,从数据库请求数据后释放锁,其他请求获取锁失败,则休眠一段时候后重试。增加锁机制,可以减轻数据库压力,同时也会降低吞吐量
- 采用异步更新机制,从缓存中查询key无论成功与否,都直接返回。value维护缓存失效时间,缓存失效异步加载数据库更新缓存。该方案项目启动时需要先加载数据库,做缓存预热
- 提供能快速判断请求是否有效的拦截机制,比如:布隆过滤器,将缓存key维护一个bitmap,如果key不存在,则不需要访问redis缓存,可以快速拦截无效请求
- 接口限流与熔断降级,重要接口需要做好限流策略,防止恶意刷接口,同时需要做降级准备,当接口的关键服务不可用时,进行熔断,具备失败快速返回机制
缓存雪崩,即同一时间大面积缓存失效,这时又来大量的请求,结果请求都打到数据库上,从而导致数据库连接异常
解决方案:- 采用互斥锁,同上方案1,但明显会降低吞吐量
- 设置缓存失效时间加上一个随机时间,尽量让缓存失效时间均匀分布,尽可能避免缓存集体失效
- 缓存备份,即双缓存,缓存A设置失效时间30分钟,缓存B不设失效时间,先访问缓存A,缓存A没有访问缓存B(脏读),异步线程更新缓存A和缓存B
如何解决redis
的并发竞争key问题
问题描述:并发竞争key问题,即多个子系统同时对一个key进行操作,执行顺序和我们期望顺序不同,最终导致执行结果不同
解决方案:
- 分布式锁+时间戳
- 整体技术方案
准备一个分布式锁,对key进行操作时需要先获取锁,获取锁成功才能对key进行操作
加锁的目的实际上就是把并行读写改成串行读写的方式,从而避免资源竞争 - 分布式锁
传统的加锁方式lock只适合单点环境,因为这是分布式环境,需要分布式锁
分布式锁可以基于很多种方式实现,但基本原理是一致的:用一个状态值来表示锁,通过状态值来标识对锁的占有和释放 - 分布式锁的要求
- 在分布式环境下,任意时刻只有一个客户端的一个线程访问
- 高效高可用的获取锁和释放锁
- 具备可重入特性(重新进入,由多个任务并发使用,不必担心数据错误)
- 具备锁的失效机制,防止死锁
- 具备非阻塞锁的特性,没有获取到锁,直接返回获取锁失败
- 分布式锁的实现
数据库(
锁表,增删记录
)
Memcached(add
)
Redis(setex
)
Zookeeper(临时节点
) - 时间戳
如果对key操作不要求顺序,那么通过分布式锁抢占,就可以实现key的操作;
如果对key操作要求顺序,加入有一个key,系统A将key设置为valueA,系统B将key设置为valueB,期望key的value按照valueA-->valueB
的顺序变化,那么就需要保存一个时间戳,例如:假设系统B先获取到锁,将key设置为{valueB, 3:05},当系统A抢占到锁时,发现自己的时间戳valueA早于缓存中的时间戳valueB,此时就不做key的更新操作,以此类推1
2系统A key {valueA, 3:00}
系统B key {valueB, 3:05}
- 消息队列
在高并发的情况下,可以通过消息中间件进行处理,将并行的读写进行串行化,把redis的更新操作放进队列使其串行,保证可以一个一个顺序执行
redis
的三种运行模式
单机模式 – standaloan(
基本不用做生产环境
)
优点:- 架构简单,部署方便
- 性价比高,缓存使用时无需备用节点
- 性能高
缺点:
- 不能保证数据可靠性,因此不能用于数据可靠性要求高的业务
- 性能受限于单核CPU的处理能力(redis是单线程机制),所以适合操作命令简单,计算较少的场景(这里的性能与网络IO无关)
主从模式 – master/slaver模式(
redis2.8版本之前的模式
)
优点:- 可靠性高,双机主备能够在主机宕机时将从机升级为主机提供服务,保证服务的平稳运行;数据持久化方面,能有效解决数据异常丢失问题
- 读写分离策略,从节点可以扩展主库节点的读能力,有效应对高并发的场景
缺点:
- 故障恢复复杂,需要手动将从节点提升为主节点并进行故障转移工作
- 主库的写能力和存储能力受到单机的限制
- 无法实现动态扩容
哨兵模式 – sentinel模式(
redis2.8及之后的模式
)
优点:- 具备主从模式的所有优点
- 主从可以自动切换,可用性更高
缺点:
- 相对主从模式更复杂一些
- 资源浪费,slave节点做为备份节点不提供服务
- 主要针对主节点进行监控高可用切换,无法做到从节点故障转移
- 动态扩容比较复杂
集群模式 – cluster模式(
redis3.0版本之后的模式
)
优点:- 有效解决了redis在分布式方便的需求,节点数据共享,可动态调整数据分布
- 负载均衡可以解决单机内存、并发和流量瓶颈等问题
- 扩展性:可实现动态扩容,提高扩展性和可用性,降低运维成本
- 高可用:部分节点不可用,能够实现故障转移,master选举,保证集群可用
- 无中心化分布式系统,取代LVS + Twemproxy层三层架构,系统复杂性降低,读写性能提高
缺点:
- 架构比较新,最佳实践较少
- 为了性能提升,客户端需要缓存路由表信息
- 数据异步复制,不保证数据强一致性
- MultiOp和Pipeline支持有限
- 支持单机版的redis处理单一键命令,不支持多个键的操作
set、mset、mget
以及事务,同时不支持多db
redis
的持久化机制
RDB持久化
:保存某个时间点的全量数据快照
- 手动触发
SAVE:阻塞Redis服务进程,直到RDB文件被创建完毕
BGSVAE:调用fork创建一个子进程创建RDB文件,不会阻塞服务进程,LASTSAVE
指令可以查看最近的备份时间 - 自动触发
根据
redis.conf
配置里的save m n
定时触发(用的是BGSAVE)
AOF持久化
:保存写状态
记录除了查询以外的所有变更数据库状态的指令
以append的形式追加保存到AOF文件中(增量)
日志重写解决AOF文件不断增大的问题,原理如下:
调用fork创建一个子进程
子进程把新的AOF写到临时文件里,不依赖原来的AOF文件(处理重复命令合并,减小文件尺寸和内存占用,加快恢复时间
)
主进程持续将新的指令同时写到内存和原来的AOF文件
主进程获取子进程重写AOF完成信号,往新的AOF同步增量
使用新的AOF文件替换掉旧的AOF文件触发机制:always、everysec、no
命令 always everysec no 优点 同步,非常慢,不会丢失数据 异步,足够快,不影响性能 从不同步,处理命令速度快 缺点 IO开销大 丢失1s的数据 不可控 RDB和AOF的优缺点
策略 优点 缺点 RDB 全量数据快照,文件小,恢复快,适合灾备 无法频繁备份,数据量大会由于IO问题影响性能 AOF 可读性高,适合增量数据备份,数据不容易破损 文件体积大,恢复时间长
RDB-AOF混合持久化
BGSAVE做全量持久化,AOF做增量持久化
缺点:兼容性差,一旦开启混合持久化,在低版本(4.0以下)不支持AOF文件,无法恢复,同时全量持久化的RDB文件,阅读行较差