Bootstrap

Redis-应用

目录

应用

缓存雪崩、击穿、穿透和解决办法?

布隆过滤器是怎么工作的?

缓存的数据一致性怎么保证

Redis和Mysql消息一致性

业务一致性要求高怎么办?

数据库与缓存的一致性问题

数据库和缓存的一致性如何保证

如何保证本地缓存和分布式缓存的一致?

如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?

如何避免缓存失效?

什么情况下,适合用订阅binlog的方式

Redis 如何实现延时队列?

Redis管道有什么用?

大 key 问题了解吗?

大key会有什么问题?

如何找到大 key?

如何处理大 key?

热 key 是什么?

热key的危害

解决热key问题

缓存预热怎么做呢?

热点 key 重建?问题?解决?

无底洞问题

为什么会产生这种现象呢?

无底洞问题如何优化呢?

Redis 支持事务吗?

Redis支持事务回滚吗?

Redis 事务为什么不支持回滚?

Redis 事务的原理

你有实际使用过Redis做什么应用么?

分布式锁

redis的分布式锁怎么实现?

为什么需要引入owner的概念呢?

你提到了lua,用lua一定能保证原子性?

分布式锁是完全可靠的吗?

Redis和Lua

Redis实现分布式锁?

Redisson 了解吗?

使用Redis实现分布式锁的优点和缺点?

如何为分布式锁设置合理的超时时间?

Redis解决集群情况下分布式锁的可靠性?

Redis做秒杀场景可以吗?讲讲思路

Redis可以做消息队列吗?什么时候能用Redis做消息队列?

redis 中使用它作为轻量级消息队列的一些常见做法:

如何设计一个缓存策略,可以动态缓存热点数据呢?


应用

缓存雪崩、击穿、穿透和解决办法?

缓存雪崩

当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

解决方法

  • 大量数据同时过期
    • 均匀设置过期时间:避免将大量的数据设置成同一个过期时间。
    • 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在Redis里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存。未能获取互斥锁的请求等待锁释放后重新读取缓存,或者返回空值或者默认值。
    • 双key策略:使用两个key, 一个是主key,设置过期时间,一个是备key,不会设置过期,key不一样,但是value值是一样。当业务线程访问不到主key的缓存数据时,就直接返回备key的缓存数据,然后在更新缓存的时候,同时更新主key和备key的数据。
    • 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存"永久有效”,并将更新缓存的工作交由后台线程定时更新。
  • Redis故障宕机
    • 服务熔断 : 启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,所以不用再继续访问数据库,保证数据库系统的正常运行,等到 Redis恢复正常后,再允许业务应用访问缓存服务。

服务熔断机制是保护数据库的正常运行,但是暂停了业务应用访问缓存系统,全部业务都无法正常工作。

    • 启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务。
    • 构建高可靠集群:通过主从节点的方式构建Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。

缓存击穿

如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。

解决方案:

  • 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。

缓存穿透

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

解决方案

  • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候会发生缓存穿透,可以在API入口处判断请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
  • 缓存空值或者默认值:当线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。(占用大量内存)
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

布隆过滤器是怎么工作的?

布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成。在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。

第一步,使用N个哈希函数分别对数据做哈希计算,得到N个哈希值

第二步,将第一步得到的N个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置

第三步,将每个哈希值在位图数组的对应位置的值设置为1

缺陷

  • 布隆过滤器由于是基于哈希函数实现查找的,会存在哈希冲突的可能性,数据可能落在相同位置,存在误判的情况。查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
  • 不支持一个关键字的删除,因为一个关键字的删除会牵连其他的关键字。改进方法就是counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。
  • 对于输入的n个元素,要确定数组m大小和hash函数的个数,hash函数个数k = (ln2) * (m/n)时,错误率最小。在错误率不大于E情况下,m至少要等于n*lg(1/E)才能表示n个元素的集合

缓存的数据一致性怎么保证

这个我们也是考虑过几个方案的,

第一个方案就是完全依赖于Redis的过期删除策略,但是这种策略的话如果过期时间太长会造成一直查到脏的数据,如果太短的话又会造成缓存失效的问题;

第二个方案是先更新数据库再删缓,这种情况的话就是可以在第一种情况下更快达到数据一致性,但是这个删除缓存不一定会成功,尽量不要作为核心逻辑,而且同时和数据库、Redis连接造成资源浪费的问题;

第三个方案可以使用消息队列的方式,我们在数据库更改数据,然后放入消息队列中,然后由消息队列告诉Redis更改缓存,这种可以实现业务的解耦,而且可靠性会更好一些,但是可能会造成一些时序性的问题,而且引入新的组件,增加了系统复杂度;

第四种方案我们可以使用Canal作为MySQL的从机,订阅binlog文件,然后将binlog文件传给canal,由canal传送给Redis,这种方案可以实现完全的解耦,如果同步要求不高的话,可以减少延迟,并且没有时序性的问题,但是引入新的组件,复杂度也会升高,同步要求比较高的话会造成很长一段时间读到脏数据;

最后基于各个方案的成本和收益的考虑,选择了旁路缓存策略+过期时间的方式来避免数据一致性的问题,因为这个方案实现最简单。

Redis和Mysql消息一致性

在项目中使用过期时间来兜底,并且在更新DB后删除缓存来提升一致性的方式。另外,在做方案设计时候我还考虑过订阅binlog 的方式,但这种方案额外引入了消息队列和消费服务,成本太高而收益不足,所以还是选择了前者。

过期时间来兜底:在更新完缓存时,给缓存加上较短的过期时间,出现缓存不一致的情况缓存的数据也会很快过期

-----------------------------------------------------------------------------------------------------------------------

如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

业务一致性要求高怎么办?

  1. 先更新数据库再更新缓存

可以先更新数据库再更新缓存,但是可能会有并发更新的缓存不一致的问题。

解决办法:

  • 更新缓存前加一个分布式锁,保证同一时间只运行一个请求更新缓存,加锁后对于写入的性能就会带来影响;
    • 过期时间保底:在更新完缓存时,给缓存加上较短的过期时间,出现缓存不一致的情况缓存的数据也会很快过期。
  1. 延迟双删(针对的是先删除缓存再更新数据库

采用延迟双删,先删除缓存,然后更新数据库,回写缓存,等待一段时间再删除缓存。保证第一个操作在睡眠之后,第二个操作完成更新缓存操作。但是具体睡眠多久其实是个玄学,很难评估出来,这个方案也只是尽可能保证一致性而已,依然也会出现缓存不一致的现象。

「先更新数据库,再删除缓存」的方案虽然保证了数据库与缓存的数据一致性,但是每次更新数据的时候,缓存的数据都会被删除,这样会对缓存的命中率带来影响。

所以,如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。

数据库与缓存的一致性问题

更新缓存的方式,在「并发」场景下是无法保证数据库和缓存的数据一致性的,且存在「缓存利用率不高」和「性能浪费」的问题。因此,我们更推荐使用删除缓存的方式; 删除缓存的速度比更新缓存的速度要快得多

  1. 「先删除缓存,再更新数据库」的方案在并发场景下存在数据不一致的问题,业内采用较多的解决方案是「延迟双删」,但这个延迟时间很难评估,所以更推荐使用「先更新数据库,再删除缓存」的解决方案;
  2. 「先更新数据库,再删除缓存」的方案,在并发场景下可以认为是不存在数据不一致的问题。但是,为了保证「更新数据库」和「删除缓存」这两步都成功执行,我们可以通过「引入消息队列,采用异步重试机制」或者采用「订阅数据库变更日志,操作缓存」的方式来做;

无论采用哪种方案,我们都是很难做到数据的强一致的!我们只能尽可能地去降低数据不一致的问题出现的概率。

数据库和缓存的一致性如何保证

  1. 对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。
  2. 对于写数据,我会选择更新 db 后,再删除缓存。

针对删除缓存异常的情况,

  1. 对 key 设置过期时间兜底,只要过期时间一到,过期的 key 就会被删除了。
  2. 引入消息队列保证缓存被删除

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。

3.数据库订阅+消息队列保证缓存被删除

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

下图是 Canal 的工作原理:

所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

如何保证本地缓存和分布式缓存的一致?

采用了本地缓存 Caffeine(或者 Guava Cache) + Redis 缓存的策略。分布式缓存基本就是采用 Redis。

当数据库发生变化时,我们直接删除 Redis 缓存中的 key 就可以了,因为下一次请求会将数据库同步到 Redis 缓存中。

那为了保证本地缓存和 Redis 缓存的一致性,我们可以采用的策略有:

①、设置本地缓存的过期时间,这是最简单也是最直接的方法,当本地缓存过期时,就从 Redis 缓存中去同步。

②、使用 Redis 的 Pub/Sub 机制,当 Redis 缓存发生变化时,发布一个消息,本地缓存订阅这个消息,然后删除对应的本地缓存。

③、Redis 缓存发生变化时,引入消息队列,比如 RocketMQ、RabbitMQ 去更新本地缓存。

如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?

在设计时,应该清楚地区分何时使用一级缓存和何时使用二级缓存。

通常情况下,对于频繁访问但不经常更改的数据,可以放在本地缓存中以提供最快的访问速度。而对于需要共享或者一致性要求较高的数据,应当放在一级缓存中

如何避免缓存失效?

  1. 后台线程频繁地检测缓存是否有效,检测到缓存失效了马上从数据库读取数据,并更新到缓存。
  2. 在业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。
  3. 在业务刚上线的时候,最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。
什么情况下,适合用订阅binlog的方式

这种模式更像是同步数据,其实比较适合缓存很长时间过期、或者不过期的场景

举个简单的例子,如果有一个视频网站,有几个展示视频,他们的基本信息流量很大,但是这几个视频的信息基本不会变动,是稳定的,就可以用这种方式。

Redis 如何实现延时队列?

使用ZSet, ZSet 有一个 Score 属性,将元素的过期时间作为分数值,进行排序,从而实现延迟队列

Score是一个浮点值,可以设计函数将时间加入到SCORE内,实现按时间排序(游戏排行榜 )

使用 zadd score1 value1 命令,再利用 zrangebysocre查询符合条件的所有待处理的任务,使用ZREM命令删除获取的成员,防止重复执行,之后通过循环执行队列任务。

----------------------------------------------------------------------------------------

第一步,将任务添加到 zset 中,score 为任务的执行时间戳,value 为任务的内容。

第二步,定期(例如每秒)从 zset 中获取 score 小于当前时间戳的任务,然后执行任务。

第三步,任务执行后,从 zset 中删除任务。

Redis管道有什么用?

管道技术本质上是客户端提供的功能,而非 Redis服务器端的功能。

Redis 提供三种将客户端多条命令打包发送给服务端执行的方式:

Pipelining(管道) 、 Transactions(事务) 和 Lua Scripts(Lua 脚本) 。

  1. Pipelining(管道)

Redis 管道是三者之中最简单的,当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。

Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。

Pipelining 的优势

在性能方面, Pipelining 有下面两个优势:

  • 节省了 RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
  • 减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。

大 key 问题了解吗?

大 key 指的是存储了大量数据的键,比如:

  • 单个简单的 key 存储的 value 很大,size 超过 10KB
  • hash,set,zset,list 中存储过多的元素(以万为单位)
大key会有什么问题?

大 key 会带来以下四种影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较小。
如何找到大 key?

①、bigkeys 参数:使用 bigkeys 命令以遍历的方式分析 Redis 实例中的所有 Key,并返回整体统计信息与每个数据类型中 Top1 的大 Key

bigkeys 命令的使用:redis-cli --bigkeys

②、redis-rdb-tools:redis-rdb-tools 是由 Python 语言编写的用来分析 Redis 中 rdb 快照文件的工具。

如何处理大 key?

  • 拆分成多个小key。这是最容易想到的办法,降低单key的大小,读取可以用mget批量读取。
  • 设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。
  • 启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。
  • 数据分片。例如使用Redis Cluster将数据分散到多个Redis实例,以减轻单个实例的负担,降低大Key问题的风险。

①、删除大 key

  • 当 Redis 版本大于 4.0 时,可使用 UNLINK 命令安全地删除大 Key,该命令能够以非阻塞的方式,逐步地清理传入的大 Key。
  • 当 Redis 版本小于 4.0 时,建议通过 SCAN 命令执行增量迭代扫描 key,然后判断进行删除。

②、压缩和拆分 key

  • 当 vaule 是 string 时,比较难拆分,则使用序列化、压缩算法将 key 的大小控制在合理范围内,但是序列化和反序列化都会带来额外的性能消耗。
  • 当 value 是 string,压缩之后仍然是大 key 时,则需要进行拆分,将一个大 key 分为不同的部分,记录每个部分的 key,使用 multiget 等操作实现事务读取。
  • 当 value 是 list/set 等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

热 key 是什么?

Redis 热key是指被频繁访问的key,可能会导致单个key的访问量过大,影响系统性能。

再比如说 Redis 是集群部署,热 key 可能会造成整体流量的不均衡(网络带宽、CPU 和内存资源),个别节点出现 OPS 过大的情况,极端情况下热点 key 甚至会超过 Redis 本身能够承受的 OPS。

OPS(Operations Per Second)是 Redis 的一个重要指标,表示 Redis 每秒钟能够处理的命令数。

通常以 Key 被请求的频率来判定,比如:

  • QPS 集中在特定的 Key:总的 QPS(每秒查询率)为 10000,其中一个 Key 的 QPS 飙到了 8000。
  • 带宽使用率集中在特定的 Key:一个拥有上千成员且总大小为 1M 的哈希 Key,每秒发送大量的 HGETALL 请求。
  • CPU 使用率集中在特定的 Key:一个拥有数万个成员的 ZSET Key,每秒发送大量的 ZRANGE 请求。
热key的危害

1.占用大量的CPU资源,影响其他请求并导致整体性能降低。

2.集群架构下,产生访问倾斜,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题。

3.在抢购或秒杀场景下,可能因商品对应库存Key的请求量过大,超出Redis处理能力造成超卖。

4.热Key的请求压力数量超出Redis的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务。

解决热key问题
  1. 热key拆分

这其实也是一种分而治之的思想。将一个热点key拆分成多个key,比如我们可以在每一个热点key后面加上前缀或者后缀然后把这些拆分后的key分散存储在各个Redis集群节点中。

  1. 引入二级缓存

我们可以通过引入二级缓存,即JVM级别的缓存的方式来缓解 Redis的读压力。

这些本地的缓存工具有很多,比如 Caffeine、Guava 等,或者直接使用 HashMap 作为本地缓存都是可以的。

注意,如果对热 Key 进行本地缓存,需要防止本地缓存过大。 京东零售的hotkey缓存组件

  1. Redis集群扩容

我们可以通过增加集群中的从节点,从而分摊瞬时的读压力!不过这种方式付出的成本较大,一般情况下不建议作为首选方案。

缓存预热怎么做呢?

缓存预热是指在系统启动时,提前将一些预定义的数据加载到缓存中,以避免在系统运行初期由于缓存未命中(cache miss)导致的性能问题

通过缓存预热,可以确保系统在上线后能够立即提供高效的服务,减少首次访问时的延迟。

项目启动时自动加载定时预热两种方式,比如说每天定时更新站点地图到 Redis 缓存中

@Scheduled(cron = "0 15 5 * * ?")
热点 key 重建?问题?解决?

开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。

但是有两个问题如果同时出现,可能就会出现比较大的问题:

  • 当前 key 是一个热点 key(热门的娱乐新闻),并发量非常大。
  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

怎么处理呢?

要解决这个问题也不是很复杂,解决问题的要点在于:

  • 减少重建缓存的次数。
  • 数据尽可能一致。
  • 较少的潜在危险。

所以一般采用如下方式:

  1. 互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  2. 永远不过期 “永远不过期”包含两层意思:
    1. 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
    2. 从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

无底洞问题

分布式缓存添加节点之后,性能没有提升,反而下降了

为什么会产生这种现象呢?

通常来说添加节点使得 Memcache 集群性能应该更强了,但事实并非如此。

键值数据库由于通常采用哈希函数将 key 映射到各个节点上,造成 key 的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是 Memcache 还是 Redis 的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。

无底洞问题如何优化呢?

先分析一下无底洞问题:

  • 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。
  • 网络连接数变多,对节点的性能也有一定影响。

常见的优化思路如下:

  • 命令本身的优化,例如优化操作语句等。
  • 减少网络通信次数。
  • 降低接入成本,例如客户端使用长连/连接池、NIO 等。

Redis 支持事务吗?

Redis 支持简单的事务,可以将多个命令打包,然后一次性的,按照顺序执行。

主要通过 multi、exec、discard、watch 等命令来实现:

  • multi:标记一个事务块的开始
  • exec:执行所有事务块内的命令
  • discard:取消事务,放弃执行事务块内的所有命令
  • watch:监视一个或多个 key,如果在事务执行之前这个 key 被其他命令所改动,那么事务将被打断

Redis支持事务回滚吗?

不支持,Redis提供的DISCARD 命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。

一旦 EXEC 命令被调用,所有命令都会被执行,即使有些命令可能执行失败。失败的命令不会影响到其他命令的执行。

Redis 事务为什么不支持回滚?

引入事务回滚机制会大大增加 Redis 的复杂性,因为需要跟踪事务中每个命令的状态,并在发生错误时逆向执行命令以恢复原始状态。

Redis 是一个基于内存的数据存储系统,其设计重点是实现高性能。事务回滚需要额外的资源和时间来管理和执行,这与 Redis 的设计目标相违背。因此,Redis 选择不支持事务回滚。

Redis 事务的原理

  • 使用 MULTI 命令开始一个事务。从这个命令执行之后开始,所有的后续命令都不会立即执行,而是被放入一个队列中。在这个阶段,Redis 只是记录下了这些命令。
  • 使用 EXEC 命令触发事务的执行。一旦执行了 EXEC,之前 MULTI 后队列中的所有命令会被原子地(atomic)执行。这里的“原子”意味着这些命令要么全部执行,要么(在出现错误时)全部不执行。
  • 如果在执行 EXEC 之前决定不执行事务,可以使用 DISCARD 命令来取消事务。这会清空事务队列并退出事务状态。
  • WATCH 命令用于实现乐观锁。WATCH 命令可以监视一个或多个键,如果在执行事务的过程中(即在执行 MULTI 之后,执行 EXEC 之前),被监视的键被其他命令改变了,那么当执行 EXEC 时,事务将被取消,并且返回一个错误。

你有实际使用过Redis做什么应用么?

项目中涉及过Redis缓存场景、Redis分布式锁场景

分布式锁

redis的分布式锁怎么实现?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

Redis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

  • 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
  • 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000
  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。

可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。

为什么需要引入owner的概念呢?

分布式锁需要保证对称性,同一个锁,加锁和解锁必须是同一个竞争者。不能把其他竞争者持有的锁给释放了(超时自动释放除外)。

你提到了lua,用lua一定能保证原子性?

lua本身不具备原子性,上面提到用lua来保证原子性是因为Redis是单线程执行,一个流程放进lua来执行,相当于是打包在一起,Redis执行他的过程中不会被其他请求打断,所以说保证了原子性。

在释放的时候将查询key,删除key打包到一起,其中只有最后删除是写操作,所以这个流程本身是保证了原子性的。

分布式锁是完全可靠的吗?

没有完全可靠的分布式锁,在使用分布式锁的时候就要考虑到这一点,关键业务还是需要幂等来兜底。当然我们可以使用RedLock集群化的分布式锁,这种模式出问题的概率就微乎其微了。

Redis和Lua

Redis 是2.6版本通过内嵌支持Lua环境。执行脚本的常用命令为EVAL

Redis因为是单线程操作,处理过程中,是不会被打断并切换到其它处理,另外Redis将Lua脚本作为一种整体执行,不出异常的情况下,也不会被打断。

Redis事务在运行时某个指令发生错误,该指令前后指令不受影响

Lua执行一半失败,没回滚,会中断,后续脚本不会继续执行

redis.call 是 Redis 提供的用于在 Lua 脚本中执行 Redis 命令的函数,

Redis实现分布式锁?

使用SETNX命令,只有插入的key不存在才插入,如果SETNX的key存在就插入失败,key插入成功代表加锁成功,否则加锁失败;

解锁的过程就是将key删除,保证执行操作的客户端就是加锁的客户端,加锁时候要设置unique_value,解锁的时候,要先判断锁的 unique_value是否为加锁客户端,是才将 lock_key键删除。(owner)

此外要给锁设置一个过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,可以指定EX/PX参数设置过期时间。

问题:锁不支持续期,不支持可重入锁,可以使用redission实现的更完善分布式锁

Redisson 的分布式锁实现了可重入的功能; 同时,为了避免锁超时,Redisson 中引入了看门狗的机制,默认每10秒钟一次会检测锁是否被释放,如果没有释放,就会重新设置一个30秒的有效期,这就是所谓的分布式锁的自动续期

但是,线程2也不会傻傻的一直等待!如果线程1业务执行完成,会执行释放锁的操作。

线程1释放锁的操作会首先删除当前分布式镇锁,然后发布一个unlockMessage 到redisson_lock_charnel频道上。根据Rodis 的发布/订间机制,此时线程2就会收到通知取消等待。线程2取消等特后,会再次执行 while循环去会试加锁:

SET lock_key unique_value NX PX 10000

Redisson 了解吗?

Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),提供了一系列 API 用来操作 Redis,其中最常用的功能就是分布式锁。

普通锁的实现源码是在 RedissonLock 类中,也是通过 Lua 脚本封装一些 Redis 命令来实现的

其中 hincrby 命令用于对哈希表中的字段值执行自增操作,pexpire 命令用于设置键的过期时间。比 SETNX 更优雅。

RLock lock = redisson.getLock("lock");
lock.lock();try {// do something} finally {
    lock.unlock();}

使用Redis实现分布式锁的优点和缺点?

  • 优点: 性能高效; 实现方便; 避免单点故障,
  • 缺点:
    • 超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。
    • Redis主从复制模式中的数据是异步复制的,导致分布式锁的不可靠性。如果在Redis主节点获取到锁后,在没有同步到其他节点时,Redis主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

如何为分布式锁设置合理的超时时间?

可以基于续约的方式设置超时时间: 先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。

实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。

Redis解决集群情况下分布式锁的可靠性?

分布式锁算法Redlock(红锁)。基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署5个Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。

基本思路:是让客户端和多个独立的Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么就认为,客户端成功地获得分布式锁,否则加锁失败即使有某个 Redis 节点发生故障,锁的数据在其他节点上也有保存,客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。

Redlock 算法加锁三个过程:

  • 第一步是,客户端获取当前时间(t1)。
  • 第二步是,客户端按顺序依次向N个Redis 节点执行加锁操作:加锁操作使用 SET NX, EX/PX选项,以及带上客户端的唯一标识。如果某个Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,需要给「加锁操作」设置一个超时时间加锁操作的超时时间需要远远地小于锁的过期时间
  • 第三步是,一旦客户端从超过半数(大于等于N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1<锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。

加锁成功要同时满足两个条件:有超过半数的Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。

加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。

如果计算的结果已经来不及完成共享数据的操作了,可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。加锁失败后,客户端向所有 Redis节点发起释放锁的操作,执行释放锁的Lua 脚本就可以。

Redis做秒杀场景可以吗?讲讲思路

Redis可以用来记录库存,利用Redis的高性能进行库存的扣减,一个Redis处理6W的请求问题不大,100W/s流量就20台Redis来支撑,当然,每个节点都要做主从容灾

另一个方式就是把Redis作为轻量级消息队列,来接受请求,但是不如kafka这种可靠。

Redis可以做消息队列吗?什么时候能用Redis做消息队列?

Redis可以作为轻量级消息队列。如果是本身业务轻量级,且团队没有已经接入完备的消息队列,这个时候没有必要引入一个重量消息队列,使用Redis即可满足要求,没有不能用的组件,只有不合适的场景。

用消息队列发短信,我们肯定也经常遇到过,短信没收到的场景吧,没收到重试就行了。

redis 中使用它作为轻量级消息队列的一些常见做法:

  • List 数据结构:Redis 中的 List 数据结构非常适合用作消息队列。通过使用 LPUSH 命令将消息推送到列表的左侧,然后使用 RPOP 命令从列表的右侧弹出消息,可以实现基本的消息入队和出队操作。
  • Pub/Sub 发布订阅功能:Redis 提供了 Pub/Sub 功能,可以实现发布者(Publisher)和订阅者(Subscriber)之间的消息传递。发布者将消息发布到指定的频道,而订阅者可以订阅感兴趣的频道并接收到发布的消息。这种模式适用于广播消息或实现简单的消息通知系统。
  • 延迟队列:通过使用 Redis 的有序集合(Sorted Set)和过期时间(TTL)特性,可以实现延迟队列。将消息作为有序集合的成员,并设置成员的分数为消息的过期时间,然后使用定时任务或轮询机制来检查过期的消息并进行处理。这种方式可以实现具有延迟执行需求的任务调度。

如何设计一个缓存策略,可以动态缓存热点数据呢?

热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。用 zadd 方法和 zrange 方法来完成排序队列和获取前面商品。

以电商平台场景中的例子,现在要求只缓存用户经常访问的Top 1000的商品。具体细节如下:

  • 先通过缓存系统做一个排序队列(比如存放1000个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
  • 同时系统会定期过滤掉队列中排名最后的200个商品,然后再从数据库中随机读取出200个商品加入队列中;
  • 当请求每次到达的时候,会先从队列中获取商品ID,如果命中,就根据ID再从另一个缓存数据结构中读取实际的商品信息,并返回。

自己整理,借鉴很多博客,感谢他们

;