热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Redis的Lua脚本编程的实现和应用

[TOC]相关命令EVALSCRIPT_LOADEVALSHA(执行之前要求执行过EVAL或者SCRIPT_LOAD)SCRIPTEXISTSSCRIPTFLUSH(慎用)SCRI

[TOC]

相关命令

  1. EVAL
  2. SCRIPT_LOAD
  3. EVALSHA(执行之前要求执行过EVAL或者SCRIPT_LOAD)
  4. SCRIPT EXISTS
  5. SCRIPT FLUSH(慎用)
  6. SCRIPT KILL(LUA的写操作务必谨慎,一旦有写入这个将失效效)

简介

  • Redis 服务器在启动时, 会对内嵌的 Lua 环境执行一系列修改操作, 从而确保内嵌的 Lua 环境可以满足 Redis 在功能性、安全性等方面的需要。
  • Redis 服务器专门使用一个伪客户端来执行 Lua 脚本中包含的 Redis 命令。
  • Redis 使用脚本字典来保存所有被 EVAL 命令执行过, 或者被 SCRIPT_LOAD 命令载入过的 Lua 脚本, 这些脚本可以用于实现 SCRIPT_EXISTS 命令, 以及实现脚本复制功能。
  • EVAL 命令为客户端输入的脚本在 Lua 环境中定义一个函数, 并通过调用这个函数来执行脚本。
  • EVALSHA 命令通过直接调用 Lua 环境中已定义的函数来执行脚本。
  • SCRIPT_FLUSH 命令会清空服务器 lua_scripts 字典中保存的脚本, 并重置 Lua 环境。
  • SCRIPT_EXISTS 命令接受一个或多个 SHA1 校验和为参数, 并通过检查 lua_scripts 字典来确认校验和对应的脚本是否存在。
  • SCRIPT_LOAD 命令接受一个 Lua 脚本为参数, 为该脚本在 Lua 环境中创建函数, 并将脚本保存到 lua_scripts 字典中。
  • 服务器在执行脚本之前, 会为 Lua 环境设置一个超时处理钩子, 当脚本出现超时运行情况时, 客户端可以通过向服务器发送 SCRIPT_KILL 命令来让钩子停止正在执行的脚本, 或者发送 SHUTDOWN nosave 命令来让钩子关闭整个服务器。
  • 主服务器复制 EVAL 、 SCRIPT_FLUSH 、 SCRIPT_LOAD 三个命令的方法和复制普通 Redis 命令一样 —— 只要将相同的命令传播给从服务器就可以了。
  • 主服务器在复制 EVALSHA 命令时, 必须确保所有从服务器都已经载入了 EVALSHA 命令指定的 SHA1 校验和所对应的 Lua 脚本, 如果不能确保这一点的话, 主服务器会将 EVALSHA 命令转换成等效的 EVAL 命令, 并通过传播 EVAL 命令来获得相同的脚本执行效果。

启动过程

  1. 创建并修改Lua环境
    1. 创建Lua环境-生成基本的Lua环境,接下来对Lua环境做进一步的修改

    2. 载入函数库

      1. 基础库
      2. 表格库:table library
      3. 字符串库:string.find、string.format、string.len、string.reverse
      4. 数学库
      5. 调试库
      6. Lua CJSON:用于处理UTF-8编码的JSON格式,其中方法 cjson.decode、cjson.encode
      7. Struct库:和c交互的库
      8. Lua cmsgpack库:用于处理MessagePack格式的数据,其中cmsgpack.pack行数将Lua值转换为MessagePack数据,而cmsgpack.unpack函数则将MessagePack数据转换为Lua值
    3. 创建redis全局表格

      1. 创建redis表格(table),并将它设置为全局变量
      2. redis.call、redis.pcall、redis.log、redis.sha1hex(计算sha1校验和)
      3. 用于返回错误信息的:redis.error_reply、redis.status_reply
    4. 修改可能产生不一致数据的命令和方法:保证脚本在不同机器上产生相同的结果,Redis要求所有传入服务器的Lua脚本,以及Lua环境中的的所有函数都是无副作用的纯函数。

      1. 替换Lua原有的随机函数
      2. 创建排序辅助函数
    5. 创建redis.pcall函数的错误报告辅助函数

    6. 保护Lua的全局环境

      1. 当脚本创建一个全局变量时,服务器会报告一个错误(保证不会因为忘记使用local关键字而将二外的全局变量添加到lua环境里面)
      2. 读取一个不存在的全局变量也会报错
      3. Redis没有禁止在脚本里修改全局变量,所以在执行Lua脚本的时候,必须小心防止错误修改已存在的全局变量
    7. 将Lua环境保存到服务器状态的lua属性里

      1. 因为Redis使用串行化的方式执行命令,所以在任何特定时间里,最多只会有一个脚本能够被放进Lua环境里面执行,因此整个Redis服务器只需要创建一个Lua环境即可
  2. 创建环境协作组件
    1. redis 伪客户端:伪客户端一直存在直到服务器关闭,执行命令的过程:
      1. 《Redis的Lua脚本编程的实现和应用》 image
    2. 保存传入服务器的Lua脚本的脚本字典:实现SCRIPT EXISTS 命令、实现脚本复制
      1. 《Redis的Lua脚本编程的实现和应用》 image

Redis Lua 的特点和注意事项

1. 特点

2. 注意事项

  1. Lua脚本的bug特别可怕,由于Redis的单线程特点,一旦Lua脚本出现不会返回(不是返回值)得问题,那么这个脚本就会阻塞整个redis实例。
  2. Lua脚本应该尽量短小实现关键步骤即可。(原因同上)
  3. Lua脚本中不应该出现常量Key,这样会导致每次执行时都会在脚本字典中新建一个条目,应该使用全局变量数组KEYS和ARGV
  4. KEYS和ARGV的索引都从1开始
  5. 传递给lua脚本的的键和参数:传递给lua脚本的键列表应该包括可能会读取或者写入的所有键。传入全部的键使得在使用各种分片或者集群技术时,其他软件可以在应用层检查所有的数据是不是都在同一个分片里面。另外集群版redis也会对将要访问的key进行检查,如果不在同一个服务器里面,那么redis将会返回一个错误。(决定使用集群版之前应该考虑业务拆分),参数列表无所谓。。
  6. lua脚本跟单个redis命令和事务段一样都是原子的
  7. 已经进行了数据写入的lua脚本将无法中断,只能使用SHUTDOWN NOSAVE杀死Redis服务器,所以lua脚本一定要测试好。

典型应用

1.分布式全局锁(distlock)

Yii2下的实现:


namespace yii\redis;
use Yii;
use yii\base\InvalidConfigException;
use yii\di\Instance;
//使用了Yii2互斥锁接口
class Mutex extends \yii\mutex\Mutex
{
//锁过期时间,秒
public $expire = 30;
public $keyPrefix;
public $redis = 'redis';
private $_lockValues = [];
public function init()
{
parent::init();
$this->redis = Instance::ensure($this->redis, Connection::className());
if ($this->keyPrefix === null) {
$this->keyPrefix = substr(md5(Yii::$app->id), 0, 5);
}
}
protected function acquireLock($name, $timeout = 0)
{
$key = $this->calculateKey($name);
$value = Yii::$app->security->generateRandomString(20);
$waitTime = 0;
//使用setnx(理解为多机版sem_acquire)命令获取锁并自动重试(这个锁支持获取超时和自动过期)
while (!$this->redis->executeCommand('SET', [$key, $value, 'NX', 'PX', (int) ($this->expire * 1000)])) {
$waitTime++;
//超时则直接返回获取失败
if ($waitTime > $timeout) {
return false;
}
sleep(1);
}
$this->_lockValues[$name] = $value;
return true;
}
protected function releaseLock($name)
{
//使用脚本最优化性能,如果不用脚本则需要使用事务段
static $releaseLuaScript = <<if redis.call("GET",KEYS[1])==ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
LUA;
if (!isset($this->_lockValues[$name]) || !$this->redis->executeCommand('EVAL', [
$releaseLuaScript,
1,
$this->calculateKey($name),
$this->_lockValues[$name]
])) {
return false;
} else {
unset($this->_lockValues[$name]);
return true;
}
}
protected function calculateKey($name)
{
return $this->keyPrefix . md5(json_encode([__CLASS__, $name]));
}
}

分析:

这个实现可以保证锁的互斥性(避免多个客户端同时获取锁)和超时性(避免资源一直处于锁定状态)

SET resource_name my_random_value NX PX 30000

这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样(保证释放资源的正确性)。

但是这个例子是在支持故障转移的主从结构中会存在竞态,下边是redis官方推荐的一个分布式锁算法RedLock,官方版分布式式锁算法实现

2.计数器信号量(counter semaphore)

几乎器也是一种锁,通常用于限制一项资源最多能够同时被多少个进程访问。

计数器信号量实现的功能(使用有序集合和时间戳分数处理计数器)

  • acquire

/*
** KEYS[1] 信号量键
** ARGV[1] 最小有效分数
** ARGV[2] 信号量最大计数值
** ARGV[3] 当前时间戳
** ARGV[4] 客户端uniqueId
*/
static $acquireLuaScript = <<--移除全部过期信号量
redis.call('zremrangebyscore', KEYS[1], '-inf', ARGV[1])
if redis.call('zcard', KEYS[1]) redis.call('zadd', KEYS[1], ARGV[3], ARGV[4])
return ARGV[4]
end
LUA;

  • reaease

zrem(key, clientId)

  • refresh(有时需要)

/*
** KEYS[1] 信号量键
** ARGV[1] 客户端uniqueId
** ARGV[2] 当前时间戳
*/
static $refreshLuaScript = <<--如果信号量仍然存在,那么对它的时间戳进行更新(通过zscore判断key存在与否)
if redis.call('zscore', KEYS[1], ARGV[1]) then
return redis.call('zadd', KEYS[1], ARGV[2], ARGV[1]) or true
end
LUA;

3.改造事务段

4.对已有结构进行分片,用来压缩占用空间

原文链接


推荐阅读
  • 本文讨论了微软的STL容器类是否线程安全。根据MSDN的回答,STL容器类包括vector、deque、list、queue、stack、priority_queue、valarray、map、hash_map、multimap、hash_multimap、set、hash_set、multiset、hash_multiset、basic_string和bitset。对于单个对象来说,多个线程同时读取是安全的。但如果一个线程正在写入一个对象,那么所有的读写操作都需要进行同步。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • redis知识汇总[随笔记录]
      ... [详细]
  • JavaScript实现拖动对话框效果
    原标题:JavaScript实现拖动对话框效果代码实现:<!DOCTYPEhtml><htmllan ... [详细]
  • 基于Redis实现分布式锁剖析
    之前的文章《分布式锁详解-分别利用Zookeeper和数据库实现分布式锁》,由于篇幅太长,又碰上加班时间不够充裕,所以没有把Redis的实 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • Openresty+Lua+Redis灰度发布
    Openresty+Lua+Redis灰度发布灰度发布,简单来说,就是根据各种条件,让一部分用户使用旧版本,另一部分用户使用新版本。百度百科中解释:灰度发布是指在黑与白之间,能够平 ... [详细]
author-avatar
手机用户2502913623
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有