本文目录:
Redis 是什么?
Redis 优缺点?
Redis 为什么这么快?
Redis 为何选择单线程
Redis6.0 为何引入多线程?
Redis 应用场景有哪些?
Memcached 和 Redis 的区别?
Redis 数据类型有哪些?
keys 命令存在的问题?
SortedSet 和 List 异同点?
Redis 事务
持久化机制
RDB 方式
AOF 方式
RDB 和 AOF 如何选择?
Redis 常见的部署方式有哪些?
主从复制
哨兵 Sentinel
Redis cluster
哈希分区算法有哪些?
过期键的删除策略?
内存淘汰策略有哪些?
如何保证缓存与数据库双写时的数据一致性?
缓存穿透
缓存雪崩
缓存击穿
Redis 怎么实现消息队列?
pipeline 的作用?
LUA 脚本
什么是 RedLock?
Redis 是什么?
Redis(Remote Dictionary Server)是一个使用 C 语言编写的, 高性能非关系型的键值对数据库. 与传统数据库不同的是, Redis 的数据是存在内存中的, 所以读写速度非常快, 被广泛应用于缓存方向. Redis 可以将数据写入磁盘中, 保证了数据的安全不丢失, 而且 Redis 的操作是原子性的.
Redis 优缺点?
优点:
基于内存操作, 内存读写速度快.
Redis 是单线程的, 避免线程切换开销及多线程的竞争问题. 单线程是指网络请求使用一个线程来处理, 即一个线程处理所有网络请求, Redis 运行时不止有一个线程, 比如数据持久化的过程会另起线程.
支持多种数据类型, 包括 String,Hash,List,Set,ZSet 等.
支持持久化. Redis 支持 RDB 和 AOF 两种持久化机制, 持久化功能可以有效地避免数据丢失问题.
支持事务. Redis 的所有操作都是原子性的, 同时 Redis 还支持对几个操作合并后的原子性执行.
支持主从复制. 主节点会自动将数据同步到从节点, 可以进行读写分离.
缺点:
对结构化查询的支持比较差.
数据库容量受到物理内存的限制, 不适合用作海量数据的高性能读写, 因此 Redis 适合的场景主要局限在较小数据量的操作.
Redis 较难支持在线扩容, 在集群容量达到上限时在线扩容会变得很复杂.
Redis 为什么这么快?
基于内存: Redis 是使用内存存储, 没有磁盘 IO 上的开销. 数据存在内存中, 读写速度快.
单线程实现( Redis 6.0 以前):Redis 使用单个线程处理请求, 避免了多个线程之间线程切换和锁资源争用的开销.
IO 多路复用模型: Redis 采用 IO 多路复用技术. Redis 使用单线程来轮询描述符, 将数据库的操作都转换成了事件, 不在网络 I/O 上浪费过多的时间.
高效的数据结构: Redis 每种数据类型底层都做了优化, 目的就是为了追求更快的速度.
Redis 为何选择单线程
避免过多的上下文切换开销. 程序始终运行在进程中单个线程内, 没有多线程切换的场景.
避免同步机制的开销: 如果 Redis 选择多线程模型, 需要考虑数据同步的问题, 则必然会引入某些同步机制, 会导致在操作数据过程中带来更多的开销, 增加程序复杂度的同时还会降低性能.
实现简单, 方便维护: 如果 Redis 使用多线程模式, 那么所有的底层数据结构的设计都必须考虑线程安全问题, 那么 Redis 的实现将会变得更加复杂.
Redis6.0 为何引入多线程?
Redis 支持多线程主要有两个原因:
可以充分利用服务器 CPU 资源, 单线程模型的主线程只能利用一个 CPU;
多线程任务可以分摊 Redis 同步 IO 读写的负荷.
Redis 应用场景有哪些?
缓存热点数据, 缓解数据库的压力.
利用 Redis 原子性的自增操作, 可以实现计数器的功能, 比如统计用户点赞数, 用户访问数等.
简单的消息队列, 可以使用 Redis 自身的发布 / 订阅模式或者 List 来实现简单的消息队列, 实现异步操作.
限速器, 可用于限制某个用户访问某个接口的频率, 比如秒杀场景用于防止用户快速点击带来不必要的压力.
好友关系, 利用集合的一些命令, 比如交集, 并集, 差集等, 实现共同好友, 共同爱好之类的功能.
Memcached 和 Redis 的区别?
Redis 只使用单核, 而 Memcached 可以使用多核.
Memcached 数据结构单一, 仅用来缓存数据, 而 Redis 支持多种数据类型.
Memcached 不支持数据持久化, 重启后数据会消失. Redis 支持数据持久化.
Redis 提供主从同步机制和 cluster 集群部署能力, 能够提供高可用服务. Memcached 没有提供原生的集群模式, 需要依靠客户端实现往集群中分片写入数据.
Redis 的速度比 Memcached 快很多.
Redis 使用单线程的多路 IO 复用模型, Memcached 使用多线程的非阻塞 IO 模型.
Redis 数据类型有哪些?
基本数据类型:
1,String: 最常用的一种数据类型, String 类型的值可以是字符串, 数字或者二进制, 但值最大不能超过 512MB.
2,Hash:Hash 是一个键值对集合.
3,Set: 无序去重的集合. Set 提供了交集, 并集等方法, 对于实现共同好友, 共同关注等功能特别方便.
4,List: 有序可重复的集合, 底层是依赖双向链表实现的.
5,SortedSet: 有序 Set. 内部维护了一个 score 的参数来实现. 适用于排行榜和带权重的消息队列等场景.
特殊的数据类型:
1,Bitmap: 位图, 可以认为是一个以位为单位数组, 数组中的每个单元只能存 0 或者 1, 数组的下标在 Bitmap 中叫做偏移量. Bitmap 的长度与集合中元素个数无关, 而是与基数的上限有关.
2,Hyperloglog.HyperLogLog 是用来做基数统计的算法, 其优点是, 在输入元素的数量或者体积非常非常大时, 计算基数所需的空间总是固定的, 并且是很小的. 典型的使用场景是统计独立访客.
3,Geospatial : 主要用于存储地理位置信息, 并对存储的信息进行操作, 适用场景如定位, 附近的人等.
keys 命令存在的问题?
Redis 的单线程的. keys 指令会导致线程阻塞一段时间, 直到执行完毕, 服务才能恢复. scan 采用渐进式遍历的方式来解决 keys 命令可能带来的阻塞问题, 每次 scan 命令的时间复杂度是 O(1), 但是要真正实现 keys 的功能, 需要执行多次 scan.
scan 的缺点: 在 scan 的过程中如果有键的变化(增加, 删除, 修改), 遍历过程可能会有以下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说 scan 并不能保证完整的遍历出来所有的键.
SortedSet 和 List 异同点?
相同点:
都是有序的;
都可以获得某个范围内的元素.
不同点:
列表基于链表实现, 获取两端元素速度快, 访问中间元素速度慢;
有序集合基于散列表和跳跃表实现, 访问中间元素时间复杂度是 OlogN;
列表不能简单的调整某个元素的位置, 有序列表可以(更改元素的分数);
有序集合更耗内存.
Redis 事务
事务的原理是将一个事务范围内的若干命令发送给 Redis, 然后再让 Redis 依次执行这些命令.
事务的生命周期:
使用 MULTI 开启一个事务
在开启事务的时候, 每次操作的命令将会被插入到一个队列中, 同时这个命令并不会被真的执行
EXEC 命令进行提交事务
一个事务范围内某个命令出错不会影响其他命令的执行, 不保证原子性:
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set a 1
- QUEUED
- 127.0.0.1:6379> set b 1 2
- QUEUED
- 127.0.0.1:6379> set c 3
- QUEUED
- 127.0.0.1:6379> exec
- 1) OK
- 2) (error) ERR syntax error
- 3) OK
WATCH 命令
WATCH 命令可以监控一个或多个键, 一旦其中有一个键被修改, 之后的事务就不会执行(类似于乐观锁). 执行 EXEC 命令之后, 就会自动取消监控.
- 127.0.0.1:6379> watch name
- OK
- 127.0.0.1:6379> set name 1
- OK
- 127.0.0.1:6379> multi
- OK
- 127.0.0.1:6379> set name 2
- QUEUED
- 127.0.0.1:6379> set gender 1
- QUEUED
- 127.0.0.1:6379> exec
- (nil)
- 127.0.0.1:6379> get gender
- (nil)
比如上面的代码中:
watch name 开启了对 name 这个 key 的监控
修改 name 的值
开启事务 a
在事务 a 中设置了 name 和 gender 的值
使用 EXEC 命令进提交事务
使用命令 get gender 发现不存在, 即事务 a 没有执行
使用 UNWATCH 可以取消 WATCH 命令对 key 的监控, 所有监控锁将会被取消.
持久化机制
持久化就是把内存的数据写到磁盘中, 防止服务宕机导致内存数据丢失.
Redis 支持两种方式的持久化, 一种是 RDB 的方式, 一种是 AOF 的方式. 前者会根据指定的规则定时将内存中的数据存储在硬盘上, 而后者在每次执行完命令后将命令记录下来. 一般将两者结合使用.
RDB 方式
RDB 是 Redis 默认的持久化方案. RDB 持久化时会将内存中的数据写入到磁盘中, 在指定目录下生成一个 dump.rdb 文件. Redis 重启会加载 dump.rdb 文件恢复数据.
bgsave 是主流的触发 RDB 持久化的方式, 执行过程如下:
执行 BGSAVE 命令
Redis 父进程判断当前是否存在正在执行的子进程, 如果存在, BGSAVE 命令直接返回.
父进程执行 fork 操作创建子进程, fork 操作过程中父进程会阻塞.
父进程 fork 完成后, 父进程继续接收并处理客户端的请求, 而子进程开始将内存中的数据写进硬盘的临时文件;
当子进程写完所有数据后会用该临时文件替换旧的 RDB 文件.
Redis 启动时会读取 RDB 快照文件, 将数据从硬盘载入内存. 通过 RDB 方式的持久化, 一旦 Redis 异常退出, 就会丢失最近一次持久化以后更改的数据.
触发 RDB 持久化的方式:
手动触发: 用户执行 SAVE 或 BGSAVE 命令. SAVE 命令执行快照的过程会阻塞所有客户端的请求, 应避免在生产环境使用此命令. BGSAVE 命令可以在后台异步进行快照操作, 快照的同时服务器还可以继续响应客户端的请求, 因此需要手动执行快照时推荐使用 BGSAVE 命令.
被动触发:
根据配置规则进行自动快照, 如 SAVE 100 10,100 秒内至少有 10 个键被修改则进行快照.
如果从节点执行全量复制操作, 主节点会自动执行 BGSAVE 生成 RDB 文件并发送给从节点.
默认情况下执行 shutdown 命令时, 如果没有开启 AOF 持久化功能则自动执行. BGSAVE..
优点:
Redis 加载 RDB 恢复数据远远快于 AOF 的方式.
使用单独子进程来进行持久化, 主进程不会进行任何 IO 操作, 保证了 Redis 的高性能.
缺点:
RDB 方式数据无法做到实时持久化. 因为 BGSAVE 每次运行都要执行 fork 操作创建子进程, 属于重量级操作, 频繁执行成本比较高.
RDB 文件使用特定二进制格式保存, Redis 版本升级过程中有多个格式的 RDB 版本, 存在老版本 Redis 无法兼容新版 RDB 格式的问题.
AOF 方式
AOF(append only file)持久化: 以独立日志的方式记录每次写命令, Redis 重启时会重新执行 AOF 文件中的命令达到恢复数据的目的. AOF 的主要作用是解决了数据持久化的实时性, AOF 是 Redis 持久化的主流方式.
默认情况下 Redis 没有开启 AOF 方式的持久化, 可以通过 appendonly 参数启用: appendonly yes. 开启 AOF 方式持久化后每执行一条写命令, Redis 就会将该命令写进 aof_buf 缓冲区, AOF 缓冲区根据对应的策略向硬盘做同步操作.
默认情况下系统每 30 秒会执行一次同步操作. 为了防止缓冲区数据丢失, 可以在 Redis 写入 AOF 文件后主动要求系统将缓冲区数据同步到硬盘上. 可以通过 appendfsync 参数设置同步的时机.
- appendfsync always // 每次写入 aof 文件都会执行同步, 最安全最慢, 不建议配置
- appendfsync everysec // 既保证性能也保证安全, 建议配置
- appendfsync no // 由操作系统决定何时进行同步操作
接下来看一下 AOF 持久化执行流程:
所有的写入命令会追加到 AOP 缓冲区中.
AOF 缓冲区根据对应的策略向硬盘同步.
随着 AOF 文件越来越大, 需要定期对 AOF 文件进行重写, 达到压缩文件体积的目的. AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程.
当 Redis 服务器重启时, 可以加载 AOF 文件进行数据恢复.
优点:
AOF 可以更好的保护数据不丢失, 可以配置 AOF 每秒执行一次 fsync 操作, 如果 Redis 进程挂掉, 最多丢失 1 秒的数据.
AOF 以 append-only 的模式写入, 所以没有磁盘寻址的开销, 写入性能非常高.
缺点:
对于同一份文件 AOF 文件比 RDB 数据快照要大.
数据恢复比较慢.
RDB 和 AOF 如何选择?
通常来说, 应该同时使用两种持久化方案, 以保证数据安全.
如果数据不敏感, 且可以从其他地方重新生成, 可以关闭持久化.
如果数据比较重要, 且能够承受几分钟的数据丢失, 比如缓存等, 只需要使用 RDB 即可.
如果是用做内存数据, 要使用 Redis 的持久化, 建议是 RDB 和 AOF 都开启.
如果只用 AOF, 优先使用 everysec 的配置选择, 因为它在可靠性和性能之间取了一个平衡.
当 RDB 与 AOF 两种方式都开启时, Redis 会优先使用 AOF 恢复数据, 因为 AOF 保存的文件比 RDB 文件更完整.
Redis 常见的部署方式有哪些?
Redis 的几种常见使用方式包括:
单机版
Redis 主从
- Redis Sentinel(哨兵)
- Redis Cluster
使用场景:
单机版: 很少使用. 存在的问题: 1, 内存容量有限 2, 处理能力有限 3, 无法高可用.
主从模式: master 节点挂掉后, 需要手动指定新的 master, 可用性不高, 基本不用.
哨兵模式: master 节点挂掉后, 哨兵进程会主动选举新的 master, 可用性高, 但是每个节点存储的数据是一样的, 浪费内存空间. 数据量不是很多, 集群规模不是很大, 需要自动容错容灾的时候使用.
Redis cluster: 主要是针对海量数据 + 高并发 + 高可用的场景, 如果是海量数据, 如果你的数据量很大, 那么建议就用 Redis cluster, 所有主节点的容量总和就是 Redis cluster 可缓存的数据容量.
主从复制
Redis 的复制功能是支持多个数据库之间的数据同步. 主数据库可以进行读写操作, 当主数据库的数据发生变化时会自动将数据同步到从数据库. 从数据库一般是只读的, 它会接收主数据库同步过来的数据. 一个主数据库可以有多个从数据库, 而一个从数据库只能有一个主数据库.
- Redis-server // 启动 Redis 实例作为主数据库
- Redis-server --port 6380 --slaveof 127.0.0.1 6379 // 启动另一个实例作为从数据库
- slaveof 127.0.0.1 6379
- SLAVEOF NO ONE // 停止接收其他数据库的同步并转化为主数据库.
主从复制的原理?
当启动一个从节点时, 它会发送一个 PSYNC 命令给主节点;
如果是从节点初次连接到主节点, 那么会触发一次全量复制. 此时主节点会启动一个后台线程, 开始生成一份 RDB 快照文件;
同时还会将从客户端 client 新收到的所有写命令缓存在内存中. RDB 文件生成完毕后, 主节点会将 RDB 文件发送给从节点, 从节点会先将 RDB 文件写入本地磁盘, 然后再从本地磁盘加载到内存中;
接着主节点会将内存中缓存的写命令发送到从节点, 从节点同步这些数据;
如果从节点跟主节点之间网络出现故障, 连接断开了, 会自动重连, 连接之后主节点仅会将部分缺失的数据同步给从节点.
哨兵 Sentinel
主从复制存在不能自动故障转移, 达不到高可用的问题. 哨兵模式解决了这些问题. 通过哨兵机制可以自动切换主从节点.
客户端连接 Redis 的时候, 先连接哨兵, 哨兵会告诉客户端 Redis 主节点的地址, 然后客户端连接上 Redis 并进行后续的操作. 当主节点宕机的时候, 哨兵监测到主节点宕机, 会重新推选出某个表现良好的从节点成为新的主节点, 然后通过发布订阅模式通知其他的从服务器, 让它们切换主机.
工作原理
每个 Sentinel 以每秒钟一次的频率向它所知道的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令.
如果一个实例距离最后一次有效回复 PING 命令的时间超过指定值, 则这个实例会被 Sentine 标记为主观下线.
如果一个 Master 被标记为主观下线, 则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真正进入主观下线状态.
当有足够数量的 Sentinel(大于等于配置文件指定值)在指定的时间范围内确认 Master 的确进入了主观下线状态, 则 Master 会被标记为客观下线 . 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被解除. 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除.
哨兵节点会选举出哨兵 leader, 负责故障转移的工作.
哨兵 leader 会推选出某个表现良好的从节点成为新的主节点, 然后通知其他从节点更新主节点信息.
Redis cluster
哨兵模式解决了主从复制不能自动故障转移, 达不到高可用的问题, 但还是存在主节点的写能力, 容量受限于单机配置的问题. 而 cluster 模式实现了 Redis 的分布式存储, 每个节点存储不同的内容, 解决主节点的写能力, 容量受限于单机配置的问题.
Redis cluster 集群节点最小配置 6 个节点以上(3 主 3 从), 其中主节点提供读写操作, 从节点作为备用节点, 不提供请求, 只作为故障转移使用.
Redis cluster 采用虚拟槽分区, 所有的键根据哈希函数映射到 0~16383 个整数槽内, 每个节点负责维护一部分槽以及槽所映射的键值数据.
哈希槽是如何映射到 Redis 实例上的?
对键值对的 key 使用 crc16 算法计算一个结果
将结果对 16384 取余, 得到的值表示 key 对应的哈希槽
根据该槽信息定位到对应的实例
优点:
无中心架构, 支持动态扩容;
数据按照 slot 存储分布在多个节点, 节点间数据共享, 可动态调整数据分布;
高可用性. 部分节点不可用时, 集群仍可用. 集群模式能够实现自动故障转移(failover), 节点之间通过 gossip 协议交换状态信息, 用投票机制完成 Slave 到 Master 的角色转换.
缺点:
不支持批量操作(pipeline).
数据通过异步复制, 不保证数据的强一致性.
事务操作支持有限, 只支持多 key 在同一节点上的事务操作, 当多个 key 分布于不同的节点上时无法使用事务功能.
key 作为数据分区的最小粒度, 不能将一个很大的键值对象如 hash,list 等映射到不同的节点.
不支持多数据库空间, 单机下的 Redis 可以支持到 16 个数据库, 集群模式下只能使用 1 个数据库空间.
哈希分区算法有哪些?
节点取余分区. 使用特定的数据, 如 Redis 的键或用户 ID, 对节点数量 N 取余: hash(key)%N 计算出哈希值, 用来决定数据映射到哪一个节点上.
优点是简单性. 扩容时通常采用翻倍扩容, 避免数据映射全部被打乱导致全量迁移的情况.
一致性哈希分区. 为系统中每个节点分配一个 token, 范围一般在 0~232, 这些 token 构成一个哈希环. 数据读写执行节点查找操作时, 先根据 key 计算 hash 值, 然后顺时针找到第一个大于等于该哈希值的 token 节点.
这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点, 对其他节点无影响.
虚拟槽分区, 所有的键根据哈希函数映射到 0~16383 整数槽内, 计算公式: slot=CRC16(key)&16383. 每一个节点负责维护一部分槽以及槽所映射的键值数据. Redis Cluser 采用虚拟槽分区算法.
过期键的删除策略?
1, 被动删除. 在访问 key 时, 如果发现 key 已经过期, 那么会将 key 删除.
2, 主动删除. 定时清理 key, 每次清理会依次遍历所有 DB, 从 db 随机取出 20 个 key, 如果过期就删除, 如果其中有 5 个 key 过期, 那么就继续对这个 db 进行清理, 否则开始清理下一个 db.
3, 内存不够时清理. Redis 有最大内存的限制, 通过 maxmemory 参数可以设置最大内存, 当使用的内存超过了设置的最大内存, 就要进行内存释放, 在进行内存释放的时候, 会按照配置的淘汰策略清理内存.
内存淘汰策略有哪些?
当 Redis 的内存超过最大允许的内存之后, Redis 会触发内存淘汰策略, 删除一些不常用的数据, 以保证 Redis 服务器正常运行.
Redisv4.0 前提供 6 种数据淘汰策略:
volatile-lru:LRU(Least Recently Used), 最近使用. 利用 LRU 算法移除设置了过期时间的 key
allkeys-lru: 当内存不足以容纳新写入数据时, 从数据集中移除最近最少使用的 key
volatile-ttl: 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random: 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-random: 从数据集中任意选择数据淘汰
no-eviction: 禁止删除数据, 当内存不足以容纳新写入数据时, 新写入操作会报错
Redisv4.0 后增加以下两种:
volatile-lfu:LFU,Least Frequently Used, 最少使用, 从已设置过期时间的数据集中挑选最不经常使用的数据淘汰.
allkeys-lfu: 当内存不足以容纳新写入数据时, 从数据集中移除最不经常使用的 key.
内存淘汰策略可以通过配置文件来修改, 相应的配置项是 maxmemory-policy, 默认配置是 noeviction.
如何保证缓存与数据库双写时的数据一致性?
1, 先删除缓存再更新数据库
进行更新操作时, 先删除缓存, 然后更新数据库, 后续的请求再次读取时, 会从数据库读取后再将新数据更新到缓存.
存在的问题: 删除缓存数据之后, 更新数据库完成之前, 这个时间段内如果有新的读请求过来, 就会从数据库读取旧数据重新写到缓存中, 再次造成不一致, 并且后续读的都是旧数据.
2, 先更新数据库再删除缓存
进行更新操作时, 先更新 MySQL, 成功之后, 删除缓存, 后续读取请求时再将新数据回写缓存.
存在的问题: 更新 MySQL 和删除缓存这段时间内, 请求读取的还是缓存的旧数据, 不过等数据库更新完成, 就会恢复一致, 影响相对比较小.
3, 异步更新缓存
数据库的更新操作完成后不直接操作缓存, 而是把这个操作命令封装成消息扔到消息队列中, 然后由 Redis 自己去消费更新数据, 消息队列可以保证数据操作顺序一致性, 确保缓存系统的数据正常.
缓存穿透
缓存穿透是指查询一个不存在的数据, 由于缓存是不命中时被动写的, 如果从 DB 查不到数据则不写入缓存, 这将导致这个不存在的数据每次请求都要到 DB 去查询, 失去了缓存的意义. 在流量大时, 可能 DB 就挂掉了.
缓存空值, 不会查数据库.
采用布隆过滤器, 将所有可能存在的数据哈希到一个足够大的 bitmap 中, 查询不存在的数据会被这个 bitmap 拦截掉, 从而避免了对 DB 的查询压力.
布隆过滤器的原理: 当一个元素被加入集合时, 通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点, 把它们置为 1. 查询时, 将元素通过散列函数映射之后会得到 k 个点, 如果这些点有任何一个 0, 则被检元素一定不在, 直接返回; 如果都是 1, 则查询元素很可能存在, 就会去查询 Redis 和数据库.
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间, 导致缓存在某一时刻同时失效, 请求全部转发到 DB,DB 瞬时压力过重挂掉.
解决方法: 在原有的失效时间基础上增加一个随机值, 使得过期时间分散一些.
缓存击穿
缓存击穿: 大量的请求同时查询一个 key 时, 此时这个 key 正好失效了, 就会导致大量的请求都落到数据库. 缓存击穿是查询缓存中失效的 key, 而缓存穿透是查询不存在的 key.
解决方法: 加分布式锁, 第一个请求的线程可以拿到锁, 拿到锁的线程查询到了数据之后设置缓存, 其他的线程获取锁失败会等待 50ms 然后重新到缓存取数据, 这样便可以避免大量的请求落到数据库.
- public String get(String key) {
- String value = Redis.get(key);
- if (value == null) { // 缓存值过期
- String unique_key = systemId + ":" + key;
- // 设置 30s 的超时
- if (Redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) { // 设置成功
- value = db.get(key);
- Redis.set(key, value, expire_secs);
- Redis.del(unique_key);
- } else { // 其他线程已经到数据库取值并回写到缓存了, 可以重试获取缓存值
- sleep(50);
- get(key); // 重试
- }
- } else {
- return value;
- }
- }
Redis 怎么实现消息队列?
使用一个列表, 让生产者将任务使用 LPUSH 命令放进列表, 消费者不断用 RPOP 从列表取出任务.
BRPOP 和 RPOP 命令相似, 唯一的区别就是当列表没有元素时 BRPOP 命令会一直阻塞连接, 直到有新元素加入.
BRPOP queue 0 //0 表示不限制等待时间
优先级队列
如果多个键都有元素, 则按照从左到右的顺序取元素.
BLPOP queue:1 queue:2 queue:3 0
发布 / 订阅模式
PSUBSCRIBE channel?* 按照规则订阅.
PUNSUBSCRIBE channel?* 退订通过 PSUBSCRIBE 命令按照某种规则订阅的频道. 其中订阅规则要进行严格的字符串匹配, PUNSUBSCRIBE * 无法退订 channel?* 规则.
- PUBLISH channel1 hi
- SUBSCRIBE channel1
- UNSUBSCRIBE channel1 // 退订通过 SUBSCRIBE 命令订阅的频道.
缺点: 在消费者下线的情况下, 生产的消息会丢失.
延时队列
使用 sortedset, 拿时间戳作为 score, 消息内容作为 key, 调用 zadd 来生产消息, 消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理.
pipeline 的作用?
Redis 客户端执行一条命令分 4 个过程: 发送命令, 命令排队, 命令执行, 返回结果. 使用 pipeline 可以批量请求, 批量返回结果, 执行速度比逐条执行要快.
使用 pipeline 组装的命令个数不能太多, 不然数据量过大, 增加客户端的等待时间, 还可能造成网络阻塞, 可以将大量命令的拆分多个小的 pipeline 命令完成.
原生批命令 (mset 和 mget) 与 pipeline 对比:
原生批命令是原子性, pipeline 是非原子性. pipeline 命令中途异常退出, 之前执行成功的命令不会回滚.
原生批命令只有一个命令, 但 pipeline 支持多命令.
LUA 脚本
Redis 通过 LUA 脚本创建具有原子性的命令: 当 lua 脚本命令正在运行的时候, 不会有其他脚本或 Redis 命令被执行, 实现组合命令的原子操作.
在 Redis 中执行 Lua 脚本有两种方法: eval 和 evalsha.eval 命令使用内置的 Lua 解释器, 对 Lua 脚本进行求值.
- // 第一个参数是 lua 脚本, 第二个参数是键名参数个数, 剩下的是键名参数和附加参数
- > eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
- 1) "key1"
- 2) "key2"
- 3) "first"
- 4) "second"
lua 脚本作用
1,Lua 脚本在 Redis 中是原子执行的, 执行过程中间不会插入其他命令.
2,Lua 脚本可以将多条命令一次性打包, 有效地减少网络开销.
应用场景
举例: 限制接口访问频率.
在 Redis 维护一个接口访问次数的键值对, key 是接口名称, value 是访问次数. 每次访问接口时, 会执行以下操作:
通过 aop 拦截接口的请求, 对接口请求进行计数, 每次进来一个请求, 相应的接口访问次数 count 加 1, 存入 Redis.
如果是第一次请求, 则会设置 count=1, 并设置过期时间. 因为这里 set()和 expire()组合操作不是原子操作, 所以引入 lua 脚本, 实现原子操作, 避免并发访问问题.
如果给定时间范围内超过最大访问次数, 则会抛出异常.
- private String buildLuaScript() {
- return "local c" +
- "\nc = redis.call('get',KEYS[1])" +
- "\nif c and tonumber(c)> tonumber(ARGV[1]) then" +
- "\nreturn c;" +
- "\nend" +
- "\nc = redis.call('incr',KEYS[1])" +
- "\nif tonumber(c) == 1 then" +
- "\nredis.call('expire',KEYS[1],ARGV[2])" +
- "\nend" +
- "\nreturn c;";
- }
- String luaScript = buildLuaScript();
- RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
- Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
PS: 这种接口限流的实现方式比较简单, 问题也比较多, 一般不会使用, 接口限流用的比较多的是令牌桶算法和漏桶算法.
什么是 RedLock?
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock, 此种方式比原先的单节点的方法更安全. 它可以保证以下特性:
安全特性: 互斥访问, 即永远只有一个 client 能拿到锁
避免死锁: 最终 client 都可能拿到锁, 不会出现死锁的情况, 即使原本锁住某资源的 client 挂掉了
容错性: 只要大部分 Redis 节点存活就可以正常提供服务
文章对你有用的话, 点个赞, 支持一下~
我是大彬, 非科班转码, 校招拿了多家互联网中大厂 offer, 专注分享 Java 技术干货, 欢迎关注~
来源: https://segmentfault.com/a/1190000041249946