Skip to content

《面渣逆袭》Redis 篇 · 第 6/8 章。原版 PDF(下载 / 打印)

40.使用Redis 如何实现异步队列?

我们知道redis 支持很多种结构的数据,那么如何使用redis 作为异步队列使用呢?

一般有以下几种方式:

使用list 作为队列,lpush 生产消息,rpop 消费消息

这种方式,消费者死循环rpop 从队列中消费消息。但是这样,即使队列里没有消息,也会进行rpop ,会导致RedisCPU 的消耗。

配图

可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。

-使用list 作为队列,lpush 生产消息,brpop 消费消息

brpop 是rpop 的阻塞版本,list 为空的时候,它会一直阻塞,直到list 中有值或者超时。

配图

这种方式只能实现一对一的消息队列。

使用Redis 的p ub/sub 来进行消息的发布/ 订阅

发布/ 订阅模式可以1 :N 的消息发布/ 订阅。发布者将消息发布到指定的频道频道(channel ),订阅相应频道的客户端都能收到消息。

配图

但是这种方式不是可靠的,它不保证订阅者一定能收到消息,也不进行消息的存储。

所以,一般的异步队列的实现还是交给专业的消息队列。

41.Redis 如何实现延时队列?

使用zset ,利用排序实现

可以使用 zset 这个结构,用设置好的时间戳作为score 进行排序,使用 zaddscore1value1.... 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务,通过循环执行队列任务即可。

配图

42.Redis 支持事务吗?

Redis 提供了简单的事务,但它对事务ACID 的支持并不完备。

multi 命令代表事务开始,exec 命令代表事务结束,它们之间的命令是原子顺序执行的:

127.0.0.1:6379>multi

OK 127.0.0.1:6379>sadduser🅰️followuser:b

QUEUED 127.0.0.1:6379>sadduser🅱️fansuser:a

QUEUED 127.0.0.1:6379>sismemberuser🅰️followuser:b

(integer)0

127.0.0.1:6379> exec 1) (integer) 1
2) (integer) 1

Redis 事务的原理,是所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。

配图

因为Redis 执行命令是单线程的,所以这组命令顺序执行,而且不会被其它线程打断。

Redis 事务的注意点有哪些?

需要注意的点有:

Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;

Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。

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

Redis 的事务不支持回滚。

如果执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。

这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性

43.Redis和Lua脚本的使用了解吗?

Redis 的事务功能比较简单,平时的开发中,可以利用L ua 脚本来增强Redis 的命令。

Lua 脚本能给开发人员带来这些好处:

Lua 脚本在Redis 中是原子执行的,执行过程中间不会插入其他命令。

Lua 脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这 些命令常驻在Redis 内存中,实现复用的效果。

Lua 脚本可以将多条命令一次性打包,有效地减少网络开销。

比如这一段很(烂)经(大)典(街)的秒杀系统利用l ua 扣减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(RoundTripTime) 对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis 服务端。

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

配图

Pipelining 的优势

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

节省了RT T:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数

减少了上下文切换:当客户端/ 服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。

当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。

45.Redis实现分布式锁了解吗?

Redis 是分布式锁本质上要实现的目标就是在 Redis 里面占一个“ 茅坑” ,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

V1 :setnx 命令

占坑一般是使用 setnx(setifnotexists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。

配图

setnxlock:fightertrue

OK ...dosomethingcritical...>dellock:fighter

(integer)1但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

V2: 锁超时释放

所以在拿到锁之后,再给锁加上一个过期时间,比如 5s ,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。

配图

setnxlock:fightertrue

OK >expirelock:fighter5 ...dosomethingcritical...>dellock:fighter

(integer)1但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。

V3:set 指令

这个问题在Redis2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和

expire 指令可以一起执行。

配图

setlock:fighter3trueex5nxOK...dosomethingcritical...>dellock:codehole上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。

当然实际的开发,没人会去自己写分布式锁的命令,因为有专业的轮子— —Redisson

本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传