使用Redis实现分布式锁
1、redis命令行执行lua脚本
01 、redis.call() 和 redis.pcall() 两个函数的参数可以是任意的 redis 命令
127.0.0.1:6379> eval "return redis.call('set','name','tinywan')" 0
ok
127.0.0.1:6379> get name
"tinywan"
说明:eval 和 evalsha 命令是从 redis 2.6.0 版本开始的,使用内置的 lua 解释器,可以对 lua 脚本进行求值。
eval的第一个参数是一段 lua 5.1 脚本程序。 这段lua脚本不需要(也不应该)定义函数。它运行在 redis 服务器中。
eval的第二个参数是参数的个数,后面的参数(从第三个参数),表示在脚本中所用到的那些 redis 键(key),这些键名参数可以在 lua 中通过全局变量 keys 数组,用 1 为基址的形式访问( keys[1] , keys[2] ,以此类推)。
在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在 lua 中通过全局变量 argv 数组访问,访问的形式和 keys 变量类似( argv[1] 、 argv[2] ,诸如此类)。
02、使用keys和argv
127.0.0.1:6379> eval "return redis.call('set',keys[2],argv[3])" 3 name01 name02 name03 tinywan01 tinywan02 tinywan03
ok
127.0.0.1:6379> keys *
1) "name02"
127.0.0.1:6379> get name02
"tinywan03"
说明:返回结果是redis multi bulk replies的lua数组,这是一个redis的返回类型,您的客户端库可能会将他们转换成数组类型。
03 、redis.call() 和 redis.pcall() 的区别
redis.call() 执行一个不存在的redis命令: setngx
127.0.0.1:6379> eval "redis.call('setngx',keys[1],argv[1]);redis.call('set',keys[3],argv[1])" 3 name01 name02 name03 tinywan01 tinywan02 tinywan03
(error) err error running script (call to f_0adfcdb3f740b2aabfe19f0e80de7cda7ce6262f): @user_script:1: @user_script: 1: unknown redis command called from lua script
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
说明:当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因。由于第一个执行错误,导致后面的也没有执行,设置不成功。
redis.pcall() 执行一个不存在的redis命令: setngx
127.0.0.1:6379> eval "redis.pcall('setngx',keys[1],argv[1]);redis.call('set',keys[3],argv[1])" 3 name01 name02 name03 tinywan01 tinywan02 tinywan03
(nil)
127.0.0.1:6379> keys *
1) "name03"
127.0.0.1:6379> get name03
"tinywan01"
127.0.0.1:6379>
说明: redis.pcall() 出错时并不引发(raise)错误,而是返回一个 nil,后面的命令任然可以执行成功。
redis.call() 与 redis.pcall()很类似, 他们唯一的区别是当redis命令执行结果返回错误时, redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以lua表的形式返回。
redis.call() 和 redis.pcall() 两个函数的参数可以是任意的 redis 命令
2、redis中lua脚本命令介绍
01、script 命令
命令用于将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本。
127.0.0.1:6379> script load "return redis.call('set',keys[1],argv[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
02、evalsha 命令
根据给定的 sha1 校验码(也就是 script load 执行脚本生成的哈希值),对缓存在服务器中的脚本进行求值。 将脚本缓存到服务器的操作可以通过 script load 命令进行。
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 github blog github.tinywan blog.tinywan
ok
127.0.0.1:6379> keys *
1) "github"
2) "name03"
127.0.0.1:6379> get github
"github.tinywan"
03、script flush 命令
清空lua脚本缓存 flush the lua scripts cache.
127.0.0.1:6379> script flush
ok
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 github blog github.tinywan blog.tinywan
(error) noscript no matching script. please use eval.
127.0.0.1:6379> script load "return redis.call('set',keys[1],argv[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 2 github blog github.tinywan blog.tinywan
ok
127.0.0.1:6379>
04、script exists
命令用于校验指定的脚本是否已经被保存在缓存当中
127.0.0.1:6379> script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d
1) (integer) 1
127.0.0.1:6379> script flush
ok
127.0.0.1:6379> script exists c686f316aaf1eb01d5a4de1b0b63cd233010e63d
1) (integer) 0
127.0.0.1:6379>
05、script kill
杀死当前正在运行的 lua 脚本
3、调试
script.lua脚本
local foo = redis.call("ping")
return foo
执行脚本
$ redis-cli --eval script.lua
pong
loop.lua脚本
local i = 0
while true do
i = i + 1
redis.debug(i)
end
return "ok"
进入调试模式
$ redis-cli --ldb --eval loop.lua set set , wet set
lua debugging session started, please use:
quit -- end the session.
restart -- restart the script in debug mode again.
help -- show lua script debugging commands.
* stopped at 1, stop reason = step over
-> 1 local i = 0
^c
打开另外一个shell窗口
www@iz2zec3dge6rwz2uw4tveuz:~$ redis-cli
127.0.0.1:6379> keys *
(error) busy redis is busy running a script. you can only call script kill or shutdown nosave.
127.0.0.1:6379> keys *
(error) busy redis is busy running a script. you can only call script kill or shutdown nosave.
127.0.0.1:6379> keys *
(error) busy redis is busy running a script. you can only call script kill or shutdown nosave.
127.0.0.1:6379> script kill
ok
127.0.0.1:6379> keys *
1) "redis_cache:resty_vod_detail:55"
4、实现分布式锁
使用php实现分布式锁
<?php
/**.-------------------------------------------------------------------------------------------------------------------
* | github: https://github.com/tinywan
* | blog: http://www.cnblogs.com/tinywan
* |--------------------------------------------------------------------------------------------------------------------
* | author: tinywan(shaobo wan)
* | datetime: 2018/9/13 22:28
* | mail: 756684177@qq.com
* | desc: 使用redis实现分布式锁
* '------------------------------------------------------------------------------------------------------------------*/
class redislock
{
/**
* 获取锁
* @param string $lock_name 锁名
* @param int $acquire_time 重复请求次数
* @param int $lock_timeout 请求超时时间
* @return bool|string
*/
public static function acquirelock($lock_name, $acquire_time = 3, $lock_timeout = 120)
{
$identifier = md5($_server['request_time'] . mt_rand(1, 10000000));
$lock_name = 'lock:' . $lock_name;
$lock_timeout = intval(ceil($lock_timeout));
$end_time = time() + $acquire_time;
while (time() < $end_time) {
$script = <<<luascript
local result = redis.call('setnx',keys[1],argv[1]);
if result == 1 then
redis.call('expire',keys[1],argv[2])
return 1
elseif redis.call('ttl',keys[1]) == -1 then
redis.call('expire',keys[1],argv[2])
return 0
end
return 0
luascript;
$result = location_redis()->evaluate($script, array($lock_name, $identifier, $lock_timeout), 1);
if ($result == '1') {
return $identifier;
}
usleep(100000); // 函数延迟代码执行若干微秒
}
return false;
}
/**
* 释放锁
* @param string $lock_name 锁名
* @param string $identifier 获取锁返回的标识
* @return bool
*/
public static function releaselock($lock_name, $identifier)
{
$lock_name = 'lock:' . $lock_name;
while (true) {
$script = <<<luascript
local result = redis.call('get',keys[1]);
if result == argv[1] then
if redis.call('del',keys[1]) == 1 then
return 1;
end
end
return 0
luascript;
$result = location_redis()->evaluate($script, array($lock_name, $identifier), 1);
if ($result == 1) {
return true;
}
break;
}
//进程已经失去了锁
return false;
}
}