前言

最近我们组的项目在做活动功能时,开发人员没有考虑到并发场景的存在,导致存在一些因为并发导致一些用户活跃度不正常。那么针对这种我进行了改进使用redis+lua实现原子性保证活跃数据正常。本文将跟大家一起学习Redis使用lua脚本的应用。

  • 1. 为什么引入Lua
  • 2. 什么是Lua
  • 3. 主要优势
  • 4. 基本用法
  • 5. 实战讲解
  • 6. 脚本的安全性

Redis中为什么引入Lua脚本?

Redis是高性能的key-value内存数据库,它帮助我们解决了大部分业务问题;提供丰富的指令集合,据官网上统计有200多个命令。这些命令显然已经满足了我们的常规的业务场景需求。但是在某些特殊的场景下,业务需要原子性操作,redis原有的命令是无法完成,所以需要额外开发实现原子操作。

因为这样的问题,Redis为开发者提供了lua脚本的支持,用户可以向服务器发送lua脚本来执行自定义动作,以此获取脚本的响应数据。Redis本身又是单线程执行lua脚本,保证了lua脚本在处理逻辑过程中不会被任意其它请求打断

什么是Lua

Lua是一种轻量小巧脚本语言,用标准C语言编写并以源代码形式开放。

其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。

比如:Lua脚本用在很多游戏上,主要是Lua脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。

小伙们可以查看我lua从入门到实战专栏

  • 我还是没有忍住,于是乎我开通了lua语言学习专栏!知乎火了一下

  • 【lua语言从青铜到王者】第一篇:lua前世今生

  • 【lua语言从青铜到王者】第二篇:开发环境搭建+3种编辑器使用示例

专栏已经在计划中出教程了,虽然看似写的内容比较简单,但是需要注重细节地方;lua语言往往在项目中出问题基本上细节较多。

主要优势

可使用版本:从 Redis 2.6.0 版本开始起;可通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行执行。

时间复杂度:根据脚本的复杂度而定(脚本尽量简洁)。

使用Lua脚本的好处

### 共有三条优势① 支持原子性操作 - Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务② 降低网络开销 - 将多个请求通过脚本的形式一次发送到服务器,减少了网络的时延③ 脚本复用    - 客户端发送的脚本可支持永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。

基本用法

Eval命令的基本语法如下:

## 命令格式
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]### 参数说明① script Lua 5.1版本以上脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua函数。② numkeys 指用于指定键名参数的个数③ key [key ...] 指要操作的键名,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取④ arg [arg ...] 指附加参数,在lua脚本中通过全局变量 ARGV 数组访问;例如:ARGV[1], ARGV[2]

① 实例实现方式之一:

### 既有key键也有附加参数
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 value2
1) "key1"
2) "key2"
3) "value1"
4) "value2"### 只有附加参数
127.0.0.1:6379> eval "return {ARGV[1],ARGV[2]}" 0 'hello!' 'my name is amumu'
1) "hello!"
2) "my name is amumu"### 注意{} 在lua里是指数据类型table,同样类似常说的数组格式

② 实例实现方式之二:

### lua脚本中,可使用两个不同函数来执行redis命令① redis.call()-- 正确的设置方式 设置amumu值为1000 60s过期  127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 1 amumu 1000 60(integer) 1127.0.0.1:6379> get amumu -- 设置成功"1000"127.0.0.1:6379> ttl amumu -- 剩余存活时间(integer) 49127.0.0.1:6379> ttl amumu -- 已经过期(integer) -2-- 出现报错的情况127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('EXPIRE', KEYS[1], ARGV[2]); return 1;" 0 amumu 1000 60(error) ERR Error running script (call to f_6aeea4b3e96171ef835a78178fceadf1a5dbe345): @user_script:1: @user_script: 1: Lua redis() command arguments must be strings or integers② redis.pcall()-- 正确的设置方式 获取amumu缓存值127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 1 amumu"1000"-- 出现报错的情况127.0.0.1:6379> EVAL "return redis.pcall('GET', KEYS[1])" 0 amumu(error) @user_script: 1: Lua redis() command arguments must be strings or integers

从上面的报错情况可以看出来:redis.call() 和 redis.pcall() 的唯一区别在于它们对错误处理的不同

  • redis.call()在执行命令的过程中发生错误时,脚本会直接停止执行,并返回一个脚本错误,会告诉你造成错误的原因

  • redis.pcall()执行中出错时并不引发致命错误,而是返回一个带err域的Lua表,展示结果

127.0.0.1:6379>  eval 'local dt = redis.pcall("HGETALL", KEYS[1]); local res = {type(dt)}; for i, v in ipairs(dt) do res[#res+1] = i; res[#res+1] = v; end; return res' 0
1) "table"

③ 实例实现方式之三

## 在命令行里使用127.0.0.1:6379> redis-cli --eval lua_filenames key1 key2 , arg1 arg2 ...### 各单位请注意① eval命令的后面参数是lua脚本文件,需要完整的文件名;例如hello.lua② 跟前两种方式不一样的地方,不需要指定numkeys个数,而是使用,(英文逗号)隔开;注意,前后有空格。

演示示例如下:

## test.lua文件-- 获取缓存key
local _key = KEYS[1]
-- 获取设置的值
local _val = ARGV[1]-- 获取缓存已经存在的值
local result = redis.call('GET', _key);
result = result and result or ""-- 定义返回的结果变量
local text = ''
if result == '' thenreturn text
elsetext = result .. _valredis.call('SET', _key, text)
endreturn text

开始命令行运行lua脚本文件,如下图:

-- 第一次设置缓存未有值 所以返回了null
➜  ~ redis-cli --raw --eval Desktop/test.lua lua:test , '欢迎关注我的lua专栏!'-- 设置默认值
➜  ~ redis-cli set lua:test '大家好,我是阿沐!'
OK➜  ~ redis-cli --raw --eval Desktop/test.lua lua:test , '欢迎关注我的lua专栏!'
大家好,我是阿沐!欢迎关注我的lua专栏!➜  ~ redis-cli --raw --eval Desktop/test.lua lua:test , '热衷于通过项目实战经验分享!'
大家好,我是阿沐!欢迎关注我的lua专栏!热衷于通过项目实战经验分享!➜  ~ redis-cli --raw --eval Desktop/test.lua lua:test , '请一定要仔细阅读,注意点很重要!'
大家好,我是阿沐!欢迎关注我的lua专栏!热衷于通过项目实战经验分享!请一定要仔细阅读,注意点很重要!### 注意实际上我们在正常开发过程,可能不会采用此方法,更多的还是在,项目里使用还是以脚本方式写入;这里只是告诉大家有多种执行方式

实战实例:

<?php
$script = <<<EOFlocal _key = KEYS[1]local _val = ARGV[1]local result = redis.call('GET', _key);  result = result and result or ""local text = ''if result == '' thenreturn textelsetext = result .. _valredis.call('SET', _key, text)endreturn text
EOF;// 获取传过来的变量
$text = isset($argv[1]) ? $argv[1] : '';
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$result = $redis->eval($script, array("lua:test", $text), 1);
echo $result;### 执行结果集
➜ ~ /usr/local/opt/php@7.2/bin/php index.php➜ ~ redis-cli set lua:test '大家好,我是阿沐!'
OK
➜  Desktop /usr/local/opt/php@7.2/bin/php index.php '欢迎关注我的lua专栏!'
大家好,我是阿沐!欢迎关注我的lua专栏!

参数说明:

Redis::eval(string script, [array keys], int keys_nums)### 解析参数① ::eval 执行命令② script 要执行的lua脚本 ③ keys 是指key值④ keys_nums 参数为KEYS的个数,用来区分KEYS和ARGV

④ lua脚本加载到脚本缓存-evalsha

原因如下:

  • ① 生成环境下,如果使用evalsha会比eval发送更小的数据包,占用更少的网络资源;

  • ② eval每次都需要把脚本完整发送给redis,而evalsha只需要传递一个sha1值即可完成

检测指定sha1是否已经存在:

## 基本命令
-- 指定一个或多个脚本的sha1校验和,返回一个结果集含有0和1的列表(tab),表示校验和所指定的脚本是否已经被保存在缓存当中
script exists sha1 [sha1 ...] ## 说明:
① redis版本号:必须大于等于 2.6.0② 时间复杂度: O(n),n为给定的sha1校验和的数量③ 结果集: 一个列表返回;0-不存在缓存中;2-存在缓存中;列表值跟结果集一一对应

演示示例:

-- 检测sha1是否存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 0-- 设置sha1值
127.0.0.1:6379> script load "return redis.call('get', 'lua:test')"
"b3e2eb6aa7bdb29e60f32cd153612a2887164b70"-- 这时已经存在
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70'
1) (integer) 1-- 获取多个返回列表
127.0.0.1:6379> script exists 'b3e2eb6aa7bdb29e60f32cd153612a2887164b70' 'd1cb717b6f16ad4e798430f98c31bc449222b946'
1) (integer) 1
2) (integer) 0

根据sha1值使用evalsha执行脚本:

## 基础执行命令-- 根据给定的 sha1 校验码,执行缓存在服务器中的脚本
evalsha sha1 numkeys key [key ...] arg [arg ...]### 参数说明:① sha1     通过上面 script load 生成的 sha1 校验码② numkeys  指定键名参数的个数③ key      redis的键名④ arg      附加参数,就是附带进入脚本的变量值

演示示例:(使用时要注意,并不是所有的脚本都适合缓存,造成不必要的内存浪费)

➜ ~ redis-cli --raw evalsha b3e2eb6aa7bdb29e60f32cd153612a2887164b70 0
大家好,我是阿沐!欢迎关注我的lua专栏!

⑤ 脚本日志

有的时候我们脚本出问题了,但是并不知道到底是因为那一行代码或者变量不对导致脚本中断;我想大部分开发都会急躁,更有甚至者调试了半天一直看不出问题,会口吐芬芳等等。其实,在实际开发过程中,我们找不到问题所在的时候,一定要多打日志,我们只有通过日志才能更好地找到问题所在,而不是一味的抱怨,抱怨解决不了任何问题。

Lua脚本中,可以通过调用 redis.log 函数来将错误信息写入 Redis 日志(log),命令如下:

redis.log(loglevel, message)### 参数说明① loglevel  错误等级,跟我们平常开发一样,bug、提示、警告等等② message   错误信息,跟我们平常开发异常抛出信息一致

其中 loglevel 参数可以是以下任意一个值:

redis.LOG_DEBUG     -- 会打印生成大量信息,适用于开发/测试阶段redis.LOG_VERBOSE   -- 包含很多不太有用的信息,但是不像debug级别那么混乱redis.LOG_NOTICE    -- 适度冗长,适用于生产环境redis.LOG_WARNING   -- 仅记录非常重要、关键的警告消息

注意:只有设置的错误等级大于等于redis实例日志等级才会被记录下来

演示示例:

27.0.0.1:6379>  eval 'redis.log(redis.LOG_WARNING, "Something is wrong with this script.")' 0
(nil)-- 在redis的日志文件中查看:
1174:M 30 May 18:09:20.347 # Something is wrong with this script.

PS:这个通过日志来看脚本问题,还是比较重要的,如果不能一眼看出你脚本问题,那么请尽量的保证你多打点日志查问题。

实战讲解

###  lua语言中如何实现原子脚本package.path = package.path..";~/redis-lua/src/?.lua"  --redis.lua所在目录local json_encode = require "cjson" .encode
local redis = require("redis")local reds, err = redis.connect('127.0.0.1',6379)--- lua脚本检测当前缓存值是否已 溢出 未溢出累加 否则 计算应增加多少值
local _introduce_myself = [[local _key  = KEYS[1]local cnt   = ARGV[1]local limit = ARGV[2]local currnt_cnt = redis.call('GET', _key)currnt_cnt = tonumber(currnt_cnt) or 0limit = tonumber(limit) or 0cnt   = tonumber(cnt) or 0local ret = {"num", 0 ,"score", 0}if currnt_cnt < limit thenlocal res = currnt_cnt + cntif res >= limit thenlocal diff = limit - currnt_cntredis.call('INCRBY', _key, diff)ret[2] = limitret[4] = diffelseredis.call('INCRBY', _key, cnt)ret[2] = cntendendreturn ret
]]-- 执行lua脚本
function  execute_script()local key   = 'lua:test' -- 缓存keylocal count = 500        -- 每次增加数量local limit = 1000       -- 限制总数溢出情况-- 执行脚本 更改执行缓存值,保证不超过限制的最大值 溢出则丢弃 local res , err = reds:eval(_introduce_myself, 1, key, count, limit)print(json_encode(res))
endexecute_script() -- 调用execute_script脚本函数

lua脚本的安全问题

根据官方所说:lua脚本内部变量禁止产生随机参数,如果在集群环境下,存在多主多从节点;当master节点执行完脚本以后,slave节点会同样执行该脚本。

一旦脚本内部含有随机值这种,就可能导致主从数据不一致;所以lua脚本会严格限制所有的脚本都无副作用

Redis 对 Lua 环境做了一些列相应的措施:

① 不提供访问系统状态状态的库② 禁止使用 loadfile 函数③ 禁止出现随机性质命令

参考文档

https://redisbook.readthedocs.io/en/latest/feature/scripting.html#id2 - 主要看脚本的安全性

各单位请注意:《Lua语言从入门到实战》已经悄悄地进行中了!

后端程序员必会:并发情况下redis-lua保证原子操作相关推荐

  1. SpringBean默认是单例的,高并发情况下,如何保证并发安全?

    以下文章来源方志朋的博客,回复"666"获面试宝典 Spring的bean默认都是单例的,某些情况下,单例是并发不安全的,以Controller举例,问题根源在于,我们可能会在Co ...

  2. 程序员,在什么情况下加班是可接受的

    目录 一.胖子请客 二.中秋加班 三.不同情况的加班 四.后记 一.胖子请客 我有个同事,是个胖子. 前几天找我喝大酒,坐在路边的烧烤摊上.他举着酒杯对着月亮又看着我,说时间过得真快,又要过中秋了. ...

  3. MySQL 后端程序员必知优化!

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by涉及的列上建立索引. 2.应尽量避免在 where 子句中使用 !=或<> 操作符,否则将引擎放弃使用 ...

  4. 【Vue】Java后端程序员也必须掌握的前端框架(下)

    Vue基础 前言 十一.自定义事件内容分发 十二.vue-cli 1.安装 vue-cli 2.第一个 vue-cli 应用程序 3.Vue-cli目录结构 十三.Vue的Webpack 十四.vue ...

  5. Java后端程序员1年工作经验总结

    java后端1年经验和技术总结(1) 1.引言 毕业已经一年有余,这一年里特别感谢技术管理人员的器重,以及同事的帮忙,学到了不少东西.这一年里走过一些弯路,也碰到一些难题,也受到过做为一名开发却经常为 ...

  6. 聊聊后端程序员的知识体系-第一篇

    聊聊后端程序员的知识体系-第一篇 原文链接:https://www.fpthinker.com/backend_knowledge_architecture/knowledge.htmll 亲爱的读者 ...

  7. Java后端程序员1年工作经验和技术总结

    本文转载自:Java后端程序员1年工作经验和技术总结 1.引言  毕业已经一年有余,这一年里特别感谢技术管理人员的器重,以及同事的帮忙,学到了不少东西.这一年里走过一些弯路,也碰到一些难题,也受到过做 ...

  8. Java后端程序员技术栈

    Java后端程序员技术栈 它可以是知识提纲,便于快速复习与查阅 它也可以是你的学习规划,帮助小白快速了解学Java要走的路(当然你也可以选择搭配我的学习路线一起享用!) 相关链接: <gitee ...

  9. 后端程序员必备的 Linux 基础知识+常见命令(近万字总结)

    大家好!我是 Guide 哥,Java 后端开发.一个会一点前端,喜欢烹饪的自由少年. 今天这篇文章中简单介绍一下一个 Java 程序员必知的 Linux 的一些概念以及常见命令. 如果文章有任何需要 ...

  10. 英:程序员必知单词、语句、英文缩写汇总

    转自: 程序员必知单词.语句.英文缩写汇总 程序员必知单词.语句.英文缩写汇总 2018年11月06日 14:02:52 牟垚 阅读数:180 综述:便于类,函数命名,工作文档阅读而做的单词积累,还是 ...

最新文章

  1. 如何每天自动备份 SourceSafe (转)
  2. 框架SpringMVC笔记系列 二 传值
  3. 彻底禁用resource manager
  4. 用servlet类返回WEB-INF中的页面
  5. 基于rancher在线安装k8s集群
  6. CG CTF WEB COOKIE
  7. 全面解析 Netflix 的微服务架构设计
  8. java 支付宝 退款_Java 支付宝支付,退款,单笔转账到支付宝账户(支付宝支付)
  9. python定义一个_Python,包括定义一个类
  10. 美研计算机案例,美国研究生申请案例:耶鲁大学录取:计算机硕士【2010】
  11. ASD: Average Surface Distance
  12. Java修改Windows注册表
  13. 软件工程之信息系统集成
  14. 码农和程序员的区别,网友:月入三万以下全是码农!
  15. bitbucket配置_如何配置Bitbucket的ssh
  16. 牛客网刷题——斩获offer
  17. [生存志] 第145节 班固著汉书
  18. 两年老网站IP100 到底错哪儿了?
  19. python数据清洗笔记
  20. 6.Python常用第三方库—jieba库的使用(中文分词词库)

热门文章

  1. 蓝桥杯 Java 自行车停放(双向链表解法)
  2. jib推送到harbor私有仓库并拉取镜像
  3. 2014 年度 Git@OSC【非GitHub】 最热门的 50 个项目
  4. 全国计算机二级ms备考,全国计算机二级MS office经验分享
  5. QQ邮箱搜索器 邮箱地址批量搜索
  6. 外汇期货市场的组织结构
  7. 程序猿生存指南-51 杭城相会
  8. c语言迷宫闯关游戏大全,C语言实现迷宫小游戏
  9. 2019杭电多校训练营(第一场)
  10. 华科计算机学院专业课,华中科技大学计算机专业课程表.xls