Redis经常作为MySQL的缓存来使用,为什么?#
有几个要点:
MySQL是磁盘操作,性能不高
Redis内存操作,性能非常高
缓存本质是高速存储临时存储低速存储的数据
回答#
MySQL是关系型数据库系统,操作数据需要访问磁盘,性能不高,通常就几千QPS,而Redis基于内存并做了很多优化,性能高达10W QPS,一些热点数据就可以缓存到Redis,查询时先查Redis,不存在才查MySQL,这种Redis+MySQL结合的方式可以有效提高系统QPS。
Redis 和 Mysql 的一致性问题⭐⭐⭐⭐⭐#
回答#
如何保证删除缓存操作一定能成功?#
缓存删除是一次 Redis 操作,可能因为网络波动等原因执行失败,提高删除成功率有2种思路,一个是失败就扔入重试队列,但是扔入队列这步也可能失败,所以只能说是提高,第二种就是绕过这个问题,缓存删除一般是为了数据同步,可以使用订阅MySQL BinLog的方式来同步数据。
回答#
可以引入消息队列,删除缓存的操作由消费者来做,删除失败的话重新去消息队列拉取相应的操作,超过一定次数没有删除成功就像业务层报错。
如果是MySQL、Redis缓存同步场景,为了保证成功率,可以用一个消费服务订阅 MySQL binlog 日志,拿到具体要操作的数据,然后再向Redis执行缓存删除操作。
业务缓存一致性要求高怎么办?#
既然用了缓存,就始终存在不一致性的时间,只能说尽可能减少这个时间,不可能完全一致,如果需要完全一致就应该用缓存。
回答#
延迟双删是提高一致性的方案,先删除缓存,然后更新数据库,等待一段时间再删除缓存。保证第一个操作再睡眠之后,第二个操作完成更新缓存操作。但是具体睡眠多久其实是个玄学,很难评估出来,这个方案也只是尽可能保证一致性而已,依然也会出现缓存不一致的现象。
如何避免缓存失效#
缓存正常来说都是有过期时间的,过期时间到了,这时候缓存就会被删除,对于MySQL(或其它数据源)而言也就是缓存失效了
回答#
首先业务发现缓存失效,是会去读MySQL数据重新加载进去的,但是为了尽可能避免缓存失效,我们可以由后台线程频繁地检测缓存是否有效,检测到即将失效了马上从数据库读取数据,并更新到缓存。
另外,在业务刚上线的时候,最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热。
Redis 做秒杀场景#
Redis在秒杀场景主要的应用方式是两个。
一个是把Redis当消息队列用,用于削峰,一个是用Redis记录库存,进行库存的加减。
回答#
Redis可以用来记录库存,利用Redis的高性能进行库存的扣减,一个Redis处理6W的请求问题不大,100W/s流量就20台Redis来支撑,当然,每个节点都要做主从容灾。
另一个方式就是把Redis作为轻量级消息队列,来接受请求,但是不如kafka这种可靠。
Redis管道有什么用?#
Redis 管道(Pipeline)是一种优化客户端与 Redis 服务器之间通信的机制,主要用于减少网络往返时间(RTT,Round-Trip Time),从而提升性能。
管道的工作原理:
管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。
客户端:
- 客户端将多个命令缓存到本地,而不是立即发送到服务器。
- 当缓存达到一定数量或显式调用EXEC时,客户端一次性将所有命令发送到服务器。
服务器:
- 服务器按照接收到的顺序依次执行所有命令。
- 执行完成后,服务器将所有结果一次性返回给客户端。
结果返回:
- 客户端接收到所有命令的执行结果,并按顺序处理。
管道的使用场景:
批量写入:
- 需要一次性写入大量数据时(如初始化缓存、批量插入数据),使用管道可以显著提高性能。
- 示例:批量设置多个键值对。
批量读取:
- 需要一次性读取多个键的值时,使用管道可以减少网络延迟。
- 示例:批量获取多个用户的信息。
高并发场景:
- 在高并发场景下,使用管道可以减少客户端与服务器之间的通信次数,降低系统负载。
事务优化:
- 在事务(MULTI/EXEC)中,使用管道可以避免多次网络往返。
回答#
管道技术是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
Pipeline的本质,是将请求在客户端打包,然后一次发送给服务端,服务端处理完成之后,会将结果存起来,等Pipeline中所有命令都完成了,再一起回包,这样可以节约很多网络交互的时间。
但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。
Redis如何处理大key?#
一般而言String 类型的值大于 10 KB;Hash、List、Set、ZSet 类型的元素的个数超过5000个;
影响:
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时。客户端认为很久没有响应。
- 引发网络阻塞:每次获取大 key 产生的网络流量较大。
- 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令,一般要用unlink去异步删除
- 内存分布不均:集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较小。
处理:
- 当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
- 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
- 分拆成几个key-value,存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性
- 当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
Redis支持事务回滚吗#
Redis不具备完整的ACID特性,执行的命令都没有回滚之说,无论是事务还是LUA脚本中的命令,都是一样的。
回答#
不支持,Redis 不具备完整的 ACID 特性,执行的命令都没有回滚之说,Redis 提供的 DISCARD 命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。至于 LUA 脚本也是一样,比如LUA里面有 2 个写操作,执行了第一个如果 Redis 挂掉,那么第二个不会执行,第一个也不回撤回。
Redis 如何实现延迟队列?#
首先要知道什么是延迟队列:
延迟队列是指把当前要做的事情,往后推迟一段时间再做:
- 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消
- 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单
- 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单
知道延迟队列是什么之后,不难发现Redis中ZSet最适合模拟这个功能
回答#
使用ZSet,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。使用 zadd score1 value1 命令,再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务。
Redis可以做消息队列吗?什么时候能用Redis做消息队列?#
在某些场景,其实我们并不是一定需要有多可靠、多完善的消息队列,比如发用消息队列发短信,我们肯定也经常遇到过,短信没收到的场景吧?没收到重试就行了。
所以,轻量级消息队列也有了市场需要,Redis就很适合来做一个不那么完善的消息队列。
回答#
Redis可以作为轻量级消息队列。如果是本身业务轻量级,且团队没有已经接入完备的消息队列,这个时候没有必要引入一个重量消息队列,使用Redis即可满足要求,没有不能用的组件,只有不合适的场景。
什么是分布式锁?#
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:

回答#
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在多线程或者多进程并发的情况下,使用锁来保证一个代码块在同一时间内只能由一个线程执行。
如何理解Redis原子性操作原理?#
Redis 原子性操作的原理:
- 单线程模型 保证单条命令原子性。
- 内置命令(INCR、HSET、LPUSH)本身不可分割。
- MULTI / EXEC 确保事务性操作不会被中断(可以不提这个,很少后端真的用Redis事务,都是用LUA)
- Lua 脚本 执行过程中不可被其他命令插入,确保整体原子性。
这些特性让 Redis 在高并发场景下能够安全、高效地执行原子操作
回答#
Redis提供的API都是单线程串行处理的,所以我们用单条对象操作命令都不用担心被中断,如果是多条命令要实现原子性,通常都是用LUA脚本来支持。
分布式锁实现要点是什么#
redis分布式锁的加锁命令(一行命令实现互斥效果+过期时间,原子性):
// lock_key 就是 key 键;
// unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
// NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
// PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁
SET lock_key unique_value NX PX 10000 要注意 setnx 这个命令,是没办法携带过期时间参数的!如果 setnx + expire 2 个命令,就没办法保证加锁的原子性了!所以要用 set 命令,携带 nx 和 px 参数,才能保证加锁的原子性!
而解锁的过程就是将 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,要设置ttl,释放时需要用lua。
- 另一种思路是直接说最重要的是加锁、解锁流程,其中加锁是怎样的命令,解锁是怎样的命令。
回答#
分加锁解锁来说。
- 加锁是用SET命令,然后带上NX参数,key是锁名字,value是持有者id,再加一个过期时间,这里value要用持有者id的原因是谁申请、谁释放的原则,会在解锁时进行检查,过期时间是为了兜底,防止异常情况下锁被永久占据。
- 解锁的话,主要涉及两步操作,一个是查看是不是自己的锁,如果是接着就是释放锁,为了保证原子性,解锁需要用LUA脚本进行。
为什么需要引入 owner 的概念#
这个问题本质是问为什么分布式锁要这种对称性,对称性也就是说同一个锁,加锁和解锁必须是同一个竞争者。不能把其他竞争者持有的锁给释放了(超时自动释放除外)。
回答没有对称性会怎样,举个场景说明。
回答#
分布式锁需要保证对称性,假设没有这种对称性,会有问题,举个例子,服务A获取了锁,由于业务流程比较长,或者网络延迟、GC卡顿等原因,导致锁过期,而业务还会继续进行。这时候,业务B已经拿到了锁,准备去执行,这个时候服务A恢复过来并做完了业务,就会释放锁,而B却还在继续执行,等B完成下次释放的可能又是别人的锁,这种情况是需要避免的。
你提到了lua,用lua一定能保证原子性?#
回答#
lua 本身不具备原子性,上面提到用 lua 来保证原子性是因为 Redis 是单线程执行,一个流程放进 lua 来执行,相当于是打包在一起,Redis 执行他的过程中不会被其他请求打断,所以说保证了原子性。
这里我们也提到,我们是在释放的时候将查询key,删除key打包到一起,其中只有最后删除是写操作,所以这个流程本身是保证了原子性的。
基于 Redis 实现分布式锁有什么优缺点?#
基于 Redis 实现分布式锁的优点:
- 性能高效(这是选择缓存实现分布式锁最核心的出发点)。
- 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
- 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
基于 Redis 实现分布式锁的缺点:
- 超时时间不好设置:如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,注意 A 线程没执行完,后续线程 B 又意外的持有了锁,意味着可以操作共享资源,那么两个线程之间的共享资源就没办法进行保护了。
- 那么如何合理设置超时时间呢? 我们可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。
- **Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性:**如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
回答#
Redis 实现分布式锁的主要优点在于其性能高效、实现简单和被业界成熟应用。但是也有其局限性,首先是 Redis 的可用性直接影响到锁的可靠性,如果 Redis 服务出现故障,可能会导致锁服务不可用,虽然 Redis 提供了持久化机制,但在极端情况下,如 Redis 突然崩溃,可能会导致锁信息的丢失,从而引发锁失效的问题。
如何为Redis分布式锁设置合理的超时时间?#
两个方面,核心是评估业务逻辑:
- 正常情况下业务耗时:估算分布式锁保护的代码块或任务执行时间,包括数据库操作、外部API调用等。
- **分布式锁访问耗时:**Redis操作通常很快,但在高延迟环境中,需考虑客户端与Redis服务器之间的通信时间。
- 考虑最坏情况:确定任务在最坏情况下的执行时间(如网络抖动、资源竞争等)
回答#
核心是根据业务逻辑来评估,其次是网络延迟等交互消耗,比如业务最大执行时间是3s,网络延迟为200毫秒,多给1s缓冲,则超时时间可设置为4.2秒,向上取整为5秒。
怎么用Redis实现可重入的分布式锁?#
可重入锁即同一个线程或客户端可以多次获取同一把锁,核心目的是允许同一客户端多次获取锁,减少不必要的等待,提升效率,比较适用于嵌套调用或递归场景。
Redis 实现可重入的分布式锁的实现思路如下:
基本思路
- 锁标识:使用 Redis 的 SET 命令设置一个键值对,键为锁的名称,值为持有锁的客户端标识(如 UUID)。
- 可重入性:维护一个计数器,记录锁的重入次数。
实现步骤
**获取锁:**使用 SET 命令设置键值对,并设置过期时间,如果键已存在且值为当前客户端标识,则增加重入计数。
if redis.call('set', lock_name, client_id, 'NX', 'EX', 30) then
return 1
elseif redis.call('get', lock_name) == client_id then
redis.call("INCR", "lock_name:count") //比如lock_name为abc,这里计数器就是abc:count
return 1
else
return 0
end释放锁:减少重入计数,如果计数为 0,则删除键。
if redis.call("GET", lock_name) == client_id then
local count = redis.call("DECR", "lock_name:count")
if count == 0 then
redis.call("DEL", KEYS[1])
redis.call("DEL", "lock_name:count")
end
return 1
else
return 0
end回答#
可重入锁的核心是计数,即额外维护一个计数器,记录锁的重入次数,我们拿加锁流程举例,即先尝试Set命令(结合NX参数),如果不存在就Set成功,存在的话则查询是否是自己的id,是的话就增加计数,解锁也是一个思路,加锁解锁都需要用LUA保证原子性。

