Redis系列六 Lua

来都来了 2020-11-26 14:24:40 ⋅ 156 阅读

 

本文目标

  1. 学习lua基本语法
  2. 能够采用redis+lua

lua 基本语法

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

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

设计目的

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

Lua 特性

  1. 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
  2. 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
  3. 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
  4. 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
  5. 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
  6. 通过闭包和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"haoel@hotmail.com""https://coolshell.cn"
end
-- 似乎必须直接解构!!!
name, age, email, website, bGay = getUserInfo()
userInfo = getUserInfo()
-- haoel   37      haoel@hotmail.com       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

  1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。
  2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他进程或者进程的命令插入。(最重要)
  3. 复用:客户端发送的脚本会永久存储在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"

字段说明

  1. KEYS[1]: 需要大写,对应的是1之后的lua-key, 【占位符】
  2. 1: 代表之后key的个数,多余的舍弃
  3. ARGV[1]: 需要大写,对应的是value的第一个

注意事项

  1. KEYS、ARGV:需要大写
  2. 内部需要用单引号

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", key, 1)
    return key .. "00001"
else
    redis.call("set", key, id + 1)
    return key .. string.format("%04d", id + 1)
end

参考链接
Redis乐观锁解决高并发抢红包的问题
Lua 教程
LUA简明教程
Redis中使用Lua语言
【redis进阶(1)】redis的Lua脚本控制(原子性)


全部评论: 0

    我有话说:

    Redis系列七 Debug Lua

      调试redis+lua 学了lua的基本语法,了解了redis+lua的配套用法,但是却不知道怎么断点调试。学就学全面点, 官网中有dubug相关说明。地址:Redis Lua

    Redis系列八 抢红包

      本文概述 掌握红包的两种常见生成算法 掌握lua+redis 实现原子性抢红包 项目中还有mysql相关内容 了解jmeter的基本用法 遗留问题 redis同步DB时机问题

    Redis系列四 锁

      本文目标 1. 熟悉乐观锁ABA概念 2. 理解掌握redis事务以及watch回滚; 3. 实战redis锁 乐观锁 乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的

    Redis系列一 基本用法&应用场景

        说明 redis的最基本使用方法以及使用场景。 字符串 // stringasync function stringFun() { const [key

    Redis系列二:位图实战,实现打卡签到

    前言 如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成。 如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取

    Redis系列九 推荐系统-布隆过滤器

      布隆过滤器 概念 布隆过滤器是一种空间利用率较高的概率型数据结构,用来测试一个元素是否在集合中。但是存在一定可能,导致结果误判。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在. 特性: 与哈希表不同,...

    Redis系列四 GEO附近的人

    GEO算法 GeoHash是一种地址编码方法。将二维的空间经纬度数据编码成一个字符串; 地球上的经度范围:[-180, 180],纬度范围:[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。 我们先将平面切割成四个正方形,然...

    为什么单线程的Redis能够达到百万级的QPS?

    作者:在江湖中coding链接:https://juejin.im/post/5e6097846fb9a07c9f3fe744 性能测试报告 查看了下阿里云 Redis 的性能测试报告如下,能够

    Redisson 3.13.6 发布,官方推荐的 Redis 客户端

    Redisson 3.13.6 已发布,这是一个 Java 编写的 Redis 客户端,具备驻内存数据网格(In-Memory Data Grid)功能,并获得了 Redis 的官方推荐

    商城系统 DBShop V3.0 Beta 发布

    全新重构,首次亮相。 系统简介 DBShop企业级商城系统,使用PHP语言基于Laminas(Zendframework 3) + Doctrine 2 组合框架开发完成。可定制、多终端、多场景、多

    BAT大牛Redis客户端与服务端交互原理

    Redis实例运行在单独的进程中,应用系统Redis客户端)通过Redis协议和Redis Server

    Redis 5.0.11、6.0.11、6.2 发布,修复 32 位系统上的整数溢出

    Redis 同时发布了 5.0.11、6.0.11 和 6.2 版本。对于使用 32 位 Redis 的用户来说,此次更新解决了一个重要的安全问题,即 32 位系统上的整数溢出((CVE-2021

    Martian框架发布 3.0.3 版本,Redis分布式锁

    项目简介 Martian 是一个声明式 API 编程(DAP)框架,可以帮助你快速开发后端服务。 以HttpServer作为 http服务,彻底脱离Tomcat这一类的Web容器和Servlet,同时也让项目减少了几个依赖 声明式API,让Co...

    Node&RabbitMQ系列 保证消费

        上篇文章主要以生产者角度:确保消息发出去了,这篇文章主要以消费者的角度:确保处理了对应的消息 Quonum Queue不适用场景适用场景代码实现RePublish 处理包含几层含义 成功ack; 失败nac...

    Redis多线程演进

    Redis作为一个基于内存的缓存系统,一直以高性能著称,因没有上下文切换以及无锁操作,即使在单线程处理情况下,读速度仍可达到11万次/s,写速度达到8.1万次/s。但是,单线程的设计也给Redis

    精品推荐:Redis主从复制

    持久化保证了即使 redis 服务重启也会丢失数据,因为 redis 服务重启后会将硬盘上持久化的数据恢复到内存中,但是当 redis 服务器的硬盘损坏了可能会导致数据丢失,如果通过 redis

    Node模块之fs模块()

    System)模块,文件系统模块中的方法均有异步...