Redis中使用Lua语言详谈

本文介绍如何在Redis中使用Lua脚本进行高效的数据操作,包括直接执行Lua代码、加载Lua脚本到Redis缓存、在Spring框架下执行Lua脚本操作对象,以及执行Lua文件。

在Redis的2.6以上版本中,除了可以使用命令外,还可以使用Lua语言操作Redis。
Redis支持两种方式运行脚本,一种是直接输入一些Lua语言的程序代码;另外一种使将Lua语言编写成文件。

初步认识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-num的个数对应起来。
  • [value1 value2 value3 …]:作为参数传递给Lua语言,它们也是可填可不填的。

这里我们看两个Lua脚本:

eval "return 'Hello World!'" 0

这个脚本只是返回一个字符串,并不需要任何参数,所以key-num填写了0,代表着没有任何key参数,执行结果就是返回 Hello World!。

eval "redis.call('set',KEYS[1],ARGV[1]" 1 lua-key lua-value

设置一个键值对,可以在Lua语言中采用redis.call(command,key[param1,param2…])进行操作,其中:

  • command是命令,包括set,get,del等。
  • key是被操作的键。
  • param1,param2…代表给key的参数。

有时候可能需要多次执行同样一段脚本,这个时候可以使用Redis缓存脚本的功能,在Redis中脚本会通过SHA-1签名算法加密脚本,然后返回一个标识字符串,可以通过这个字符串执行加密后的脚本。
首先使用命令:

script load script

这个脚本的返回值是一个SHA-1签名过后的标识字符串,我们把它记为shastring。通过shastring可以使用命令执行签名后的脚本,命令的格式是:

evalsha shastring keynum [key1 key2 key3 ...] [param1 param2 param3 ...]

下面我们在Spring中演示这个过程。

这里我们先定义一个可序列化的对象Role,因为要序列化所以需要实现Serializable接口,代码如下:

package com.ssm.lua.pojo;

import java.io.Serializable;

public class Role implements Serializable {
    private static final long serialVersionUID=7247714666080613254L;
    private Long id;
    private String roleName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="50" />
        <property name="maxTotal" value="100" />
        <property name="maxWaitMillis" value="20000" />
    </bean>

    <bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />

    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="localhost" />
        <property name="port" value="6379" />
        <property name="password" value="123456" />
        <property name="poolConfig" ref="poolConfig" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory" />
        <property name="defaultSerializer" ref="stringRedisSerializer" />
        <property name="keySerializer" ref="stringRedisSerializer" />
        <property name="valueSerializer" ref="stringRedisSerializer" />
    </bean>
</beans>

这个时候就可以通过Spring提供的DefaultRedisScript对象执行Lua脚本来操作对象,代码如下:

package com.ssm.lua.test;

import com.ssm.lua.pojo.Role;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;

import java.util.ArrayList;
import java.util.List;

public class testLua {
    public static void main(String[] args){
        ApplicationContext applicationContext=new ClassPathXmlApplicationContext("redisSpring-cfg.xml");
        RedisTemplate redisTemplate=applicationContext.getBean(RedisTemplate.class);
        //定义默认脚本封装类
        DefaultRedisScript<Role> redisScript=new DefaultRedisScript<>();
        //设置脚本
        redisScript.setScriptText("redis.call('set',KEYS[1],ARGV[1]) return redis.call('get',KEYS[1])");
        //定义操作的key列表
        List<String> keyList=new ArrayList<>();
        keyList.add("role1");
        //需要序列化保存和读取的对象
        Role role=new Role();
        role.setId(1L);
        role.setRoleName("role_name_1");
        role.setNote("role_note_1");
        //获取标识字符串
        String shastring=redisScript.getSha1();
        System.out.println(shastring);
        //设置返回结果的类型,如果没有这句话,结果返回为空。
        redisScript.setResultType(Role.class);
        //定义序列化器
        JdkSerializationRedisSerializer serializer=new JdkSerializationRedisSerializer();
        //执行脚本
        //第一个是RedisScript接口对象,第二个是参数序列化器
        //第三个是结果序列化器,第四个是Redis的key列表,最后是参数列表
        Role obj=(Role)redisTemplate.execute(redisScript,serializer,serializer,keyList,role);
        //打印结果
        System.out.println(obj);
    }
}

执行Lua文件

在上面我们把Lua变成一个字符串传递给Redis执行,而有些时候要直接执行Lua文件,尤其是当Lua脚本存在很多逻辑的时候,就很有必要单独编写一个独立的Lua文件:

redis.call('set',KEYS[1],ARGV[1])
redis.call('set',KEYS[2],ARGV[2])
local n1=tonumber(redis.call('get',KEYS[1]))
local n2=tonumber(redis.call('get',KEYS[2]))
if n1>n2 then
	return 1
end
if n1==n2 then
	return 0
end
if n1<n2 then
	return 2
end

这是一个可以输入两个键和两个数字(记为n1和n2)的脚本,其意义就是先按键保存两个数字,然后去比较这两个数字的大小。且把它以文件名test.lua保存起来。这个时候可以对其进行测试。

redis-cli --eval test.lua key1 key2 , 2 4

这里需要非常注意命令,执行的命令键和参数是要使用逗号隔开的,而键之间用空格分开。而上面的逗号前后的空格是不能省略的,这是要非常注意的地方。

Redis使用 Lua 脚本可以**实现原子性操作**,避免多命令竞态条件,同时减少网络开销(只需一次通信)。以下是详细使用方法及示例: --- ## **一、Lua 脚本基础语法** ### **1. 脚本执行命令** ```bash # 直接执行脚本 EVAL "return 'Hello, Redis!'" 0 # 使用参数 EVAL "return ARGV[1]" 0 "Hello" ``` - **`EVAL`**:执行脚本的核心命令。 - **参数说明**: - 第一个参数:Lua 脚本内容。 - 第二个参数:`KEYS` 的数量(即使不使用也必须声明)。 - 后续参数:`ARGV` 数组传递的额外参数。 --- ### **2. 键(KEYS)与参数(ARGV)** - **`KEYS`**:用于标识 Redis 中的键(保证原子性操作的资源)。 - **`ARGV`**:其他非键参数。 ```bash EVAL "return {KEYS[1], ARGV[1]}" 1 "user:1001" "name" ``` **输出**: ```json ["user:1001", "name"] ``` --- ## **二、实际应用场景与示例** ### **1. 原子性计数器递增** **需求**:实现一个带过期时间的计数器,避免竞态条件。 ```lua -- KEYS[1]: 计数器键 -- ARGV[1]: 过期时间(秒) -- ARGV[2]: 递增步长 local current = redis.call('GET', KEYS[1]) if not current then redis.call('SET', KEYS[1], 0, 'EX', ARGV[1]) end return redis.call('INCRBY', KEYS[1], ARGV[2]) ``` **执行命令**: ```bash EVAL "local current=redis.call('GET',KEYS[1]);if not current then redis.call('SET',KEYS[1],0,'EX',ARGV[1]) end;return redis.call('INCRBY',KEYS[1],ARGV[2])" 1 "counter:page_view" 60 1 ``` --- ### **2. 分布式锁实现** **需求**:获取锁时设置过期时间,避免死锁。 ```lua -- KEYS[1]: 锁键 -- ARGV[1]: 锁值(唯一标识) -- ARGV[2]: 过期时间(毫秒) if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 -- 获取锁成功 else return 0 -- 获取锁失败 end ``` **执行命令**: ```bash EVAL "if redis.call('SETNX',KEYS[1],ARGV[1])==1 then redis.call('PEXPIRE',KEYS[1],ARGV[2]);return 1 else return 0 end" 1 "lock:order" "uuid-1234" 5000 ``` --- ### **3. 限流器(滑动窗口)** **需求**:限制用户每分钟最多 10 次操作。 ```lua -- KEYS[1]: 限流键(如 "rate_limit:user123") -- ARGV[1]: 当前时间戳(毫秒) -- ARGV[2]: 窗口大小(毫秒,如 60000) -- ARGV[3]: 最大请求数(如 10) local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) -- 清除过期请求 redis.call('ZREMRANGEBYSCORE', key, 0, now - window) -- 获取当前请求数 local count = redis.call('ZCARD', key) if count < limit then -- 允许请求,记录时间戳 redis.call('ZADD', key, now, now) redis.call('PEXPIRE', key, window) return 1 -- 允许 else return 0 -- 拒绝 end ``` **执行命令**: ```bash EVAL "local key=KEYS[1];local now=tonumber(ARGV[1]);local window=tonumber(ARGV[2]);local limit=tonumber(ARGV[3]);redis.call('ZREMRANGEBYSCORE',key,0,now-window);local count=redis.call('ZCARD',key);if count<limit then redis.call('ZADD',key,now,now);redis.call('PEXPIRE',key,window);return 1 else return 0 end" 1 "rate_limit:user123" 1630000000000 60000 10 ``` --- ## **三、性能优化技巧** ### **1. 使用 `SCRIPT LOAD` + `EVALSHA`** - **减少网络传输**:先加载脚本生成 SHA1 摘要,后续通过摘要调用。 ```bash # 加载脚本 SCRIPT LOAD "return redis.call('GET', KEYS[1])" # 返回: "abc123sha1hash..." # 通过 SHA1 执行 EVALSHA "abc123sha1hash..." 1 "user:1001" ``` ### **2. 避免长脚本** - Redis 是单线程模型,长脚本会阻塞其他命令。 - 复杂逻辑拆分为多个短脚本。 ### **3. 错误处理** - Lua 脚本中调用 Redis 命令出错时会抛出异常: ```lua local ok, err = pcall(redis.call, 'GET', 'nonexistent_key') if not ok then return "Error: " .. err end ``` --- ## **四、Java 集成示例(Spring Data Redis)** ```java import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; // 定义 Lua 脚本 String luaScript = "return redis.call('GET', KEYS[1])"; // 创建脚本对象 DefaultRedisScript<String> script = new DefaultRedisScript<>(); script.setScriptText(luaScript); script.setResultType(String.class); // 执行脚本 String result = redisTemplate.execute( script, Collections.singletonList("user:1001"), // KEYS "optional_arg" // ARGV ); ``` --- ## **五、注意事项** 1. **原子性**:Lua 脚本Redis 中单线程执行,天然原子性。 2. **调试**:脚本中可用 `redis.log(redis.LOG_NOTICE, "Debug message")` 输出日志。 3. **资源限制**: - 脚本默认最长执行时间:5秒(可通过 `lua-time-limit` 配置)。 - 超出限制后,Redis 会记录警告但**不会终止脚本**(需用 `SCRIPT KILL` 手动干预)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值