Redis相关知识点

Redis:Remote Dictionary Server

  • 基于内存,速度非常快(因此受物理内存大小限制)
  • 数据结构非常丰富(String, List, Hash, Set, SortedSet)
  • 单线程,避免了线程切换和锁机制的性能消耗
  • 可持久化(RDB + AOF)
  • 支持发布订阅
  • 支持 Lua 脚本
  • 支持分布式锁
  • 支持原子操作和事务
  • 支持主从复制(Master-Slave)和高可用(Redis Sentinel)(3.0 版本以上)
  • 支持管道(管道在这里类似于 batch,一次性发送多个命令,一次性返回所有结果,减少网络开销)

主要的五种数据结构及使用场景

Redis 支持多种数据结构,常用的有 5 种

  • String:计数器,kv 存储,限流,分布式锁
  • List:当队列来使用(lpush + rpop 或者 rpush + lpop);发红包(抢红包是入队,拆红包则是出队);列表(朋友圈点赞,评论)
  • Hash:kv,比如存一些标签信息,在保存用户信息等的场景,Hash 和 String 的不同点在于,String 要修改某个字段必须将整条用户完整信息取出来,再进行系列化反序列化操作,而 Hash 可以只对某个字段进行修改,节省了网络流量,不过 Hash 要比 String 占用多一点内存;购物车(用户 id,商品 id,商品数量);存储对象
  • Set:好友、关注、粉丝、感兴趣的人的集合(sinter 获取交集,sismember 判断是否存在,scard 获取数量);首页随机推荐展示(srandmember);存储需要去重的场景,比如中奖,保证不会中两次
  • SortedSet:排行榜(因为有序)

单线程的 Redis 为什么这么快

  • Redis 是完全基于内存的,所以读写效率非常高,当然 Redis 存在持久化操作,但 Redis 的持久化操作是通过 fork 子进程和利用 Linux 系统的页缓存技术来完成,所以并不会影响 Redis 的性能
  • 对于 Redis 来说,它的性能瓶颈主要在网络和 IO 上,而不在 CPU,多线程频繁进行上下文切换会带来额外的性能消耗,可能会造成负优化;Redis 高版本也支持多线程,但是主要是用来执行对一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程的阻塞时间,提高执行的效率
  • 数据结构设计得合理高效
  • 采用了非阻塞 IO 多路复用机制:IO 多路复用是利用 select、poll、epoll 可以同时监测多个流的 IO 事件的能力,在空闲的时候,会阻塞当前线程,当有 IO 事件发生时才唤醒来处理就绪的流,这样就避免了大量的无用操作

数据过期淘汰策略

Redis 中数据过期淘汰策略采用的是定期删除 + 惰性删除

  • 定期删除(activeExpireCycle):Redis 后台有一个定时器来定时监视所有的 key,过期就删除。通过定期删除策略,可以保证过期的 key 最后都会被删掉,缺点就是每次都要都遍历 Redis 中的所有 key,非常消耗 CPU 资源,如果 key 过期了,但是定时器还没开始工作,这个过期的 key 就还能用
  • 惰性删除(expireIfNeeded):在使用 key 时,先判断 key 是否过期,如果过期则删除。这种方式也有缺点,就是虽然这个 key 过期了,但是一直没人用,那么它就会一直存在 Redis 中,造成浪费

因此 Redis 将这两种方式结合起来,定时随机抽取一些幸运 key 进行删除

这样没有缺点吗?

有,如果某个 key 是天选之子,每次都抽不到,但是已经过期了,就会常驻内存

所以 Redis 还会有另外的机制来处理这种情况,就是内存淘汰

内存淘汰机制

volatile:设置过期时间的 key
allkeys:全部 key
lru:最近最少使用
lfu:最近最不经常使用
random:随机
ttl:过期时间
noeviction:永不过期

排列组合:

  • volatile-lru
  • allkeys-lru
  • volatile-lfu
  • allkeys-lfu
  • volatile-random
  • allkeys-random
  • volatile-ttl
  • noeviction

默认是永不过期,如果满了就会报 OOM

Redis 和 Memcached 的区别

  • 存储方式不同:Memcachedb 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小;Redis 有部分存在硬盘上,这样能保证数据的持久性
  • 数据类型支持:Memcached 对数据类型支持相对简单;Redis 有复杂的数据类型

为什么要使用 Redis 而不用 map/guava 做缓存

缓存分为本地缓存和分布式缓存

map 或者 guava 实现的是本地缓存,这种缓存是存在内存中的,一旦 JVM 挂了或手动停掉,数据就丢失了,本地缓存无法做到多实例共享,因为每个实例都有自己的一份,生命周期和 JVM 绑定

Redis 或者 Memcached 则称为分布式缓存,支持多实例共享,Redis 还支持数据持久化,宕机数据还在

Redis 三大经典问题

缓存穿透

查询一个数据库不存在的数据,因为数据库没有,所以不能将数据库值缓存起来,导致每次请求都直接去到数据库,这样会增加数据库压力,降低系统性能,Redis 形同虚设

解决方案:

  • 缓存空值
    当请求的数据库不存在于 Redis 中也不存在于数据库时,设置一个默认空值,后续再进行查询时就直接返回,避免请求去到数据库,除此之外还需要对该数据设置一个合适的过期时间,不然后续这个 key 的数据在数据库有了,永远不能被查到

  • 布隆过滤器
    在数据写入数据库的同时,将这个 ID 同步到布隆过滤器中,当请求的 ID 不存在布隆过滤器中,则说明该请求查询的数据一定没有在数据库中保存,就不要再去查数据库

    • 原理:首先分配一块数组,里面的值全为 0,当存入元素时,采用 N 个哈希函数对该元素进行哈希计算,映射出来的位置全部设置为 1;当检查这个 key 是否存在时,也是同样的方法,如果得到的全为 1,则 key 存在;布隆过路器存的只是这个 key 计算出来的哈希值,并不是这个 key 本身,而哈希函数是会出现碰撞的,所以就算计算出来全是 1,也不能保证其实这个 key 存在,也就是会出现判断 key 存在,但实际不存在的的情况,存在误判;反之,当判断这个 key 不存在的时候,就一定不存在
  • 对于恶意构造的请求

    • 对参数进行校验,非法则拦截
    • 拉黑 IP(治标不治本)

缓存击穿

高并发流量,访问的这个数据是热点数据,请求的数据在数据库中存在,但是 Redis 存的那一份已经过期,后端需要从数据库加载数据并写到 Redis(单一热点数据、高并发、数据失效)

  • 解决方案:
    • 设置随即过期时间:让缓存数据慢慢过期
    • 缓存预热:提前把热门数据存入 Redis,设置一个较大的过期时间
    • 锁机制:当发现缓存失效时,需要先获取锁,成功后才执行数据库查询和写数据到缓存的操作,失败则说明当前有其他线程在对数据库进行操作,休眠一段时间再重试

缓存雪崩

分两种情况

Redis 中大量数据同时失效,高并发场景下,大量的请求直接去到数据库,造成数据库压力激增,严重的话会搞挂数据库

  • 解决方案:
    • 设置随即过期时间,避免大量数据同时过期
    • 对非核心业务的接口进行限流,避免过多请求直接被去到数据库
    • 后台开启时任务,在缓存快要失效时及时更新缓存(具体问题具体分析)

Redis 挂了,也会使请求全部去到数据库

  • 解决方案:
    • 将热点数据均匀分布在不同的节点上
    • 服务熔断和限流
    • 构建高可用集群

缓存击穿和缓存雪崩的最大区别就是单一热点数据失效,还是大量数据同时失效

总之把握前中后的原则去进行系统设计和考虑

故障之前,尽可能集群部署,放置单点故障
发生了故障,对服务进行限流、降级操作,避免搞挂 MySQL
修复了故障后,通过 Redis 持久化机制 RDB 和 AOF,恢复缓存数据

Redis 持久化

  • RDB(Redis Database Backup file):Redis 快照文件,里面是一些二进制的数据,通过 save、bgsave 等命令进行保存,宕机恢复起来较快,文件体积也小,有压缩
  • AOF(Append Only File):对 Redis 操作的每一条写命令都会记录起来,是纯文本,人可以看,故障恢复时,重新跑记录的命令,因为记录每一条命令,所以体积会较大

Redis 支持同时开启 RDB 和 AOF,在这种情况下,会优先利用 AOF 文件来恢复数据,因为 AOF 文件保存了每一条记录,相对来说数据完整性高一点

Redis 主从

单节点 Redis 的并发能力有上限,搭建主从集群可以提高并发能力,实现读写分离
只有 master 节点可以执行写操作,slave 节点只能执行读操作

主从同步

主从同步分为全量同步和增量同步

  • 全量同步
    当 slave 第一次连接到 master 时,会执行一次全量同步,这个时候 master 会将内存中的数据生成快照 RDB,并发送给 slave,后续通过发送保存在 repl_backlog 的命令给 slave 完成增量同步
    怎么知道是不是第一次连接?
    master 和 slave 都有一个 replication id,如果不一致,则是第一次同步,那么 slave 就会修改自己的 id 为 master 的 id
    如果 slave 断开太久,导致 offset 差太多,也要全量同步,offset 可以简单理解为 slave 和 master 的数据相差的量,详细的话记不清了,有个圆环什么的,如果在圆上走了一圈数据还没同步过去,那圆上的数据就会被覆盖,为了保证数据的一致性,就要全量同步
  • 增量同步
    slave 把 offset 发给 master,master 去 repl_backlog 里查看,并发送 offset 之后的命令给 slave

Redis Sentinel 哨兵机制

Sentinel 是 Redis 的高可用解决方案,用来对 Redis 主从节点进行监控,发生故障时通过投票机制选举出新的 master,并将所有 slave 连接到新的 master 上

哨兵也是一个 Redis 服务器,运行的是 redis-sentinel 命令,而普通 redis 服务运行的则是 redis-server 命令

哨兵不提供数据服务,通常配置成单数,可以运行多个实例组成一个分布式系统

对于一些起监控作用的中间件,完成的事情无非就是下面几个:

  • 监控:确保主从节点正常运行
  • 通知:出现问题时发出通知
  • 故障转移:选举新 master,将其他 slave 连接到新的 master 上