架构师-Redis(二)

Lua脚本

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

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

redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]

在 Redis 中调用 Lua 脚本

  • eval 代表执行 Lua 语言的命令。
  • lua-script 代表 Lua 语言脚本内容。
  • key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。
  • [key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。
  • [value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的
redis> eval "return 'Hello World'" 0

使用 redis.call(command, key [param1, param2…])进行操作

  • command 是命令,包括 set、get、del 等。
  • key 是被操作的键。
  • param1,param2…代表给 key 的参数。
127.0.0.1:6379> set key1 3
OK
127.0.0.1:6379> set key2 0
OK
127.0.0.1:6379> eval "local v1 = redis.call('get',KEYS[1]); return redis.call('incrby',KEYS[2],v1);" 2 key1 key2
(integer) 3
127.0.0.1:6379>

案例:对IP进行限流

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

6 秒钟内限制访问 10 次,调用测试(连续调用 10 次)

./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 6 10

缓存Lua脚本

  • 为什么要缓存

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

  • 如何缓存

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

    127.0.0.1:6379> script load "return 'Hello World'" 
    "470877a599ac74fbfda41caa908de682c5fc7d4b" 
    127.0.0.1:6379> evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b" 0 
    "Hello World"
    
  • 自乘案例

    Redis 有 incrby 这样的自增命令,但是没有自乘,比如乘以 3,乘以 5。 我们可以写一个自乘的运算,让它乘以后面的参数:

    local curVal = redis.call("get", KEYS[1]) 
    if curVal == false then 
    	curVal = 0 
    else 
    	curVal = tonumber(curVal) 
    end 
    curVal = curVal * tonumber(ARGV[1]) 
    redis.call("set", KEYS[1], curVal) 
    return curVal
    

    把这个脚本变成单行,语句之间使用分号隔开,script load ‘命令’

    127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
    "be4f93d8a5379e5e5b768a74e77c8a4eb0434441"
    

    调用

    127.0.0.1:6379> set num 2 
    OK 
    127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6 
    (integer) 12
    

脚本超时

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

eval 'while(true) do end' 0

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

# redis.conf
################################ LUA SCRIPTING  ###############################

# Max execution time of a Lua script in milliseconds.
#
# If the maximum execution time is reached Redis will log that a script is
# still in execution after the maximum allowed time and will start to
# reply to queries with an error.
#
# When a long running script exceeds the maximum execution time only the
# SCRIPT KILL and SHUTDOWN NOSAVE commands are available. The first can be
# used to stop a script that did not yet called write commands. The second
# is the only way to shut down the server in the case a write command was
# already issued by the script but the user doesn't want to wait for the natural
# termination of the script.
#
# Set it to 0 or a negative value for unlimited execution without warnings.
lua-time-limit 5000

当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚 本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。 Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端:script kill

如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的。

127.0.0.1:6379> eval "redis.call('set','gupao','666') while true do end" 0 

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

127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

遇到这种情况,只能通过 shutdown nosave 命令来强行终止 redis。 shutdown nosaveshutdown 的区别在于 shutdown nosave 不会进行持久化 操作,意味着发生在上一次快照后的数据库修改都会丢失

如果我们有一些特殊的需求,可以用 Lua 来实现,但是要注意那些耗时的操作

lua源码

struct redisServer {
   
...
 /* Scripting */
 	// lua解释器,所有客户端公用
    lua_State *lua; /* The Lua interpreter. We use just one for all clients */
    // lua中向Redis查询的“伪客户端”
    client *lua_client;   /* The "fake client" to query Redis from Lua */
    // 正在执行脚本调用的客户端
    client *lua_caller;   /* The client running EVAL right now, or NULL */
    // sha1 和原始脚本的字典映射
    dict *lua_scripts;         /* A dictionary of SHA1 -> Lua scripts */
    // 缓存脚本使用的内存,单位:字节
    unsigned long long lua_scripts_mem;  /* Cached scripts' memory + oh */
    // 脚本超时,单位 毫秒
    mstime_t lua_time_limit;  /* Script timeout in milliseconds */
    // 脚本启动时间
    mstime_t lua_time_start;  /* Start time of script, milliseconds time */
    // 脚本执行期间有调用写命令,则为true
    int lua_write_dirty;  /* True if a write command was called during the
                             execution of the current script. */
    // 脚本执行期间有调用随机命令                         
    int lua_random_dirty; /* True if a random command was called during the
                             execution of the current script. */
    // 如果是脚本效果复制,则为true                         
    int lua_replicate_commands; /* True if we are doing single commands repl. */
    // 如果是传播事务,则为true
    int lua_multi_emitted;/* True if we already proagated MULTI. */
    // 脚本复制标志
    int lua_repl;         /* Script replication flags for redis.set_repl(). */
    // 脚本执行超时,则为true
    int lua_timedout;     /* True if we reached the time limit for script
                             execution. */
    // 杀死脚本,则为true                         
    int lua_kill;         /* Kill the script if true. */
    // 默认复制类型
    int lua_always_replicate_commands; /* Default replication type. */
};

scripting.c
在Redis服务初始化程序initServer 调用 脚本初始化函数scriptingInit
redis.call具体执行函数 luaRedisGenericCommand

发布与订阅

通过列表实现消息队列局限
通过队列的 rpush 和 lpop 可以实现消息队列(队尾进队头出),但是消 费者需要不停地调用 lpop 查看 List 中是否有等待处理的消息(比如写一个 while 循环)。 为了减少通信的消耗,可以 sleep()一段时间再消费,但是会有两个问题
1、如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内 存。
2、消息的实时性降低。
list 还提供了一个阻塞的命令:blpop,没有任何元素可以弹出的时候,连接会被阻塞
基于list 实现的消息队列,不支持一对多的消息分发

发布订阅模式
除了通过 list 实现消息队列之外,Redis 还提供了一组命令实现发布/订阅模式。 这种方式,发送者和接收者没有直接关联(实现了解耦),接收者也不需要持续尝 试获取消息
首先,我们有很多的频道(channel),我们也可以把这个频道理解成 queue。订 阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发布消息。 只要有消息到达了频道,所有订阅了这个频道的订阅者都会收到这条消息。

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

# 调阅三个频道
subscribe channel-1 channel-2 channel-3
# 发布消息
publish channel-1 111
# 需要订阅(不能在订阅状态使用)
unsubscribe channel-1
# 按pattern订阅 支持?和*
psubscribe *sport
psubscribe news*
# 生成者
publish news-sport yaoming
publish news-music jaychou

相关源码

struct redisServer{
   
	...
    /* Pubsub */
    // key为channel值,value为一个个clients
    dict *pubsub_channels;  /* Map channels to list of subscribed clients */
    // 链表,节点值为一个个pubsubPattern
    list *pubsub_patterns;  /* A list of pubsub_patterns */
    int notify_keyspace_events; /* Events to propagate via Pub/Sub. This is an
                                   xor of NOTIFY_... flags. */
  ...                                  
}
typedef struct pubsubPattern {
   
    client *client; // 订阅该模式的客户端
    robj *pattern; // 模式结构体
} pubsubPattern;

typedef struct client {
   
	... 
	    int flags;              /* Client flags: CLIENT_* macros. */
		...
	   dict *pubsub_channels;  /* channels a client is interested in (SUBSCRIBE) */
    list *pubsub_patterns;  /* patterns a client is interested in (SUBSCRIBE) */
	...
}

pubsub.c

/* Publish a message */
int pubsubPublishMessage(robj *channel, robj *message) {
   
    ...
    /* Send to clients listening for that channel */
    // 从pubsub_channels取出调阅该channel的客户端
    de = dictFind(server.pubsub_channels,channel);
    // 如果有订阅该channel的客户端,依次向客户端发送该消息
    if (de) {
   
        list *list = dictGetVal(de);
        listNode *ln;
        listIter li;

        listRewind(list,&li);
        while ((ln = listNext(&li)) != NULL) {
   
            client *c = ln->value;

            addReply(c,shared.mbulkhdr[3]);
            addReply(c,shared.messagebulk);
            addReplyBulk(c,channel);
            addReplyBulk(c,message);
            receivers++;
        }
    }
    /* Send to clients listening to matching channels */
    if (listLength(server.pubsub_patterns)
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值