Redis 就是一个使用 C 语言开发的数据库, Redis 的数据是存在内存中的。Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列。
Redis 除了做缓存,还能做什么?
分布式锁 : 通过 Redis 来做分布式锁是一种比较常见的方式。
限流 :一般是通过 Redis + Lua 脚本的方式来实现限流。
消息队列 :Redis 自带的 list 数据结构可以作为一个简单的队列使用。
复杂业务场景 :通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。
Redis 常见数据结构以及使用场景分析
string
1、介绍:string 数据结构是简单的 key-value 类型,构建了一种简单动态字符串(SDS),Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)
2、常用命令: set,get,strlen,exists,decr,incr,setex 等等
3、应用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等
list
1、介绍 :list 即是双向链表,可以支持反向查找和遍历
2、常用命令: rpush,lpop,lpush,rpop,lrange,llen 等
3、应用场景: 发布与订阅或者说消息队列、慢查询。
hash
1、介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
2、常用命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等
3、应用场景: 系统中对象数据的存储。
set
1、介绍 : set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。
2、常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
3、应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sorted set
1、介绍: 和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。
2、常用命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
3、应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息。
bitmap
1、介绍 : bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。
2、常用命令:setbit 、getbit 、bitcount、bitop
3、应用场景:适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)。
Redis 单线程模型详解
Redis 基于 Reactor 模式来设计开发了自己的文件事件处理器。由于文件事件处理器是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。
Redis是单线程,那怎么监听大量的客户端连接呢?
Redis 通过IO 多路复用程序来监听来自客户端的大量连接,它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:1. 文件事件; 2. 时间事件。
文件事件处理器
多个 socket(客户端连接)
IO 多路复用程序(支持多个客户端连接的关键)
文件事件分派器(将 socket 关联到相应的事件处理器)
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
Redis6.0 之前 为什么不使用多线程?
1、单线程编程容易并且更容易维护;
2、Redis 的性能瓶颈不在 CPU ,主要在内存和网络;
3、多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
Redis6.0 之后为何引入了多线程?
Redis6.0 引入多线程主要是为了提高网络 IO 读写性能
Redis 给缓存数据设置过期时间有啥用?
1、有助于缓解内存的消耗
2、根据业务场景需要设置过期时间
Redis 是如何判断数据是否过期的呢?
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。
过期的数据的删除策略了解么?
1、惰性删除 :只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
2、定期删除 : 每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
Redis 采用的是定期删除+惰性/懒汉式删除 。
Redis 内存淘汰机制
1、volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3、volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
4、allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6、no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
7、volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
8、allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)
Redis 支持持久化,而且支持两种不同的持久化操作,一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。
快照(snapshotting)持久化(RDB)
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
AOF(append-only file)持久化
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf 中,然后再根据 appendfsync 配置来决定何时将其同步到硬盘中的 AOF 文件
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
Redis bigkey
简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个。
bigkey 有什么危害?
会消耗更多的内存空间,对性能也会有比较大的影响。
Redis 事务
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)。
缓存穿透
大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层
解决办法?
1)缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。
2)布隆过滤器
把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。
布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
当一个元素加入布隆过滤器中的时候,会进行哪些操作:
1、使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
2、根据得到的哈希值,在位数组中把对应下标的值置为 1
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
1、对给定元素再次进行相同的哈希计算;
2、得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)
缓存击穿
大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。
缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key
解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。
缓存雪崩
场景一 Redis服务不可用:缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。
举个例子:系统的缓存模块出了问题比如宕机导致不可用。造成系统的所有访问,都要走数据库。
有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。
场景二 热点缓存失效:有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。
针对 Redis 服务不可用的情况:
1、采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
2、限流,避免同时处理大量的请求。
针对热点缓存失效的情况:
1、设置不同的失效时间比如随机设置缓存的失效时间。
2、缓存永不失效
如何保证缓存和数据库数据的一致性?
更新数据库,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话:
增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。