百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

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

mhr18 2024-10-25 12:38 29 浏览 0 评论

本文目标

  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 = 1, 4, 2 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(1, 3, 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({800, 19, 1, 4, 8, 102}))

-- 可变参数 三点 ... 表示函数有可变的参数
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合集-使用benchmark性能测试

采用开源Redis的redis-benchmark工具进行压测,它是Redis官方的性能测试工具,可以有效地测试Redis服务的性能。本次测试使用Redis官方最新的代码进行编译,详情请参见Redis...

Java简历总被已读不回?面试挂到怀疑人生?这几点你可能真没做好

最近看了几十份简历,发现大部分人不是技术差,而是不会“卖自己”——一、简历死穴:你写的不是经验,是岗位说明书!反面教材:ד使用SpringBoot开发项目”ד负责用户模块功能实现”救命写法:...

redission YYDS(redission官网)

每天分享一个架构知识Redission是一个基于Redis的分布式Java锁框架,它提供了各种锁实现,包括可重入锁、公平锁、读写锁等。使用Redission可以方便地实现分布式锁。red...

从数据库行锁到分布式事务:电商库存防超卖的九重劫难与破局之道

2023年6月18日我们维护的电商平台在零点刚过3秒就遭遇了严重事故。监控大屏显示某爆款手机SKU_IPHONE13_PRO_MAX在库存仅剩500台时,订单系统却产生了1200笔有效订单。事故复盘发...

SpringBoot系列——实战11:接口幂等性的形而上思...

欢迎关注、点赞、收藏。幂等性不仅是一种技术需求,更是数字文明对确定性追求的体现。在充满不确定性的网络世界中,它为我们建立起可依赖的存在秩序,这或许正是技术哲学最深刻的价值所在。幂等性的本质困境在支付系...

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享

如何优化系统架构设计缓解流量压力提升并发性能?Java实战分享在高流量场景下。首先,我需要回忆一下常见的优化策略,比如负载均衡、缓存、数据库优化、微服务拆分这些。不过,可能还需要考虑用户的具体情况,比...

Java面试题: 项目开发中的有哪些成长?该如何回答

在Java面试中,当被问到“项目中的成长点”时,面试官不仅想了解你的技术能力,更希望看到你的问题解决能力、学习迭代意识以及对项目的深度思考。以下是回答的策略和示例,帮助你清晰、有说服力地展示成长点:一...

互联网大厂后端必看!Spring Boot 如何实现高并发抢券逻辑?

你有没有遇到过这样的情况?在电商大促时,系统上线了抢券活动,结果活动刚一开始,服务器就不堪重负,出现超卖、系统崩溃等问题。又或者用户疯狂点击抢券按钮,最后却被告知无券可抢,体验极差。作为互联网大厂的后...

每日一题 |10W QPS高并发限流方案设计(含真实代码)

面试场景还原面试官:“如果系统要承载10WQPS的高并发流量,你会如何设计限流方案?”你:“(稳住,我要从限流算法到分布式架构全盘分析)…”一、为什么需要限流?核心矛盾:系统资源(CPU/内存/数据...

Java面试题:服务雪崩如何解决?90%人栽了

服务雪崩是指微服务架构中,由于某个服务出现故障,导致故障在服务之间不断传递和扩散,最终造成整个系统崩溃的现象。以下是一些解决服务雪崩问题的常见方法:限流限制请求速率:通过限流算法(如令牌桶算法、漏桶算...

面试题官:高并发经验有吗,并发量多少,如何回复?

一、有实际高并发经验(建议结构)直接量化"在XX项目中,系统日活用户约XX万,核心接口峰值QPS达到XX,TPS处理能力为XX/秒。通过压力测试验证过XX并发线程下的稳定性。"技术方案...

瞬时流量高并发“保命指南”:这样做系统稳如泰山,老板跪求加薪

“系统崩了,用户骂了,年终奖飞了!”——这是多少程序员在瞬时大流量下的真实噩梦?双11秒杀、春运抢票、直播带货……每秒百万请求的冲击,你的代码扛得住吗?2025年了,为什么你的系统一遇高并发就“躺平”...

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。

其实很多Java工程师不是能力不够,是没找到展示自己的正确姿势。比如上周有个小伙伴找我,五年经验但简历全是'参与系统设计''优化接口性能'这种空话。我就问他:你做的秒杀...

PHP技能评测(php等级考试)

公司出了一些自我评测的PHP题目,现将题目和答案记录于此,以方便记忆。1.魔术函数有哪些,分别在什么时候调用?__construct(),类的构造函数__destruct(),类的析构函数__cal...

你的简历在HR眼里是青铜还是王者?

你的简历在HR眼里是青铜还是王者?兄弟,简历投了100份没反应?面试总在第三轮被刷?别急着怀疑人生,你可能只是踩了这些"隐形求职雷"。帮3630+程序员改简历+面试指导和处理空窗期时间...

取消回复欢迎 发表评论: