vlambda博客
学习文章列表

Redis系列五:Redis+Lua高并发场景的数据一致性

本文目标

  • 学习lua基本语法

  • 能够采用redis+lua

lua 基本语法

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组于 1993 年开发的,该小组成员有:Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo。

设计目的

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 特性

  • 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。

  • 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。

  • 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);

  • 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;

  • 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;

  • 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。

基本语法

-- 行注释
--[[
块注释
--]]

--全局
num = 3

-- 变量
a1 = 5
local a2 = 6

function fun1()
    a3 = 7
    local a4 = 8 --局部变量
end
print("1.----变量分为:全局变量和局部变量")
print(a1, a2, a3, a4)

print("2.----循环与控制语句")
b1 = 1
while (b1 < num) do
    b1 = b1 + 1 -- 没有 【+= ++】 语法
    print("while循环", b1)
end
--[[
    数值 for 循环
for var=exp1, exp2, exp3 do end
var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次"执行体"。exp3 是可选的,如果不指定,默认为 1。
--]]

for i = 142 do
    print("for数值-循环", i)
end

--[[
    泛型循环
    i 是数组索引值,v 是对应索引的数组元素值。ipairs 是 Lua 提供的一个迭代器函数,用来迭代数组。
--]]

cArray = {
    "v1",
    "v2",
    "v3"
}
for i, v in ipairs(cArray) do
    print("for泛型-index: ", i, "value: ", v)
end

--[[
repeat...until循环
repeat...until 循环和 C 语言里面的 do...while() 作用是一样的。
--]]

d1 = 0
repeat
    d1 = d1 + 1
    print("repeat-", d1)
until (d1 > num)

--[[
    if 语句
    Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
    !boolean 类型只有两个可选值:true(真) 和 false(假),Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true:
--]]

if false or nil then
    print("至少有一个是 true")
else
    print("false 和 nil 都为 false")
end

if 0 then
    print("数字 0 是 true")
else
    print("数字 0 为 false")
end

--[[
运算符

1. +加、 -减、 *乘、 /除、 %取余、 ^乘幂、 -负数
2. ==等于、【~=】不等于、>、<、>=、<=
3. and、or、【not】 
--]]

print("---------分割线---------")

e1 = true
e2 = true

if (e1 and e2) then
    print("e1 and e2 - 条件为 true")
end

if (e1 or e2) then
    print("e1 or e2 - 条件为 true")
end

print("---------分割线---------")

-- 修改 a 和 b 的值
e1 = false
e2 = true

if (e1 and e2) then
    print("e1 and e2 - 条件为 true")
else
    print("e1 and e2 - 条件为 false")
end

if (not (e1 and e2)) then
    print("not( e1 and e2) - 条件为 true")
else
    print("not( e1 and e2) - 条件为 false")
end

print("---------函数---------")
myprint = function(params)
    print("函数 ##", params, "##")
end

function add(num1, num2, functionPrint)
    sum = num1 + num2
    functionPrint(sum)
end
add(13, myprint)

function maximun(array)
    local index = 1
    local value = array[index]
    for i, v in ipairs(array) do
        if v > value then
            index = i
            value = v
        end
    end
    return index, value
end
-- !Lua的下标不是从0开始的,是从1开始的。
print(maximun({80019148102}))

-- 可变参数 三点 ... 表示函数有可变的参数
function add(...)
    local sum = 0
    for i,v in ipairs{...} do 
        sum = sum + v
    end
    -- select("#",...) 来获取可变参数的数量
    -- .. 字符串拼接
    print("总共传入 " .. select("#",...) .. " 个数")
    return sum
end
print(add(1,2,3,4,5))
-- 斐波那契数列
function fib(n)
    if n<2 then return 1 end
    return fib(n-2) + fib(n+1)
end

--[[
    闭包
--]]

function newCounter()
    local i=0
    return function()
        i= i+1
        return i
    end
end
newCounter = newCounter()
newCounter()
newCounter()

--[[
    函数返回值,多个
--]]

function getUserInfo(id)
    print(id)
    return "haoel"37"[email protected]""https://coolshell.cn"
end
-- 似乎必须直接解构!!!
name, age, email, website, bGay = getUserInfo()
userInfo = getUserInfo()
-- haoel   37      [email protected]       https://coolshell.cn    nil
print(name, age, email, website, bGay)
-- haoel
print(userInfo)

--[[
    函数返回值,多个
--]]

print("---------函数---------")
print("---------table 类型---------")

--[[
    table 类型
--]]


mytable = {}
-- table 里面值的设置和获取
mytable[1] = "元素1"
mytable["er"] = "元素2"
print(mytable[1])

-- 数组,lua里面的元素是从 1 开始的
array = {10,20,30,40,50}
--[[
    等价于
    array = {[1]=10, [2]=20, [3]=30, [4]=40, [5]=50}
--]]


-- 数组里面值得获取
print(array[1], array[2], array[3])
-- 字典
dictionary = {
    key1 = "value1",
    key2 = "value2",
    key3 = "value3"
}

-- 字典里面值得获取
print(dictionary.key1, dictionary.key2, dictionary.key3)
print("---------table 类型---------")

Redis + Lua

在 Redis 中,执行 Lua 语言是原子性的,有助于 Redis 对并发数据一致性的支持。

为什么要用lua

  • 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。

  • 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他进程或者进程的命令插入。(最重要)

  • 复用:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复用这一脚本而不需要使用代码完成同样的逻辑。

两种方法运行脚本

  1. 直接输入一些 Lua 语言的程序代码;简单的脚本可以直接采用这种

  2. 将 Lua 语言编写成文件。有一定逻辑的采用这种

基本命令

eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]

其中:

  • eval 代表执行 Lua 语言的命令。

  • Lua-script 代表 Lua 语言脚本。

  • key-num 整数代表参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。

  • [key1 key2 key3…] 是 key 作为参数传递给 Lua 语言,也可以不填它是 key 的参数,但是需要和 key-num 的个数对应起来。

  • [value1 value2 value3…] 这些参数传递给 Lua 语言,它们是可填可不填的。

实例一 嵌入脚本

基本用法:set、get

# set
127.0.0.1:6379> EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lua-key lua-value
OK
# get
127.0.0.1:6379> EVAL "return redis.call('get', KEYS[1])" 1 lua-key
"lua-value"

127.0.0.1:6379> lpush person a b c
(integer) 3
# 多参数
127.0.0.1:6379> EVAL "return redis.call('lrange', KEYS[1], ARGV[1], ARGV[2])" 1 person 0 -1
1) "c"
2) "b"
3) "a"

字段说明

  • KEYS[1]: 需要大写,对应的是1之后的lua-key, 【占位符】

  • 1: 代表之后key的个数,多余的舍弃

  • ARGV[1]: 需要大写,对应的是value的第一个

注意事项

  • KEYS、ARGV:需要大写

  • 内部需要用单引号

node代码

// 嵌入脚本
async function fun1() {
    const argv = ['lua-key''lua-value'];
    const set = await redis.", 1, argv);
    const get = await redis."
1, argv[0]);
    console.log("简单:", set, get);
    // 同时传入多个key需要借助lua中的循环
    // const list = await redis.", 1, 'list', 1,2,3);
    await redis.", 1, 'list', '1');
    const listGet = await redis."
1'list'0-1);
    console.log("队列:", listGet)
}
const evalScript = `return redis.call('SET', KEYS[1], ARGV[2])`;
// 缓存脚本
async function fun2() {
    // 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"
}

实例二 脚本文件 -- 频次限制

// 脚本文件
async function fun3() {
    const luaScript = fs.readFileSync('./limit.lua');
    const key = 'rate:limit';
    // @ts-ignore
    const limit = await redis.;
    console.log('limit', limit);
}
--[[
Author: simuty
Date: 2020-11-26 11:17:33
LastEditTime: 2020-11-26 13:50:03
LastEditors: Please set LastEditors
Description:

limit.lua

!10秒内只能访问3次。 后续该脚本可以在nginx或者程序运行脚本中直接使用,判断返回是否为0,就0就不让其继续访问。
!以上,如果不使用redis+lua,那高并发下incr和expire就会出现原子性破坏,造成expire执行多次浪费

--]]


local times = redis.call("incr", KEYS[1])

if times == 1 then
    redis.call("expire", KEYS[1], ARGV[1])
else
    if times > tonumber(ARGV[2]) then
        return 0
    end
end

return 1
➜  6Lua git:(main) ✗ ts-node index.ts
limit 1
limit 1
limit 1
limit 0
limit 0

实例三 脚本文件 -- 自增ID

local key = KEYS[1]
local id = redis.call("get"key)
if id == false then
    redis.call("set"key1)
    return key .. "00001"
else
    redis.call("set"keyid + 1)
    return key .. string.format("%04d"id + 1)
end