目录
- 说明
- 什么是Lua脚本
- 为什么要使用Lua脚本
- Lua脚本的安装
- Lua脚本的使用
- Lua的变量
- Lua脚本的算术运算符
- Lua脚本的关系运算符
- Lua脚本的逻辑运算符
- Lua脚本不同的操作
- Lua脚本的函数和标准库
- Redis整合Lua脚本(重点)
- 在Java集成Lua
- 在SpringBoot项目中使用Redis集成Lua
说明
我学习Lua脚本的初衷是为为了解决Redis分布式锁中出现的问题,Lua脚本具有原子型,要么同时执行,要么同时失败,所以某一些操作,特别是我们在分布式环境下的一些操作,我们需要让他具有原子性,要不然就会出现问题。
说明:Redis在2.6版本之后,支持在Redis里面写Lua脚本,也就是可以在Redis里面调用Lua脚本。
什么是Lua脚本
Lua脚本是一个小巧的轻量级的脚本语言,它的设计目的你说为了通过嵌入到应用程序中从而为应用程序提供拓展和定制功能。Lua是由C语言来编写的,几乎所有的操作系统和平台都可以编译和运行Lua脚本语言。在所有脚本引擎中,Lua的速度是最快的。所有它才被作为嵌入式脚本语言。
Lua脚本很容易被C、C++代码调用,也可以反过来调用C、C++代码。
为什么要使用Lua脚本
我们使用一个东西,我们需要去知道,我们为什么要用这个东西,这个东西对我们来说有什么作用。
使用Lua脚本的好处:
1、减少网络开销。
本来我们在Redis里面需要去执行三四个命令,但是在Lua脚本里面我们只需要调用一个就可以达到我们的目的。
2、Redis里面,对Lua脚本执行是一个原子性的操作,要么同时执行,要么同时失败。
3、具有复用性。
Lua脚本的安装
我们这次使用 Linux 的Wget来安装Lua。在我们安装Lua之前,我们需要去搞懂Linux中的Wget命令:Linux的Wget命令。
获取Lua RPM包的地址:https://pkgs.org/download/lua(x86-64)
我们将它下载到我们的Linux环境中,然后进行RPM安装,顺便熟悉一下RPM安装的指令:
wget -P /opt/software/ http://mirror.centos.org/centos/7/os/x86_64/Packages/lua-5.1.4-15.el7.x86_64.rpm
下载之后安装,使用RPM环境会帮我们自动进行配置(这用来安装Java环境挺好的,不用去配置)
rpm -ivh lua-5.1.4-15.el7.x86_64.rpm
安装完成之后,我们使用lua命令进入Lua客户端,具体流程如图
Lua脚本的使用
Lua脚本语言是一种动态类型的语言,变量的类型是根据值去推到的,不像我们Java的强变量类型。
Lua的变量
全局变量、局部变量
全局变量:a = 1
局部变量:local b = 1
数组的定义
local xx = {“a”,“b”,“c”}
Lua脚本的算术运算符
+、-、*、/是和其他语言一样的
Lua脚本的关系运算符
== :判断两个数是否相等
~=:不等于
Lua脚本的逻辑运算符
and / or (相当于Java中的&& / || )
not (a and b)
Lua脚本不同的操作
1、连接两个字符串
local a = "hello"
local b = "world"
print(a..b)
2、计算当前字符串的长度 #xxx
a = "hello"
b = "world"
print(#a..b)
逻辑判断:
if expression then
elseif expression then
else
end
循环语句:
for i=1,100 do
print(i)
end
foreach语句:
xx = {"a","b","c"}
for i,v in ipairs(xx) do
print(i)
end
Lua脚本的函数和标准库
函数模板:
-function 函数名(params....)
return(可以有可以没有)
end
函数名 = function(params....)
return(可以有可以没有)
end
String 、Table(对数组的操作)
Redis整合Lua脚本(重点)
在Lua脚本里面是可以调用Redis中的命令的。
redis.call(‘set’,‘key’,value)
redis.call(‘get’,‘key’)
Redis中如何整合Lua脚本?
在Redis中内置了Lua脚本,所以我们在Redis中使用Lua脚本,我们需要使用命令:
eval 脚本内容 keynumbers key… args…
如果我们Lua脚本中有key和参数,是根据我们的KEYS[] 和 ARGV[]来获取
eval “redis.call(‘set’,‘name’,‘liu’)” keynumbers
redis客户端调用lua脚本时候命令:
redis-cli --eval luaFileDir key1 key2… (这里需要空格),(这里需要空格) value1 value2…
才不会报错
这里需要说明一下:Redis在运行Lua脚本的时候,不能运行Redis的其他命令,这样保证了Lua脚本的原子性。
在Java集成Lua
在普通的Java工程中,由于Redis中内置了Lua脚本,所以我们可以使用Jedis来调用Lua。
使用Jedis来运行Lua脚本有两种方式:
1、直接使用jedis.eval(String LuaScript,List KEYS,List ARGV)
2、使用jedis.scriptLoad(String LuaScript)将Lua脚本缓存到Redis中并返回一个经过sha加密的摘要,然后我们使用jedis.evalsha(String sha,List KEYS,List ARGV)来运行,这样子就不用每次都将Lua脚本网络传输到远程的Redis中,减少网络IO,经过测试,确实是缓存到Redis中了。
举个例子:
写一个限制一个IP地址访问的次数(或者是防止手机注册或者是登录刷短信,我们可以使用这个方法来限制)
ip_limit.lua
local num = redis.call('incr',KEYS[1])
if tonumber(num) == 1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num) > tonumber(ARGV[2])
return 0
else
return 1
end
Jedis方式一:
1、通过类加载器获取Java工程项目中resources下的ip_limit.lua资源,并将该Lua脚本转换为字符串。
2、通过jedis.eval(String LuaScript,List KEYS,List ARGV),来运行
public class LuaTest {
public static void main(String[] args) throws IOException {
Jedis jedis = JedisUtil.getJedisConnection();
String fileName = LuaTest.class.getClassLoader().getResource("ip_limit.lua").getPath();//获取文件路径
String lua = fileToString2(fileName);
String sha = jedis.scriptLoad(lua);
List<String> keys = new ArrayList<String>();
keys.add("127.0.0.1");
List<String> argv = new ArrayList<String>();
argv.add("60000");
argv.add("5");
System.out.println(jedis.evalsha(sha, keys, argv));
}
public static String fileToString2(String path) throws IOException {
FileReader reader = new FileReader(new File(path));
StringBuilder stringBuilder = new StringBuilder();
char[] buffer = new char[1024];
int size;
while ((size = reader.read(buffer)) != -1) {
stringBuilder.append(buffer, 0, size);
}
return stringBuilder.toString();
}
}
方式二:
1、通过通过上述的方式来获取resources下的lua脚本然后转换称为字符串,之后通过jedis.scriptload(String LuaScript)将Lua脚本缓存到Redis中然后返回一个通过sha加密之后的摘要。(缓存到Redis之后,我们就不用再解析Lua脚本了直接使用那个sha加密之后的摘要就行,除非Redis重启)
2、然后通过jedis.evalsha(sha,List KEYS,List ARGV)来运行。
public class LuaTest {
public static void main(String[] args) throws IOException {
Jedis jedis = JedisUtil.getJedisConnection();
String fileName = LuaTest.class.getClassLoader().getResource("ip_limit.lua").getPath();//获取文件路径
String lua = fileToString2(fileName);
String sha = jedis.scriptLoad(lua);
List<String> keys = new ArrayList<String>();
keys.add("127.0.0.1");
List<String> argv = new ArrayList<String>();
argv.add("60000");
argv.add("5");
System.out.println(jedis.evalsha(sha, keys, argv));
}
public static String fileToString2(String path) throws IOException {
FileReader reader = new FileReader(new File(path));
StringBuilder stringBuilder = new StringBuilder();
char[] buffer = new char[1024];
int size;
while ((size = reader.read(buffer)) != -1) {
stringBuilder.append(buffer, 0, size);
}
return stringBuilder.toString();
}
}
在SpringBoot项目中使用Redis集成Lua
这里我先说一下我在集成的时候,遇到的坑:
1、由于急于集成,原本项目没有对RedisTemplate进行序列化配置,导致一直报错。所以解决方案是将Redis序列化配置
redisTemplate.opsForValue().increment()【自增和自减操作】就可能报错redis.clients.jedis.exceptions.JedisDataException:
ERR value is not an integer or out of range了。
2、DefaultRedisScript的构造器没有指定Long的反射对象,产生如下错误。
具体步骤:
1、配置好RedisTemplate的配置,Host、Port、序列化等。
2、如果将lua文件放到了resources下面,那么我们需要根据类加载器获取resources下面的Lua脚本的路径,然后根据流转成String.
3、创建DefaultRedisScript(LuaScript,Long.class)对象,然后调用redisTemplate.execute(redisScript,KEYS,Object…)
解决1:
package com.cheng.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* rides配置类
* @author 15594
*/
@EnableCaching
@Configuration
public class RidesConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
//是否开启事务,true为开启
template.setEnableTransactionSupport(true);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
解决2:
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript,Long.class);