vlambda博客
学习文章列表

Redis专题3:发布订阅模式、事务、Lua脚本揭秘

Redis的原理篇

发布订阅模式

列表的局限性

当我们通过队列的rpush和lpop可以实现消息队列(队尾进队头出),但是消费者需要不停的调用lpop查看list中是否有等待处理的消息(比如写一个while循环)。为了减少通信的消耗,可以sleep()一段时间再消费,但是会有两个问题:

  • 如果生产者消息的速度远大于消费者消息的速度,List会占用大量的内存。
  • 消息的实时性降低

list还提供了一个阻塞命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞。

blpop queue 5

基于list实现的消息队列,不支持一对多的消息分发

发布订阅模式

除了通过list 实现消息队列之外,Redis还提供了一组命令实现发布订阅模式

这种方法,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝试获取消息。

订阅频道

首先,我们会有很多的频道(channel),我们也可以把这个频道理解成queue。订阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。只要有消息达到了频道,所有订阅了这个频道的订阅者都会收到这条消息。

需要注意的是,发出去的消息不会被持久化,因为它已经从队列里面移除了,所以消费者只能收到它开始订阅这个频道之后发布的消息。

「发布订阅命令的使用方法」

订阅者订阅频道:可以一次订阅多个,比如这个客户端订阅了3个频道

subscribe channel-1 channel-2 channel-3

发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息):

publish channel-1 2673

取消订阅

unsubscribe channel-1

按照规则订阅频道

支持?和*占位符。?代表一个字符, * 代表0个或者多个字符。

例如:

消费端1,关注运动消息:

psubscribe *sport

消费端2,关注所有的新闻

psubscribe news*

消费端3,关注天气新闻:

psubscribe news-weather

生产者,发布3条消息

publish news-sport yaomingpublish news-music zhoujielunpublish news-weather rain


事务

https://redis.io/topics/transactions/

http://redisdoc.com/topic/transaction.html/

为什么要使用事务

我们知道Redis的单个命令是原子性的,如果涉及到多个命令的时候,需要把多个命令作为一个不可分割的处理序列,就需要用到事务。

例如,setnx 实现分布式锁,我们先set 然后对key 设置expire,防止del 发生异常的时候锁不会被释放,业务处理完了以后再del,这三个动作我们希望他们作为一组命令执行:

Redis 的事务有两个特点:

  • 按照进入队列的顺序执行
  • 不会受到其他客户端的请求的影响,

Redis 的事务涉及到四个命令:multi(开启事务),exec(实行事务),discard(取消事务),watch(监视)

用法

例如,A和B各有1000元,A向B转账100元

set A 1000set B 1000
multidecrby A 100incrby B 100exec
get Aget B

通过multi 命令开启事务,事务不能嵌套,多个multi 命令效果是一样的

multi 执行后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当exec 命令被调用时,所有队列中的命令才会被执行。

通过 exec的命令执行事务,如果没有执行exec,所有命令都不会被执行。

如果中途,不想执行事务了,怎么办?

可以调用 discard 可以清空事务队里额,放弃执行。

multiset k1 1set k2 2set k3 3discard

watch 命令

在Redis 中还提供了一个watch 命令,他可以为Redis 事务提供CAS 乐观锁行为(Check and Set/ Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修改的情况下,才更新成新的值。

我们可以用watch 监视一个或者多个key,如果开启事务之后,至少有一个被监视key键在exec 执行之前被修改了,那么整个事务都会被取消(key 提前过期除外)。可以用unwatch 取消。

例如:

clientA clientB
set A 1000
watch A
multi
incrby

decrby A 100

输出 900
exec
get A
此时事务被取消,输出也是:900

事务可能遇到的问题

我们把事务执行遇到的问题分成两种,一种是在执行exec 之前发生的错误,一种是在执行exec 之后发生错误。

  • 执行exec 之前发生错误

例如:入队的命令存在语法错误,包括参数数量,参数名等等

Redis专题3:发布订阅模式、事务、Lua脚本揭秘


在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。

  • 在执行exec之后发生错误

比如类型错误,比如对String 使用Hash的命令,这是一种运行时错误。

Redis专题3:发布订阅模式、事务、Lua脚本揭秘


最后我们发现,set A 1000 居然是成功的,也就是在这种发生了运行时错误的情况下,只有错误的命令没有被执行,但是其他命令没有收到影响。

Lua 脚本

Lua 是一种轻量级的脚本语言,他是用C语言编写的,跟数据的存储过程有点类似。使用Lua 脚本 来执行Redis 命令的好处:

  • 一次发送多个命令,减少网络开销。
  • Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
  • 对与复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。

语法命令

使用 eval 方法 ,语法格式

Redis专题3:发布订阅模式、事务、Lua脚本揭秘


  • eval 代表执行Lua语言的命令
  • script 代表Lua语言脚本内容
  • numkeys 表示参数中有多少个key,需要注意的是Redis 中key 是从1 开始的,如果没有参数,那么写0
  • [key ……] 是key作为参数传递给 Lua 语言,可以不填,但是需要 和 numkeys 对应起来
  • [arg ……] 这些参数传递给Lua ,他们可以不填

例如:

Redis专题3:发布订阅模式、事务、Lua脚本揭秘


设置键值对

在Redis 中 调用 Lua 脚本执行Redis 命令


在Redis 中 直接写Lua 脚本不方便,也不能实现编辑和复用,通常我们会把脚本放到文件里面,然后执行这个命令。

在Redis 中调用Lua 脚本文件中的命令

创建 Lua 脚本文件

cd /usr/data/redisvim test.lua

Lua 脚本内容

redis.call('set','A','lua22222')return redis.call('get','A')

在Redis客户端中调用Lua脚本

redis-cli --eval test.lua 0

案例:IP限流

需求:在X秒内只能访问Y次

设计思路:用key记录IP,用value 记录访问次数

拿到IP以后,对IP+1,如果是第一次访问,对key设置过期时间(参数1)。否则判断次数,超过限定的次数(参数2),返回0,。如果没有超过次数则返回1。超过时间,key过期以后,可以再次访问。

KEY[1] 是 IP,ARGV[1]是过期时间,ARGV[2]是限制访问的次数Y

-- ip_limit.lua-- IP限流,对某个IP频率进行限制local num = redis.call('incr',KEYS[1])if tonumber(num) == 1 then redis.call('expire',KEYS[1],KEYS[1]) return 1elseif tonumber(num) > tonumber(ARGV[2]) then return 0else  return 1end

6 秒钟内限制访问10 次,

./redis-cli --eval 'ip_limit.lua' app:ip:limit:192.168.1.111 , 6 10
  • app:ip:limit:192.168.1.111 是key 值,后面是参数值,中间加上一个空格和一个逗号,再加上一个空格
  • 多个参数之间用空格分割。

缓存Lua 脚本

在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生较大的网络开销。为了解决这个问题,Redis 提供了EVALSHA 命令,允许开发者通过脚本内容的SHA1摘要来执行脚本。

「如何缓存」

Redis 在执行 script load 命令会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了执行脚本,否则返回错误:“NOSCRIPT No mathing script. Please use EVAL”

脚本超时

Reids 的指令执行本身时单线程的,这个线程还要执行客户端的Lua 脚本,如果Lua 脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?

为了防止某个脚本执行时间超长导致Redis 无法提供服务,Redis 提供了 lua-time-limit 限制脚本最长运行时间,默认为5秒钟

  • lua-time-limit 5000 (在redis.conf配置文件中)

当脚本运行时间超过这个限制之后,Redis 将开始接受其他命令但是不会执行(以确保脚本的原子性,因为此脚本并没有终止),而是会返回“BUSY”错误。

Redis 提供了一个script skill 的命令来终止脚本的执行,新开一个客户端。

script skill

如果当前执行的Lua 脚本对Redis的数据进行了修改,那么通过script skill 命令是不能终止脚本的执行的。

因为要确保脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。