做一个redis知识点的回忆和总结,大部分基于javaguide,比较简略,详细可见链接: javaguide
什么是redis
redis是一个数据库,与传统数据库不同的是,redis的数据是存在内存中的。所以读写速度非常快,经常被拿来做缓存使用。除此之外也会拿来做分布式锁和消息队列使用。
redis提供了多种数据类型来支持不同业务场景下的使用,还具有事务、持久化、Lua脚本和集群等特性。
为什么要用redis/缓存
高性能:
如果用户高频地直接访问数据库的话,经过数据库的查询、磁盘IO等过程,十分消耗时间。因此,如果用户高频访问不经常变化的数据,可以将其放入缓存中,提高性能。但是会存在数据库和缓存一致性的问题(旁路缓存模式)。
高并发:
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
因此可以考虑将部分数据转移到缓存中,用户的一部分直接到缓存而不必到数据库中,提高了并发。
redis除了做缓存还能做什么?
- 分布式锁:redission
- 限流:redis+lua
- 消息队列:redis5.0之后增加stream类型的数据结构适合来做消息队列。类似kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
- 复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 bitmap 统计活跃用户、通过 sorted set 维护排行榜。
redis常见的数据结构
可以通过 redis在线环境来测试
string:
- 介绍:最简单的key/value类型
- 常用命令:
set,get,strlen,exists,decr,incr,setex
- 应用场景:需要基数的场景,如用户访问次数、热点文章的点赞转发数量等
list:
- 介绍:链表,易于插入和删除元素,但随机访问困难。redis的list是一个双向链表,可以支持反向查找和遍历。
- 常用命令:
rpush,lpop,lpush,rpop,lrange,llen
- 应用场景:发布与订阅或者说消息队列、慢查询等
hash:
- 介绍:hash是一个string类型的field和value的映射表,特别适合用于存储对象
- 常用命令:
hset,hmset,hexists,hget,hgetall,hkeys,hvals
- 应用场景:对象数据的存储,如用户信息,商品信息等
set:
- 介绍:set是一种无序无重复的集合,基于set轻易实现交集、并集、差集的操作
- 常用命令:
sadd,spop,smembers,sismember,scard,sinterstore,sunion
- 应用场景:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景,如共同关注、共同粉丝、共同喜好等功能
sorted set:
- 介绍:相比于set,sort set增加了一个权重参数score,使集合中元素可以按score排列,也可以按score的范围来获取元素
- 常用命令:
zadd,zcard,zscore,zrange,zrevrange,zrem
- 应用场景: 需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息等
bitmap:
- 介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间。
- 常用命令:
setbit,getbit,bitcount,bitop
- 应用场景: 适合需要保存状态信息(比如是否签到、是否登录…)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)
- 扩展场景1:用户行为分析,很多网站为了分析你的喜好,需要研究你点赞过的内容
- 扩展场景2:统计活跃用户,使用时间作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1
redis单线程模型详解
redis基于reactor模式来设计开发了一套高效的事件处理模型,对应的是redis中的文件事件处理器。
单线程如何监听大量客户端连接:
通过IO多路复用程序来监听来自客户端的大量连接(socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。
好处是I/O多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗,单线程也不会有多线程的并发和锁的问题。
另外, Redis 服务器是一个事件驱动程序,服务器需要处理两类事件:1. 文件事件; 2. 时间事件。
时间事件不需要多花时间了解,我们接触最多的还是 文件事件(客户端进行读取写入等操作,涉及一系列网络通信)。
文件事件处理器(file event handler)主要是包含 4 个部分:
- 多个 socket(客户端连接)
- IO 多路复用程序(支持多个客户端连接的关键)
- 文件事件分派器(将 socket 关联到相应的事件处理器)
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
https://javaguide.cn/assets/redis%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86%E5%99%A8.66ac2f3d.png
redis中的多线程
4.0后引入了对多线程的支持,但主要是针对一些大键值的删除操作命令,使用这些命令会使用主处理之外的其他线程来异步处理。总体来说,6.0之前还是单线程,为什么不用多线程的原因:
- 单线程编程容易并且更容易维护
- redis的性能不在cpu,而在内存和网络
- 多线程会存在死锁、上下文切换等问题,甚至会影响性能
6.0后引入了多线程
redis6.0引入多线程主要是为了提高网络IO读写性能,但也只是在网络数据这类耗时的操作上使用了,执行命令仍然是单线程顺序执行,因此无需担心线程安全问题。
redis中给缓存设置过期时间的作用
原因:内存有限,需要释放
注:除了string类型有自己的设置过期时间的命令setex,其他都需要依靠expire来设置过期时间。另外,persist命令可以移除一个键的过期时间。
过期时间还有什么用处
很多业务场景需要某个数据只在某个时间内存在,比如短信验证码、用户token等
如何判断是否过期
redis通过一个过期字典(hash表),来保存数据过期时间。过期字典的键指向数据库中的键,相对应的值是一个long long的整数,即过期时间(毫秒精度的UNIX时间戳)
过期数据的删除策略
- 惰性删除:只会在取出key的时候才会对数据进行过期检查。对CPU好,但可能造成太多过期key没有被删除
- 定期删除:每隔一段时间抽取一批key执行过期key操作,redis底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响
redis采用的策略:惰性+定期,但是也会存在漏掉很多key在内存中的问题,使用redis内存淘汰机制来解决。
redis内存淘汰机制
相关问题:如何保证redis中的数据都是热点数据(经常访问的数据)
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
redis持久化机制
很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)。
快照(RDB)
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
只追加文件(AOF)
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf
中,然后再根据 appendfsync
配置来决定何时将其同步到硬盘中的 AOF 文件。(感觉有点类似于mysql中的log文件)
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec
选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
redis bigkey
什么是bigkey
简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
危害
除了会消耗更多的内存空间,bigkey 对性能也会有比较大的影响。
如何发现
- 使用redis自带的
--bigkeys
来查找 - 分析RDB文件
redis事务
redis可以通过 muti,exec,discard,watch
等命令来实现事务
- 调用
muti
后可以输入多个命令,redis将它们放到队列(FIFO先进先出的顺序执行),调用exec
后将执行所有命令 - 调用
discard
来取消一个事务,会清空事物队列中的所有命令 watch
监听指定的键,当调用exec
时,某个被watch
的键被修改,整个事务都不会执行
与平时的关系型数据库不同处
redis不支持roll back,因此不支持原子性
和一致性
redis可以做消息队列吗
redis5.0新的数据结构类型steam可以用来做消息队列,stream支持:
- 发布/订阅
- 按消费者组进行消费
- 消息持久化(RDB和AOF)
但是不建议,和专业的相比还是有缺陷,比如消息丢失和堆积的问题不好解决
缓存穿透
什么是缓存穿透
大量请求的key不存在于缓存中,直接请求到了数据库上
解决方法
缓存无效key:如果一条数据缓存和数据库都查不到,就将其放入缓存中,并设置过期时间,但是只能适用于key变化不频繁的情况。如果黑客使用大量不同的key,就会导致redis中缓存大量无效的key。很明显不能解决问题,如果非要用此方式来解决,尽量将无效key的过期时间设置短一点。
布隆过滤器:可以非常方便地判断一个数据是否存在于含量数据中。具体来说就是,把所有可能请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误给客户端,存在的话继续走流程。
需要注意的是,布隆过滤器会存在误判的情况:布隆过滤器说某个元素存在,小概率会误判;布隆过滤器说不在,那么这个元素一定不在。
为什么布隆过滤器会存在误判
当一个元素加入布隆过滤器时,会进行哪些操作:
- 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值
- 根据得到的哈希值,在位数组中把对应下标的值置为1
当判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
- 对给定元素进行相同的哈希计算
- 得到值之后判断位数组中每个元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中,如果存在一个值不为1,说明该元素不在布隆过滤器中
一定会出现的情况:不同的字符串可能哈希出来的位置相同(可以适当增加位数组大小或者调整哈希函数来降低概率)
缓存雪崩
缓存在同一时间大面积失效,后面的请求都直接落到数据库上,造成数据库短时间内承受大量请求。举个例子:系统的缓存模块出了问题,造成系统的所有访问都要走数据库。
还有一种场景就是:有一些被大量访问的数据(热点缓存)在某一时刻大量失效,导致对应的请求直接落到了数据库上。举个例子,秒杀活动开始前12小时,统一把一批商品存到redis中,设置过期时间12小时。秒杀开始时缓存失效,导致相应请求直接落在了数据库上。
解决办法
针对redis服务不可用的情况:
- 采用redis集群,避免单机出现问题导致整个缓存服务无法使用
- 限流,避免同时处理大量请求
针对热点缓存失效的情况:
- 设置不同的实现时间比如随即设置缓存的失效时间
- 缓存永不失效
如何保证缓存和数据库一致性
旁路缓存模式:
遇到写请求的过程:更新DB,然后删除cache
如果碰到更新数据库成功,删除缓存失败的情况,两个解决方案:
- 缓存失效时间变短(治标不治本):使缓存的过期时间变短,这样的话缓存就会从数据库中加载数据。
- 增加cache更新重试机制(常用):如果cache服务当前不可用导致缓存删除失败的话,就隔一段时间重试,重试次数自己定。如果多次重试还是失败,可以把更新失败的key存入队列中,等缓存服务可能后,再将缓存中的key删除即可