目录
如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?
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 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。
业务一致性要求高怎么办?
- 先更新数据库再更新缓存
可以先更新数据库再更新缓存,但是可能会有并发更新的缓存不一致的问题。
解决办法:
- 更新缓存前加一个分布式锁,保证同一时间只运行一个请求更新缓存,加锁后对于写入的性能就会带来影响;
-
- 过期时间保底:在更新完缓存时,给缓存加上较短的过期时间,出现缓存不一致的情况缓存的数据也会很快过期。
- 延迟双删(针对的是先删除缓存再更新数据库)
采用延迟双删,先删除缓存,然后更新数据库,回写缓存,等待一段时间再删除缓存。保证第一个操作在睡眠之后,第二个操作完成更新缓存操作。但是具体睡眠多久其实是个玄学,很难评估出来,这个方案也只是尽可能保证一致性而已,依然也会出现缓存不一致的现象。
「先更新数据库,再删除缓存」的方案虽然保证了数据库与缓存的数据一致性,但是每次更新数据的时候,缓存的数据都会被删除,这样会对缓存的命中率带来影响。
所以,如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,因为更新缓存并不会出现缓存未命中的情况。
数据库与缓存的一致性问题
更新缓存的方式,在「并发」场景下是无法保证数据库和缓存的数据一致性的,且存在「缓存利用率不高」和「性能浪费」的问题。因此,我们更推荐使用删除缓存的方式; 删除缓存的速度比更新缓存的速度要快得多
- 「先删除缓存,再更新数据库」的方案在并发场景下存在数据不一致的问题,业内采用较多的解决方案是「延迟双删」,但这个延迟时间很难评估,所以更推荐使用「先更新数据库,再删除缓存」的解决方案;
- 「先更新数据库,再删除缓存」的方案,在并发场景下可以认为是不存在数据不一致的问题。但是,为了保证「更新数据库」和「删除缓存」这两步都成功执行,我们可以通过「引入消息队列,采用异步重试机制」或者采用「订阅数据库变更日志,操作缓存」的方式来做;
无论采用哪种方案,我们都是很难做到数据的强一致的!我们只能尽可能地去降低数据不一致的问题出现的概率。
数据库和缓存的一致性如何保证
- 对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。
- 对于写数据,我会选择更新 db 后,再删除缓存。
针对删除缓存异常的情况,
- 对 key 设置过期时间兜底,只要过期时间一到,过期的 key 就会被删除了。
- 引入消息队列保证缓存被删除
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个例子,来说明重试机制的过程。
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 去更新本地缓存。
如果在项目中多个地方都要使用到二级缓存的逻辑,如何设计这一块?
在设计时,应该清楚地区分何时使用一级缓存和何时使用二级缓存。
通常情况下,对于频繁访问但不经常更改的数据,可以放在本地缓存中以提供最快的访问速度。而对于需要共享或者一致性要求较高的数据,应当放在一级缓存中。
如何避免缓存失效?
- 由后台线程频繁地检测缓存是否有效,检测到缓存失效了马上从数据库读取数据,并更新到缓存。
- 在业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。
- 在业务刚上线的时候,最好提前把数据缓存起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。
什么情况下,适合用订阅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 脚本) 。
- 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问题
- 热key拆分
这其实也是一种分而治之的思想。将一个热点key拆分成多个key,比如我们可以在每一个热点key后面加上前缀或者后缀,然后把这些拆分后的key分散存储在各个Redis集群节点中。
- 引入二级缓存
我们可以通过引入二级缓存,即JVM级别的缓存的方式来缓解 Redis的读压力。
这些本地的缓存工具有很多,比如 Caffeine、Guava 等,或者直接使用 HashMap 作为本地缓存都是可以的。
注意,如果对热 Key 进行本地缓存,需要防止本地缓存过大。 京东零售的hotkey缓存组件
- Redis集群扩容
我们可以通过增加集群中的从节点,从而分摊瞬时的读压力!不过这种方式付出的成本较大,一般情况下不建议作为首选方案。
缓存预热怎么做呢?
缓存预热是指在系统启动时,提前将一些预定义的数据加载到缓存中,以避免在系统运行初期由于缓存未命中(cache miss)导致的性能问题。
通过缓存预热,可以确保系统在上线后能够立即提供高效的服务,减少首次访问时的延迟。
项目启动时自动加载和定时预热两种方式,比如说每天定时更新站点地图到 Redis 缓存中
@Scheduled(cron = "0 15 5 * * ?")
热点 key 重建?问题?解决?
开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
- 当前 key 是一个热点 key(热门的娱乐新闻),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
怎么处理呢?
要解决这个问题也不是很复杂,解决问题的要点在于:
- 减少重建缓存的次数。
- 数据尽可能一致。
- 较少的潜在危险。
所以一般采用如下方式:
- 互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
- 永远不过期 “永远不过期”包含两层意思:
-
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个 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再从另一个缓存数据结构中读取实际的商品信息,并返回。
自己整理,借鉴很多博客,感谢他们