Redis第三章之事务与Lua
文章目录
一、事务
事务简介
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED
可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执行,而是暂时保存在Redis中。如果此时另一个客户端执行sismember user : a : follow user:b返回结果应该为0。
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0
只有当exec执行后,用户A关注用户B的行为才算完成,如下所示回的两个结果对应sadd命令。
127.0.0.1:6379> exec
(integer) 1
(integer) 1
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1
如果要停止事务的执行,可以使用discard命令代替exec命令即可。
如果事务中的命令出现错误,Redis的处理机制也不尽相同。
事务错误处理之命令错误
例如下面操作错将set写成了sett,属于语法错误,但是剩余的正确命令也会正常执行。(使用nodejs调用的时候如果检测出事务中的关键字有错误,则报错,事务中所有命令都不会执行)
> multi
OK
> sett key world
(error) ERR unknown command
> incr counter
QUEUED
> exec
1
nodejs 测试
client.multi([
['set','test001','test002'],
['seet','test001','test003'],
['get','test001'],
]).exec(function(error,result){
if(error){
console.log(error);
client.discard();
}
console.log(result);
})
//报错 Cannot read property 'apply' of undefined
事务错误处理之运行时错误
例如在set中多加了一个参数,开始运行时没有检测出语法错误,在运行时错误仅仅是有错误的语句不执行,其他正确的语句照常执行,不会回滚。
nodejs测试
client.multi([
['set','test001','test002',''],
['set','test001','test003'],
['get','test001'],
]).exec(function(error,result){
if(error){
console.log(error);
client.discard();
}
console.log(result);
})
//结果
[
ReplyError: ERR syntax error
at parseError (E:\WorkSpace\VScode\Nodejs\redisClient\node_modules\redis-parser\lib\parser.js:179:12)
at parseType (E:\WorkSpace\VScode\Nodejs\redisClient\node_modules\redis-parser\lib\parser.js:302:14) {
code: 'ERR',
command: 'SET'
},
'OK',
'test003'
]
watch
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题。
二、Lua用法简述
数据类型及其逻辑处理
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。
字符串
下面定义一个字符串类型的数据:
--声明一个局部变量
local strings val = "world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量。
print函数可以打印出变量的值,例如下面代码将打印world,其中"–"是Lua语言的注释。
数组
在Lua中,如果要使用类似数组的功能,可以用tables类型,下面代码使用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是,Lua的数组下标从1开始计算:
local tables myArray = {"redis", "jedis", true, 88.0}
--true
print(myArray[3])
如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语言是一致的。
(a)for
local int sum = 0
-- for 起始值,终值,步长(默认为1)
for i = 1, 100
do
sum = sum + i
end
-- 输出结果为5050
print(sum)
获取一个列表或集合的长度只需在变量前面添加#即可。
LIST = {1,2,3,4}
-- 打印LIST长度
print(#LIST)
除此之外,Lua还提供了内置函数ipairs,使用for index,value
ipairs(tables)可以遍历出所有的索引下标和值:
for index,value in ipairs(myArray)
do
print(index)
print(value)
end
(b)while
下面代码同样会计算1到100的和,只不过使用的是while循环,while循环同样以end作为结束符。
local int sum = 0
local int i = 0
while i <= 100
do
sum = sum +i
i = i + 1
end
--输出结果为5050
print(sum)
(c)if else
要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后紧跟then:
local tables myArray = {"redis", "jedis", true, 88.0}
for i = 1, #myArray
do
if myArray[i] == "jedis"
then
print("true")
break
else
--do nothing
end
end
哈希
如果要使用类似哈希的功能,同样可以使用tables类型,例如下面代码定义了一个tables,每个元素包含了key和value,其中strings1…string2是将两个字符串进行连接:
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_1 age is " .. user_1["age"])
如果要遍历user_1,可以使用Lua的内置函数pairs:
for key,value in pairs(user_1)
do print(key .. value)
end
函数定义
在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体:
function funcName()
...
end
contact函数将两个字符串拼接:
function contact(str1, str2)
return str1 .. str2
end
--"hello world"
print(contact("hello ", "world"))
三、Redis与Lua
在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
eval
--语法
eval 脚本内容 key个数 key列表 参数列表
--实例(..是lua中的拼接符)
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
此时KEYS[1]=“redis”,ARGV[1]=“world”,所以最终的返回结果是"hello redisworld"。
如果Lua脚本较长,还可以使用redis-cli --eval直接执行文件。
./redis-cli --eval xxx.lua
eval命令和–eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端。
evalsha
除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
加载脚本:script load命令可以将脚本内容加载到Redis内存中(现在不能直接使用redis-cli 加载,需要使用其他语言调用)
执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和eval一致。
--语法
evalsha 脚本SHA1值 key个数 key列表 参数列表
--实例
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
nodejs 连接reids调用lua脚本
//nodejs脚本
const Redis = require("ioredis");
const redis = new Redis(6379, "106.14.136.247");
const fs = require('fs');
// 直接获取
// const evalScript = `return redis.call('SET', KEYS[1], ARGV[2])`;
// 读取文件获取
const evalScript = fs.readFileSync('E:\\WorkSpace\\VScode\\Nodejs\\redisClient\\lua\\script.lua')
redis.select(7)
async function evalSHA() {
// 1. 缓存脚本获取 sha1 值
const sha1 = await redis.script("load", evalScript);
console.log(sha1); // 6bce4ade07396ba3eb2d98e461167563a868c661
// 2. 通过 evalsha 执行脚本
await redis.evalsha(sha1, 2, 'name1', 'name2', 'val1', 'val2');
// 3. 获取数据
const result = await redis.get("name1");
console.log(result); // "val2"
}
evalSHA();
简单的lua脚本
return redis.call('MSET', KEYS[1], ARGV[2],KEYS[2], ARGV[2])
Lua的Redis API
Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用redis.call调用了Redis的set和get操作:
redis.call("set", "hello", "world")
redis.call("get", "hello")
放在Redis的执行效果如下:
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据具体的应用场景进行函数的选择。
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
四、Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍。
script load
// redis内置命令行
script load lua脚本
-------------------------------------------------------
// nodejs 调用
-- 导入文件操作模块
const fs = require('fs');
-- 读取文件获取
const evalScript = fs.readFileSync('E:\\WorkSpace\\VScode\\Nodejs\\redisClient\\lua\\script.lua')
-- 缓存脚本获取 sha1 值
const sha1 = await redis.script("load", evalScript);
此命令用于将Lua脚本加载到Redis内存中。
script exists
// redis内置命令行
script exists sha1 [sha1 …]
------------------------------------------------------
// nodejs 调用
redis.script("exists", '0043877f8b6e376d4cde4659ec48944d19e81467');
此命令用于判断sha1是否已经加载到Redis内存中:
//存在返回1,不存在返回0
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
script flush
// redis内置命令行
script flush
// nodejs 调用
redis.script('flush')
此命令用于清除Redis内存已经加载的所有Lua脚本。
script kill
script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。
如果lua脚本在死循环前已经对数据库进行操作,则无法使用SCRIPT KILL进行终止。需要使用SHUTDOWN NOSAVE,重启后之前写入的数据会丢失。
总结:
- lua脚本运行时不允许其他命令执行,所以lua脚本如果执行很慢会影响redis的响应。(抢红包,秒杀)
- 如果lua脚本在死循环前已经对数据库进行操作,则无法使用SCRIPT KILL进行终止。需要使用SHUTDOWN NOSAVE,重启后之前写入的数据会丢失。
- 如果lua脚本执行到某行时报错,在报错之前对数据库的所有操作都是有效的。(不会回滚)
- lua脚本一旦缓存,所有的库都能获取到
- lua脚本加载后对应的哈希码是根据内容生成,同样的内容生成相同的哈希码。
集群模式下的lua
- 集群模式下每当加载一个lua脚本,该脚本会随机存储在大多数节点下。
- 集群模式下每个节点都能调用脚本。
- 集群中执行lua脚本需要所涉及到的值都在同一solt,不然会报错(CROSSSLOT Keys in request don’t hash to the same slot)
案例:(python线程池 + redis + lua 模拟秒杀)
lua脚本
--选择数据库
redis.call('SELECT',10)
--查看进来的用户是否重复
local isRepeat = tonumber(redis.call('sismember','users:ms',KEYS[1]))
--如果重复,返回repeat
if isRepeat == 1 then
return 'repeat'
end
--查看剩余量
local kc = tonumber(redis.call('get','kc'))
--如果库存大于0,则进行秒杀
if kc > 0 then
redis.call('sadd','users:ms',KEYS[1])
redis.call('decrby','kc',1)
return 'ok'
end
-- 没有库存返回no
return 'no'
python脚本
import redis
import os
from multiprocessing.dummy import Pool
import chardet
# 加载本地lua脚本到redis
def loadScript():
# 连接
r = redis.StrictRedis(host='10.20.1.40', port=6379, db=0,password='redis2020@123')
# 打开脚本
luaScript = open('E:\\WorkSpace\\VScode\\Nodejs\\redisClient\\lua\\script.lua',encoding='UTF-8')
#将lua脚本读取到字符串lua中
lua = luaScript.read()
# 将字符串lua缓存到redis,此时会返回sha检验码
sha1 = r.script_load(lua)
# 打印校验码
print(sha1) # 87a91c7189879b8f6cc8cb8b8236798b2b70d277
# 关闭连接
r.close()
# 秒杀函数
def ms(username):
# 获取连接
r = redis.StrictRedis(host='10.20.1.40', port=6379, db=0,password='redis2020@123')
# 利用evalsha执行缓存在redis中的lua脚本,1是参数中key的数量,返回的是
msInfo = bytes.decode(r.evalsha('87a91c7189879b8f6cc8cb8b8236798b2b70d277',1,username))
if msInfo == 'ok':
print(username + '已抢到')
if msInfo == 'repeat':
print(username+'重复抢')
if msInfo == 'no':
print(username+'没有抢到')
r.close()
if __name__ == '__main__':
# 声明一个列表
userList = []
# 生成用户列表
for number in range(20):
number = 'user' + str(number)
userList.append(number)
# 打印用户列表
print(userList)
# 生成一个有20个进程的进程池
pool = Pool(20)
# 执行,20个进程分别获取userList中的参数去执行ms函数
pool.map(ms,userList)
篇幅较长,如果内容或排版有问题请指正。。。