本文主要是分享在实际工做中同事遇到的问题案例;活动组在作活动时,开发人员未考虑到接口并发场景,致使由于一些用户在实际抽奖(土豪通常都是狂抽)过程当中对余额产生了增长/减小的操做,致使缓存的余额出现异常;经过我review代码发现,开发者在更新缓存时:先get后set或者incrby,致使并发场景下get的值是一致的,因此缓存异常。php
那么针对这种我进行了改进使用:redis+lua脚本实现原子性保证余额数据正常。本文将跟你们一块儿学习Redis使用lua脚本的应用:html
Redis是高性能的key-value内存数据库,它帮助咱们解决了大部分业务问题;提供丰富的指令集合,据官网上统计有200多个命令。这些命令显然已经知足了咱们的常规的业务场景需求。可是在某些特殊的场景下,业务须要原子性操做,redis原有的命令是没法完成,因此须要额外开发实现原子操做。redis
由于这样的问题,Redis为开发者提供了lua
脚本的支持,用户能够向服务器发送lua脚原本执行自定义动做,以此获取脚本的响应数据。Redis自己又是单线程执行lua脚本,保证了lua脚本在处理逻辑过程当中不会被任意其它请求打断。数据库
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。json
其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。由于普遍的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。数组
好比:Lua脚本用在不少游戏上,主要是Lua脚本能够嵌入到其余程序中运行,游戏升级的时候,能够直接升级脚本,而不用从新安装游戏。缓存
小伙们能够查看我lua从入门到实战专栏
:安全
专栏已经在计划中出教程了,虽然看似写的内容比较简单,可是须要注重细节地方;lua语言每每在项目中出问题基本上细节较多。
可以使用版本:从 Redis 2.6.0
版本开始起;可经过内置的 Lua 解释器,可使用 EVAL
命令对 Lua 脚本进行执行。
时间复杂度:根据脚本的复杂度而定(脚本尽可能简洁)。
使用Lua脚本的好处:
### 共有三条优点
① 支持原子性操做 - Redis会将整个脚本做为一个总体执行,中间不会被其余请求插入。所以在脚本运行过程当中无需担忧会出现竞态条件,无需使用事务
② 下降网络开销 - 将多个请求经过脚本的形式一次发送到服务器,减小了网络的时延
③ 脚本复用 - 客户端发送的脚本可支持永久存在redis中,这样其余客户端能够复用这一脚本,而不须要使用代码完成相同的逻辑。
复制代码
Eval命令的基本语法以下:
## 命令格式
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
### 参数说明
① script Lua 5.1版本以上脚本程序,它会被运行在Redis服务器上下文中,这段脚本没必要(也不该该)定义为一个 Lua函数。
② numkeys 指用于指定键名参数的个数
③ key [key ...] 指要操做的键名,能够指定多个,在lua脚本中经过KEYS[1], KEYS[2]获取
④ arg [arg ...] 指附加参数,在lua脚本中经过全局变量 ARGV 数组访问;例如:ARGV[1], ARGV[2]
复制代码
### 既有key键也有附加参数
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 value2
1) "key1"
2) "key2"
3) "value1"
4) "value2"
### 只有附加参数
127.0.0.1:6379> eval "return {ARGV[1],ARGV[2]}" 0 'hello!' 'my name is amumu'
1) "hello!"
2) "my name is amumu"
### 注意
{} 在lua里是指数据类型table,一样相似常说的数组格式
复制代码
### lua脚本中,可以使用两个不一样函数来执行redis命令
① redis.call()
-- 正确的设置方式 设置amumu值为1000 60s过时
127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 amumu 1000 60
(integer) 1
127.0.0.1:6379> get amumu -- 设置成功
"1000"
127.0.0.1:6379> ttl amumu -- 剩余存活时间
(integer) 49
127.0.0.1:6379> ttl amumu -- 已通过期
(integer) -2
-- 出现报错的状况
127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 0 amumu 1000 60
(error) ERR Error running script (call to f_6aeea4b3e96171ef835a78178fceadf1a5dbe345): @user_script:1: @user_script: 1: Lua redis() command arguments must be strings or integers
② redis.pcall()
-- 正确的设置方式 获取amumu缓存值
127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 1 amumu
"1000"
-- 出现报错的状况
127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 0 amumu
(error) @user_script: 1: Lua redis() command arguments must be strings or integers
复制代码
从上面的报错状况能够看出来:redis.call() 和 redis.pcall() 的惟一区别在于它们对错误处理的不一样
redis.call()在执行命令的过程当中发生错误时,脚本会直接中止执行,并返回一个脚本错误,会告诉你形成错误的缘由
redis.pcall()执行中出错时并不引起致命错误,而是返回一个带err域的Lua表,展现结果
127.0.0.1:6379> eval 'local dt = redis.pcall("HGETALL", KEYS[1]); local res = {type(dt)}; for i, v in ipairs(dt) do res[#res+1] = i; res[#res+1] = v; end; return res' 0
1) "table"
复制代码
## 在命令行里使用
127.0.0.1:6379> redis-cli --eval lua_filenames key1 key2 , arg1 arg2 ...
### 各单位请注意
① eval命令的后面参数是lua脚本文件,须要完整的文件名;例如hello.lua
② 跟前两种方式不同的地方,不须要指定numkeys个数,而是使用,(英文逗号)隔开;注意,先后有空格。
复制代码
演示示例以下:
## test.lua文件
-- 获取缓存key
local _key = KEYS[1]
-- 获取设置的值
local _val = ARGV[1]
-- 获取缓存已经存在的值
local result = redis.call('GET', _key);
result = result and result or ""
-- 定义返回的结果变量
local text = ''
if result == '' then
return text
else
text = result .. _val
redis.call('SET', _key, text)
end
return text
复制代码
开始命令行运行lua脚本文件,以下图:
-- 第一次设置缓存未有值 因此返回了null
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '欢迎关注个人lua专栏!'
-- 设置默认值
➜ ~ redis-cli set lua:test '你们好,我是阿沐!'
OK
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '欢迎关注个人lua专栏!'
你们好,我是阿沐!欢迎关注个人lua专栏!
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '热衷于经过项目实战经验分享!'
你们好,我是阿沐!欢迎关注个人lua专栏!热衷于经过项目实战经验分享!
➜ ~ redis-cli --raw --eval Desktop/test.lua lua:test , '请必定要仔细阅读,注意点很重要!'
你们好,我是阿沐!欢迎关注个人lua专栏!热衷于经过项目实战经验分享!请必定要仔细阅读,注意点很重要!
### 注意
实际上咱们在正常开发过程,可能不会采用此方法,更多的仍是在,项目里使用仍是以脚本方式写入;
这里只是告诉你们有多种执行方式
复制代码
实战实例:
<?php
$script = <<<EOF local _key = KEYS[1] local _val = ARGV[1] local result = redis.call('GET', _key); result = result and result or "" local text = '' if result == '' then return text else text = result .. _val redis.call('SET', _key, text) end return text EOF;
// 获取传过来的变量
$text = isset($argv[1]) ? $argv[1] : '';
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$result = $redis->eval($script, array("lua:test", $text), 1);
echo $result;
### 执行结果集
➜ ~ /usr/local/opt/php@7.2/bin/php index.php
➜ ~ redis-cli set lua:test '你们好,我是阿沐!'
OK
➜ Desktop /usr/local/opt/php@7.2/bin/php index.php '欢迎关注个人lua专栏!'
你们好,我是阿沐!欢迎关注个人lua专栏!
复制代码
参数说明:
Redis::eval(string script, [array keys], int keys_nums)
### 解析参数
① ::eval 执行命令
② script 要执行的lua脚本
③ keys 是指key值
④ keys_nums 参数为KEYS的个数,用来区分KEYS和ARGV
复制代码
缘由以下:
① 生成环境下,若是使用evalsha会比eval发送更小的数据包,占用更少的网络资源;
② eval每次都须要把脚本完整发送给redis,而evalsha只须要传递一个sha1值便可完成
检测指定sha1是否已经存在:
## 基本命令
-- 指定一个或多个脚本的sha1校验和,返回一个结果集含有0和1的列表(tab),表示校验和所指定的脚本是否已经被保存在缓存当中
script exists sha1 [sha1 ...]
## 说明:
① redis版本号:必须大于等于 2.6.0
② 时间复杂度: O(n),n为给定的sha1校验和的数量
③ 结果集: 一个列表返回;0-不存在缓存中;2-存在缓存中;列表值跟结果集一一对应
复制代码
演示示例:
-- 检测sha1是否存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 0
-- 设置sha1值
127.0.0.1:6379> script load "return redis.call('get', 'lua:test')"
"b3e2eb6aa7bdb29e60f32cd153612a2887164b70"
-- 这时已经存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 1
-- 获取多个返回列表
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70' 'd1cb717b6f16ad4e798430f98c31bc449222b946'
1) (integer) 1
2) (integer) 0
复制代码
根据sha1值使用evalsha执行脚本:
## 基础执行命令
-- 根据给定的 sha1 校验码,执行缓存在服务器中的脚本
evalsha sha1 numkeys key [key ...] arg [arg ...]
### 参数说明:
① sha1 经过上面 script load 生成的 sha1 校验码
② numkeys 指定键名参数的个数
③ key redis的键名
④ arg 附加参数,就是附带进入脚本的变量值
复制代码
演示示例:(使用时要注意,并非全部的脚本都适合缓存,形成没必要要的内存浪费)
➜ ~ redis-cli --raw evalsha b3e2eb6aa7bdb29e60f32cd153612a2887164b70 0
你们好,我是阿沐!欢迎关注个人lua专栏!
复制代码
有的时候咱们脚本出问题了,可是并不知道究竟是由于那一行代码或者变量不对致使脚本中断;我想大部分开发都会急躁,更有甚至者调试了半天一直看不出问题,会口吐芬芳等等。其实,在实际开发过程当中,咱们找不到问题所在的时候,必定要多打日志,咱们只有经过日志才能更好地找到问题所在,而不是一味的抱怨,抱怨解决不了任何问题。
Lua
脚本中,能够经过调用 redis.log
函数来将错误信息写入 Redis 日志(log),命令以下:
redis.log(loglevel, message)
### 参数说明
① loglevel 错误等级,跟咱们日常开发同样,bug、提示、警告等等
② message 错误信息,跟咱们日常开发异常抛出信息一致
复制代码
其中 loglevel 参数能够是如下任意一个值:
redis.LOG_DEBUG -- 会打印生成大量信息,适用于开发/测试阶段
redis.LOG_VERBOSE -- 包含不少不太有用的信息,可是不像debug级别那么混乱
redis.LOG_NOTICE -- 适度冗长,适用于生产环境
redis.LOG_WARNING -- 仅记录很是重要、关键的警告消息
复制代码
注意:只有设置的错误等级大于等于redis实例日志等级才会被记录下来
演示示例:
27.0.0.1:6379> eval 'redis.log(redis.LOG_WARNING, "Something is wrong with this script.")' 0
(nil)
-- 在redis的日志文件中查看:
1174:M 30 May 18:09:20.347 # Something is wrong with this script.
复制代码
PS:这个经过日志来看脚本问题,仍是比较重要的,若是不能一眼看出你脚本问题,那么请尽可能的保证你多打点日志查问题。
### lua语言中如何实现原子脚本
package.path = package.path..";~/redis-lua/src/?.lua" --redis.lua所在目录
local json_encode = require "cjson" .encode
local redis = require("redis")
local reds, err = redis.connect('127.0.0.1',6379)
--- lua脚本检测当前缓存值是否已 溢出 未溢出累加 不然 计算应增长多少值
local _introduce_myself = [[ local _key = KEYS[1] local cnt = ARGV[1] local limit = ARGV[2] local currnt_cnt = redis.call('GET', _key) currnt_cnt = tonumber(currnt_cnt) or 0 limit = tonumber(limit) or 0 cnt = tonumber(cnt) or 0 local ret = {"num", 0 ,"score", 0} if currnt_cnt < limit then local res = currnt_cnt + cnt if res >= limit then local diff = limit - currnt_cnt redis.call('INCRBY', _key, diff) ret[2] = limit ret[4] = diff else redis.call('INCRBY', _key, cnt) ret[2] = cnt end end return ret ]]
-- 执行lua脚本
function execute_script()
local key = 'lua:test' -- 缓存key
local count = 500 -- 每次增长数量
local limit = 1000 -- 限制总数溢出状况
-- 执行脚本 更改执行缓存值,保证不超过限制的最大值 溢出则丢弃
local res , err = reds:eval(_introduce_myself, 1, key, count, limit)
print(json_encode(res))
end
execute_script() -- 调用execute_script脚本函数
复制代码
根据官方所说:lua脚本内部变量禁止产生随机参数,若是在集群环境下,存在多主多从节点;当master节点执行完脚本之后,slave节点会一样执行该脚本。
一旦脚本内部含有随机值这种,就可能致使主从数据不一致;因此lua脚本会严格限制全部的脚本都无反作用。
Redis 对 Lua 环境作了一些列相应的措施:
① 不提供访问系统状态状态的库
② 禁止使用 loadfile 函数
③ 禁止出现随机性质命令
复制代码
redisbook.readthedocs.io/en/latest/f… - 主要看脚本的安全性
各单位请注意:《Lua语言从入门到实战》已经悄悄地进行中了!