使用Redis的Lua脚本实现CAS原子性操作

342 阅读3分钟

一、redis使用lua脚本的优势

1. 原子性操作: Lua脚本在Redis中执行是原子操作,可以保证多个命令的执行不会被其他命令插入,确保数据操作的一致性和完整性。在实际使用中,尤其并发场景,想要保持原子性如果是使用redis,可以使用lua脚本来保证原子性。

2. 减少网络通信: 将多个操作合并为一个Lua脚本,可以减少客户端与Redis服务器之间的网络通信次数,提高系统的性能和效率。

3. 复杂业务逻辑支持: Lua脚本可以支持复杂的业务逻辑,在数据库中执行操作,提高了Redis的功能性和灵活性。

4. 执行效率高: Lua脚本是在Redis服务器端执行的,不需要将数据传输到客户端再传输回来,可以减少数据传输的时间,提高执行效率。

5. 可以减少内存开销: 通过Lua脚本可以在Redis中实现常见的数据处理逻辑,减少了客户端的内存开销

二、代码实现

说明: 本来准备放在 ape-common-redis 中的,但是想想可能每个模块都有自己的相关需求,就放在了业务模块,不过可以将Lua初始化部分抽取为工具类(此处后期可自行优化)!

2.1 RedisLua工具类

说明: 用于初始化RedisLua脚本的初始环境,以及编写了一个 CAS 的Demo!

package com.ssm.user.redislua;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

//RedisLua工具类用于初始化RedisLua脚本的初始环境
@Component
@Slf4j
public class CompareAndSetLua {

    @Resource
    private RedisTemplate redisTemplate;

    private DefaultRedisScript<Boolean> casScript;

    //初始化环境
    @PostConstruct 
    public void init() {
        casScript = new DefaultRedisScript<>();
        casScript.setResultType(Boolean.class); 
        casScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("compareAndSet.lua")));
    }

    //调用Lua脚本
    public boolean compareAndSet(String key, Long oldValue, Long newValue) {
        //lua中接收key的为KEYS数组类型,所以我们要传递集合类型
        List<String> keys = new ArrayList<>();
        keys.add(key);
        //casScript在bean初始化时已赋值
        Boolean result = (Boolean) redisTemplate.execute(casScript, keys, oldValue, newValue);
        return result;
    }

}

重点解释

  • DefaultRedisScript 是Spring Data Redis提供的一个类,用于封装Redis脚本的执行

  • @PostConstruct注解 在Spring容器初始化这个Bean之后自动执行。这样做的目的是在Bean的实例化之后,立即执行一些初始化操作,比如加载Redis脚本。

  • setResultType(Boolean.class) 设置脚本执行后返回的结果类型

  • setScriptSource 指定脚本的来源

  • ClassPathResource 用于加载类路径下的资源

2.2 相关Lua脚本

--在此key下,如果传入的oldValue和存在的值相同,则更新为newValue,否则不变!
local key = KEYS[1]
local oldValue = ARGV[1]
local newValue = ARGV[2]

local redisValue = redis.call('get', key)
if (redisValue == false or tonumber(redisValue) == tonumber(oldValue))
then
    redis.call('set', key, newValue)
    return true
else
    return false
end

2.3 运行测试

package com.ssm.user;

import com.ssm.user.redislua.CompareAndSetLua;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@SpringBootTest(classes = {UserApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RunWith(SpringRunner.class)
@Slf4j
public class RedisLuaTest {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private CompareAndSetLua compareAndSetLua;

    @Test
    public void redisLuaTest() {
        //先往redis存入一个数据(RedisUtils中未封装 set Long类型的值)
        ValueOperations<String, Long> valueOperations = redisTemplate.opsForValue();
        valueOperations.set("age", 18L);
        log.info("age的值为:{}", valueOperations.get("age"));

        boolean result = compareAndSetLua.compareAndSet("age", 18L, 19L);
        if (result) {
            log.info("修改成功!age的值为:{}", valueOperations.get("age"));
        }
    }
}

image.png