SpringBoot + Redis 執行lua腳本


1、背景

有時候,我們需要一次性操作多個 Redis 命令,但是 這樣的多個操作不具備原子性,而且 Redis 的事務也不夠強大,不支持事務的回滾,還無法實現命令之間的邏輯關系計算。所以,一般在開發中,我們會利用 lua 腳本來實現 Redis 的事務。

2、lua 腳本

Redis 中使用 lua 腳本,我們需要注意的是,從 Redis 2.6.0后才支持 lua 腳本的執行。
使用 lua 腳本的好處:

  • 原子操作:lua腳本是作為一個整體執行的,所以中間不會被其他命令插入。
  • 減少網絡開銷:可以將多個請求通過腳本的形式一次發送,減少網絡時延。
  • 復用性:lua腳本可以常駐在redis內存中,所以在使用的時候,可以直接拿來復用,也減少了代碼量。

3、Redis 中執行 lua 腳本

1、命令格式:

EVAL script numkeys key [key ...] arg [arg ...]

說明:

  • script是第一個參數,為Lua 5.1腳本(字符串)。
  • 第二個參數numkeys指定后續參數有幾個key。
  • key [key ...],被操作的key,可以多個,在lua腳本中通過KEYS[1], KEYS[2]獲取
  • arg [arg ...],參數,可以多個,在lua腳本中通過ARGV[1], ARGV[2]獲取。

2、如果直接使用 redis-cli命令:

redis-cli --eval lua_file key1 key2 , arg1 arg2 arg3

說明:

  • eval 命令后不再是 lua 腳本的字符串形式,而是一個 lua 腳本文件。后綴為.lua
  • 不再需要numkeys參數,而是用 , 隔開多個key和多個arg

4、使用 RedisTemplate 執行 lua 腳本

例子:刪除 Redis 分布式鎖
引入依賴:此依賴為我們整合了 Redis ,並且提供了非常好用的 RedisTemplate。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

方式一:lua 腳本文件
1、新建 lua 腳本文件:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

說明:先獲取指定key的值,然后和傳入的arg比較是否相等,相等值刪除key,否則直接返回0。

2、代碼測試:

/**
 * @author Howinfun
 * @desc lua 測試
 * @date 2019/11/5
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@SpringBootTest(classes = ThirdPartyServerApplication.class)
public class RedisTest {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Test
    public void contextLoads() {
        String lockKey = "123";
        String UUID = cn.hutool.core.lang.UUID.fastUUID().toString();
        boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey,UUID,3, TimeUnit.MINUTES);
        if (!success){
            System.out.println("鎖已存在");
        }
        // 執行 lua 腳本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // 指定 lua 腳本
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/DelKey.lua")));
        // 指定返回類型
        redisScript.setResultType(Long.class);
        // 參數一:redisScript,參數二:key列表,參數三:arg(可多個)
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),UUID);
        System.out.println(result);
    }
}

方式二:直接編寫 lua 腳本(字符串)
1、代碼測試:

/**
 * @author Howinfun
 * @desc lua 腳本測試
 * @date 2019/11/5
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@SpringBootTest(classes = ThirdPartyServerApplication.class)
public class RedisTest {

    /** 釋放鎖lua腳本 */
    private static final String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Test
    public void contextLoads() {
        String lockKey = "123";
        String UUID = cn.hutool.core.lang.UUID.fastUUID().toString();
        boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey,UUID,3, TimeUnit.MINUTES);
        if (!success){
            System.out.println("鎖已存在");
        }
        // 指定 lua 腳本,並且指定返回值類型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT,Long.class);
        // 參數一:redisScript,參數二:key列表,參數三:arg(可多個)
        Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),UUID);
        System.out.println(result);
    }
}

注意:可能會有同學發現,為什么返回值不用 Integer 接收而是用 Long。這里是因為 spring-boot-starter-data-redis 提供的返回類型里面不支持 Integer。
大家可以看看源碼:

/**
 * Represents a data type returned from Redis, currently used to denote the expected return type of Redis scripting
 * commands
 *
 * @author Jennifer Hickey
 * @author Christoph Strobl
 */
public enum ReturnType {

    /**
     * Returned as Boolean
     */
    BOOLEAN,

    /**
     * Returned as {@link Long}
     */
    INTEGER,

    /**
     * Returned as {@link List<Object>}
     */
    MULTI,

    /**
     * Returned as {@literal byte[]}
     */
    STATUS,

    /**
     * Returned as {@literal byte[]}
     */
    VALUE;

    /**
     * @param javaType can be {@literal null} which translates to {@link ReturnType#STATUS}.
     * @return never {@literal null}.
     */
    public static ReturnType fromJavaType(@Nullable Class<?> javaType) {

        if (javaType == null) {
            return ReturnType.STATUS;
        }
        if (javaType.isAssignableFrom(List.class)) {
            return ReturnType.MULTI;
        }
        if (javaType.isAssignableFrom(Boolean.class)) {
            return ReturnType.BOOLEAN;
        }
        if (javaType.isAssignableFrom(Long.class)) {
            return ReturnType.INTEGER;
        }
        return ReturnType.VALUE;
    }
}

所以當我們使用 Integer 作為返回值的時候,是報以下錯誤:

org.springframework.data.redis.RedisSystemException: Redis exception; nested exception is io.lettuce.core.RedisException: java.lang.IllegalStateException

    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:74)
    at org.springframework.data.redis.connection.lettuce.LettuceExceptionConverter.convert(LettuceExceptionConverter.java:41)

 

 

 

 參考文章:

https://www.cnblogs.com/Howinfun/p/11803747.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM