Redis应用 - Redis 面试题及答案
Redis应用
40.使用Redis 如何实现异步队列?
我们知道redis支持很多种结构的数据,那么如何使用redis作为异步队列使用呢?
一般有以下几种方式:
- 使用list作为队列,lpush生产消息,rpop消费消息
这种方式,消费者死循环rpop从队列中消费消息。但是这样,即使队列里没有消息,也会进行rpop,会导致Redis CPU的消耗。
可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。
-使用list作为队列,lpush生产消息,brpop消费消息
brpop是rpop的阻塞版本,list为空的时候,它会一直阻塞,直到list中有值或者超时。
这种方式只能实现一对一的消息队列。
- 使用Redis的pub/sub来进行消息的发布/订阅
发布/订阅模式可以1:N的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。

但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。
所以,一般的异步队列的实现还是交给专业的消息队列。
41.Redis 如何实现延时队列?
- 使用zset,利用排序实现
可以使用 zset这个结构,用设置好的时间戳作为score进行排序,使用 zadd score1 value1 ....命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。
42.Redis 支持事务吗?
Redis提供了简单的事务,但它对事务ACID的支持并不完备。
multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0
127.0.0.1:6379> exec 1) (integer) 1
1) (integer) 1
Redis事务的原理,是所有的指令在 exec 之前不执行,而是缓存在
服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。
因为Redis执行命令是单线程的,所以这组命令顺序执行,而且不会被其它线程打断。
Redis事务的注意点有哪些?
需要注意的点有:
Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
Redis 事务为什么不支持回滚?
Redis 的事务不支持回滚。
如果执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。
这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
43.Redis和Lua脚本的使用了解吗?
Redis的事务功能比较简单,平时的开发中,可以利用Lua脚本来增强Redis的命令。
Lua脚本能给开发人员带来这些好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
比如这一段很(烂)经(大)典(街)的秒杀系统利用lua扣减Redis库存的脚本:
-- 库存未预热
if (redis.call('exists', KEYS[2]) == 1) then
return -9;
end;
-- 秒杀商品库存存在
if (redis.call('exists', KEYS[1]) == 1) then
local stock = tonumber(redis.call('get', KEYS[1]));
local num = tonumber(ARGV[1]);
-- 剩余库存少于请求数量
if (stock < num) then
return -3
end;
-- 扣减库存
if (stock >= num) then
redis.call('incrby', KEYS[1], 0 - num);
-- 扣减成功
return 1
end;
return -2;
end;
-- 秒杀商品库存不存在
return -1;
44.Redis的管道了解吗?
Redis 提供三种将客户端多条命令打包发送给服务端执行的方式:
Pipelining(管道) 、 Transactions(事务) 和 Lua Scripts(Lua 脚本) 。
Pipelining(管道)
Redis 管道是三者之中最简单的,当客户端需要执行多条 redis 命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。
Pipelining的优势
在性能方面, Pipelining 有下面两个优势:
- 节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
- 减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。
45.Redis实现分布式锁了解吗?
Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
- V1:setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。
> setnx lock:fighter true
OK
... do something critical ...
> del lock:fighter
(integer) 1
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
- V2:锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
> setnx lock:fighter true
OK
> expire lock:fighter 5
... do something critical ...
> del lock:fighter
(integer) 1
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
- V3:set指令
这个问题在Redis 2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行。
set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。
当然实际的开发,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。
最近建一些几十个工作内推群,各大城市都有,群里目前已经收集了很多内推岗位,大厂、中厂、小厂、外包都有。 欢迎HR、开发、测试、运维和产品加入。

扫描下方微信,备注:网站+所在城市,即可拉你进工作内推群。
