[toc]html
主要用到的语法: 注释,变量,方法调用和声明,循环,流程控制python
下载安装:http://luabinaries.sourceforg...
IDE编辑器:Settings -> Plugins -> Marketplace -> 搜索并安装EmmyLuanginx
使用EVAL命令对 Lua 脚本进行求值redis
EVAL script numkeys key [key ...] arg [arg ...]json
[info] numkeys : keys的数量有几个。这是一个必传的参数,即便没有keys也要传个0;
# 注意redis的计数是从1开始的 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
EVAL命令会将脚本添加到脚本缓存中,而且会当即对输入的脚本进行求值。缓存
若是给定的脚本已经在缓存里面了,那么不作动做。服务器
在脚本被加入到缓存以后,经过 EVALSHA 命令,可使用脚本的 SHA1 校验和来调用这个脚本。网络
脚本能够在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。多线程
redis> SCRIPT LOAD "return 'hello moto'" "232fd51614574cf0867b83d384a5e898cfd24e5a" # 判断脚本是否存在 redis> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 1) (integer) 1 redis> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0 "hello moto" # 清空缓存 redis> SCRIPT FLUSH OK
在 Lua 脚本中,可使用两个不一样函数来执行 Redis 命令,它们分别是:并发
# 0表示没有keys > eval "return redis.call('set','foo','bar')" 0 OK # 以参数的形式传入 > eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
redis.call()和redis.pcall()的惟一区别在于它们对错误处理的不一样。redis.pcall()出错时并不引起(raise)错误,而是返回一个带err域的 Lua 表(table) ,用于表示错误:
redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0 (error) ERR Operation against a key holding the wrong kind of value
Redis 内置的 Lua 解释器加载了如下 Lua 库:
其中cjson库可让 Lua 以很是快的速度处理 JSON 数据,除此以外,其余别的都是 Lua 的标准库。
每一个 Redis 实例都保证会加载上面列举的库,从而确保每一个 Redis 脚本的运行环境都是相同的。
为了防止没必要要的数据泄漏进 Lua 环境, Redis 脚本不容许建立全局变量。若是一个脚本须要在屡次执行之间维持某种状态,它应该使用 Redis key 来进行状态保存。
redis 127.0.0.1:6379> eval 'a=10' 0 (error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'
在 Lua 脚本中,能够经过调用redis.log函数来写 Redis 日志(log):
redis.log(loglevel,message)
其中,message参数是一个字符串,而loglevel参数能够是如下任意一个值:
打印的日志在redis日志文件中,redis的日志文件能够在其配置里面找logfile。默认是没有的。redis必须带配置文件启动,若是直接启动的话,它会使用默认配置(并且并不存在这个默认配置文件,因此不要想改它)。
[root@test-02 bin]# cat test.lua return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]} # 经过逗号来分割key和arg,注意,这个逗号必须先后要有空格 [root@test-02 bin]# ./redis-cli --eval test.lua key1 key2 , first second 1) "key1" 2) "key2" 3) "first" 4) "second"
[info] 注意: 逗号先后必需要有空格。
$redis->eval($lua,array('key1','key2','first','second'),2)
$lua = <<<SCRIPT return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]} SCRIPT; //对应的redis命令以下 eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second $s = $redis->eval($lua,array('key1','key2','first','second'),2);
10秒内只能访问3次。 后续该脚本能够在nginx或者程序运行脚本中直接使用,判断返回是否为0,就0就不让其继续访问。
-- redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3 -- rate.limitingl + 1 local times = redis.call('incr',KEYS[1]) -- 第一次访问的时候加上过时时间10秒(10秒事后重新计数) if times == 1 then redis.call('expire',KEYS[1], ARGV[1]) end -- 注意,从redis进来的默认为字符串,lua同种数据类型只能和同种数据类型比较 if times > tonumber(ARGV[2]) then return 0 end return 1
以上,若是不使用redis+lua,那高并发下incr和expire就会出现原子性破坏,形成expire执行屡次浪费
Zset 里面存储的是 Value/Score 键值对,咱们将 Value 存储为序列化的任务消息,Score 存储为下一次任务消息运行的时间(Deadline),而后轮询 Zset 中 Score 值大于 Now 的任务消息进行处理。
# 生产延时消息 zadd(queue-key, now_ts+5, task_json) # 消费延时消息 while True: task_json = zrevrangebyscore(queue-key, now_ts, 0, 0, 1) if task_json: grabbed_ok = zrem(queue-key, task_json) if grabbed_ok: process_task(task_json) else: sleep(1000) // 歇 1s
当消费者是多线程或者多进程的时候,这里会存在竞争浪费问题。当前线程明明将 task_json 从 Zset 中轮询出来了,可是经过 Zrem 来争抢时却抢不到手。
这时就可使用 LUA 脚原本解决这个问题,将轮询和争抢操做原子化,这样就能够避免竞争浪费。
local res = nil local tasks = redis.pcall("zrevrangebyscore", KEYS[1], ARGV[1], 0, "LIMIT", 0, 1) if #tasks > 0 then local ok = redis.pcall("zrem", KEYS[1], tasks[1]) if ok > 0 then res = tasks[1] end end return res
local key = KEYS[1] local id = redis.call('get',key) if(id == false) then redis.call('set',key,1) return key.."0001" else redis.call('set',key,id+1) return key..string.format('%04d',id + 1) end
经过lua使get和set命令原子化,杜绝高并发下的
业务需求: 每次只容许领取10个红包
操做流程:判断是否能抢->抢到红包->记录抢到红包的人->异步发红包
解决问题:高并发下的红包超发(或者商品超卖),判断可否抢和抢必定要原子性的捆绑在一块儿,不然就会出现超发
-- 抢红包脚本 --[[ --red:list 为 List 结构,存放预先生成的红包金额 red:draw_count:u:openid 为 k-v 结构,用户领取红包计数器 red:draw为 Hash 结构,存放红包领取记录 red:task 也为 List 结构,红包异步发放队列 openid 为用户的openid ]]-- local openid = KEYS[1] local isDraw = redis.call("HEXISTS","red:draw",openid) -- 已经领取 if isDraw ~= 0 then return true end -- 领取太屡次了 local times = redis.call("INCR","red:draw_count:u:"..openid) if times and tonumber(times) > 9 then return 0 end local number = redis.call("RPOP","red:list") -- 没有红包 if not number then return {} end -- 领取人昵称为Fhb,头像为 https:// xxxxxx local red = {money=number,name=KEYS[2] , pic = KEYS[3] } -- 领取记录 redis.call("HSET","red:draw",openid,cjson.encode(red)) -- 处理队列 red["openid"] = openid redis.call("RPUSH","red:task",cjson.encode(red)) return true
Redis在 2.6
之前的版本用setnx作分布式锁的时候,会出现setnx
和 expire
遭到原子性破坏的可能,必需要配合lua脚原本实现原子性。但在2.6.12
版本开始,为 SET
命令增长了一系列选项:
SET key value[EX seconds][PX milliseconds][NX|XX]
能够看出来, SET
命令的自然原子性彻底能够取代 SETNX
和 EXPIRE
命令。
/** * redis排重锁 * @param $key * @param $expires * @param int $value * @return mixed */ public function redisLock($key, $expires, $value = 1) { //在key不存在时,添加key并$expires秒过时 return $this->redis->set($key, $value, ['nx', 'ex' => $expires]); }
[info] 总结:凡是须要多条redis命令须要捆绑在一块儿原子性操做的,都要使用lua来实现。