Redis基础
Redis基础
Redis为什么快?
怎么保证缓存和数据库的数据一致性?
当数据库中的数据修改时,
- 首先是更新缓存的方案
无论是先更新数据库、再更新缓存还是先更新缓存、再更新数据库;都可能会在两个更新请求的时候出现并发问题,比如请求A先进行修改数据库中的数据为1,另外一个请求B进行了数据库和缓存中的修改为2,之后请求A再修改缓存中的数据为1,就会导致缓存和数据库数据不一致的情况发生。然后删除缓存是一种懒加载的思想,因为每次更改数据之后,不一定立马就有人来用。 若更新的次数远大于读取的次数,此时会频繁更新缓存,会非常浪费性能资源。
- 更新数据库,删除缓存;之后读取数据时,发现缓存中没了数据之后,再从数据库中读取数据,更新到缓存中。【旁路缓存策略】
能不能先删除缓存,再更新数据库?
不能,因为这样还是会出现,请求A先删除缓存,请求B从数据库中读出旧数据并写到缓存中,A再往数据库中写入新数据,就会导致数据不一致。
但是先更新数据库,再删除缓存一般不会发生这种情况【请求A从数据库读旧数据,请求B更新数据库并删除缓存,请求A往缓存中写入旧数据】,因为缓存的写入速度要远远快于数据库的写入速度,所以请求A更新缓存的旧数据一般早于请求B的删除缓存的。
如果第二步删除缓存失败,可以给缓存加上一个过期时间,在这期间缓存不一致可以忍受,那也可以之后从数据库中读出正确数据。
怎么保证删除缓存操作成功执行?
如果删除操作没有成功执行,读到的会是缓存中的旧值。
- 消息队列
我们可以引入消息队列,将删除缓存要操作的数据加入到消息队列。
- 由消费者来操作数据。如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,进行重试。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
这个方案缺点是,对代码入侵性比较强,因为需要改造原本业务的代码。
- 订阅MySQL的binlog再操作缓存
旁路缓存策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在binlog里。 于是我们就可以通过订阅binlog日志,拿到具体要操作的数据,然后再执行缓存删除。
阿里巴巴开源的Canal中间件就是基于这个实现的。Canal模拟MySQL主从复制的交互协议,把自己伪装成一个MySQL的从节点,向MySQL主节点发送dump请求,MySQL收到请求后,就会开始推送Binlog给Canal, Canal 解析Binlog字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
说出一些优化Redis性能的措施
使用批量操作减少网络传输
批量操作:将多个命令一次性发送给 Redis 服务器执行的方式
一个 Redis 命令的执行可以简化为4 步:发送命令;命令排队;命令执行;返回结果
其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time (RTT,往返时间) ,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在read()
和write()
系统调用),批量操作还可以减少 socket I/O 成本。
1)原生批量操作命令
Redis 中有一些原生支持批量操作的命令,比如:
MGET
(获取一个或多个指定 key 的值)、MSET
(设置一个或多个指定 key 的值)、HMGET
(获取指定哈希表中一个或者多个指定字段的值)、HMSET
(同时将一个或多个 field-value 对设置到指定哈希表中)、SADD
(向指定集合添加一个或多个元素)
不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 MGET
无法保证所有的 key 都在同一个 hash slot(哈希槽)上,MGET
可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
- 找到 key 对应的所有 hash slot;
- 分别向对应的 Redis 节点发起
MGET
请求获取数据; - 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。
如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。
Redis Cluster 并没有使用一致性哈希,采用的是 哈希槽分区 ,每一个键值对都属于一个 hash slot(哈希槽) 。当客户端发送命令请求的时候,需要先根据 key 通过上面的计算公示找到的对应的哈希槽,然后再查询哈希槽和节点的映射关系,即可找到目标 Redis 节点。
2)pipeline
对于不支持批量操作的命令,我们可以利用 pipeline(流水线) 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 元素个数(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。
与MGET
、MSET
等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 hash slot(哈希槽)上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。
原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意:
- 原生批量操作命令是原子操作,pipeline 是非原子操作。
- pipeline 可以打包不同的命令,原生批量操作命令不可以。
- 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。
顺带补充一下 pipeline 和 Redis 事务的对比:
- 事务是原子操作,pipeline 是非原子操作。两个不同的事务不会同时运行,而 pipeline 可以同时以交错方式执行。
- Redis 事务中每个命令都需要发送到服务端,而 Pipeline 只需要发送一次,请求次数更少。
事务可以看作是一个原子操作,但其实并不满足原子性。当我们提到 Redis 中的原子操作时,主要指的是这个操作(比如事务、Lua 脚本)不会被其他操作(比如其他事务、Lua 脚本)打扰,并不能完全保证这个操作中的所有写命令要么都执行要么都不执行。这主要也是因为 Redis 是不支持回滚操作。
另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 Lua 脚本 。
3)Lua 脚本
Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 原子操作 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。
并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。
不过, Lua 脚本依然存在下面这些缺陷:Lua脚本没法回滚,
- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上。
避免出现缓存雪崩(大量key集中过期)问题
- 给key设置过期时间
- 为了避免需要删除大量过期key占用CPU资源的情况,可以开启惰性删除并启动异步删除,避免阻塞主线程。
减少Redis中的BigKey
bigkey简介
bigkey可以是key本身数据量过大,key中的成员数多等等。
bigkey 通常是由于下面这些原因产生的:
- 程序设计不当,比如直接使用 String 类型存储较大的文件对应的二进制数据。
- 对于业务的数据规模考虑不周到,比如使用集合类型的时候没有考虑到数据量的快速增长。
- 未及时清理垃圾数据,比如哈希中冗余了大量的无用键值对。
bigkey 除了会消耗更多的内存空间和带宽,大 key 还会造成阻塞问题。具体来说,主要体现在下面三个方面:
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
所以大 key 带来的潜在问题是非常多的,我们应该尽量避免 Redis 存在 bigkey。
怎么发现并处理BigKey?
使用Redis自带的—bigkeys命令来查找。这个命令会扫描Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 String 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
bigkey常见优化方法,可以从以下几方面入手:
开发方面:
- 分割 bigkey:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。
- 采用合适的数据结构:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。
业务层面
可以根据实际情况,调整存储策略,只存一些必要的数据
对Redis中的热key进行相应处理
如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
热key的危害:处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。因此,hotkey 很可能成为系统性能的瓶颈点,需要对其进行优化,以保证系统的高可用性和稳定性。
如何发现热key?
- 根据业务场景进行分析,比如秒杀活动,抢票活动等。
- 使用—hotkey指令监控 优点:因为这个命令是Redis自带的,使用起来简单快捷;需要扫描整个keyspace, 如果Redis中的key数紕较多的话,可能导致执行时间非常长,并且实时性不好。
- 使用monitor指令,monitor指令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,但是由于该命令对 Redis 性能的影响比较大,因此禁止长时间开启
MONITOR
(生产环境中建议谨慎使用该命令)。在发生紧急情况时,我们可以选择在合适的时机短暂执行MONITOR
命令并将输出重定向至文件,在关闭MONITOR
命令后,再对文件进行分析即可找出这段时间中的 hotkey。
如何解决热key问题?
- 多级缓存:可以将 hotkey 再存放一份到 JVM 本地内存中(可以用 Caffeine),hotkey 采用二级缓存的方式进行处理
- 使用 Redis Cluster【redis集群】:将热点数据分散存储在多个 Redis 节点上。