Redis第三章之事务与Lua

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

  1. 集群模式下每当加载一个lua脚本,该脚本会随机存储在大多数节点下。
  2. 集群模式下每个节点都能调用脚本。
  3. 集群中执行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)

篇幅较长,如果内容或排版有问题请指正。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒狮运维

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值