使用redisTemplate集群模式下执行lua脚本报错的问题解决过程

7 篇文章 0 订阅
5 篇文章 0 订阅

一、问题环境介绍

1.1 pom引入

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

1.2 redisTemplate配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

/**
 * @author :cw
 * @date :Created in 2020/6/12 下午2:35
 * @description:
 * @modified By:
 * @version: $
 */

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(@Qualifier("requestFactory") ClientHttpRequestFactory clientHttpRequestFactory){
        return new RestTemplate(clientHttpRequestFactory);
    }

    @Bean("requestFactory")
    public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);//单位为ms
        factory.setConnectTimeout(5000);//单位为ms
        return factory;
    }
}

以上默认使用 jdk中的HttpURLConnection连接,没有使用连接池,可使用HttpClient进行连接池配置
1.3 application-local.yml配置

spring:
  redis:
    database: xx
    host: xxx.xx.xx
    password: xx
    port: xxxx
    lettuce:
      pool:
        max-active: 50
        max-idle: 30
        min-idle: 30

1.3 执行lua脚本时出现问题配置如下

StringBuilder sb = new StringBuilder();
    sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
    sb.append("    if (tonumber(redis.call('get',KEYS[1])) == -1) then");
    sb.append("        return -1;");
    sb.append("    end;");
    sb.append("    if (tonumber(redis.call('get',KEYS[1])) >= tonumber(ARGV[1])) then");
    sb.append("        return redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1]));");
    sb.append("    end;");
    sb.append("    return -2;");
    sb.append("end;");
    sb.append("return -3;");
String  redisScript= sb.toString();
// 脚本里的KEYS参数
List<String> keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List<String> args = new ArrayList<>();
args.add("-1");
long result = (long) redisTemplate.execute(redisScript, keys, args);

这时候执行lua时,redis.call(‘exists’, KEYS[1])执行成功,即表示对应的key存在,但是redis.call(‘get’,KEYS[1])执行会一直拿不到这个结果(查阅信息了解到阿里云对lua这块做了限制或是其它集群模式下的solt等原因),返回结果为nil,所以再执行则会一直报错

二、解决方法

2.1 方法描述

通过redisTemplate去拿到redis的connection对象,再通过connnection对象去执行。

2.2 方法步骤一:pom引入和改变

<!-- redis-->
<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.0.2.RELEASE</version>
    <exclusions>
        <!-- 不依赖Redis的异步客户端lettuce -->
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

上面主要是为了解决 io.lettuce.core.RedisAsyncCommandsImpl cannot be cast to redis.clients.jedis.JedisCluster,这样的问题

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

2.2 方法步骤二:代码改变

StringBuilder sb = new StringBuilder();
    sb.append("if (redis.call('exists', KEYS[1]) == 1) then");
    sb.append("    if (tonumber(redis.call('get',KEYS[1])) == -1) then");
    sb.append("        return -1;");
    sb.append("    end;");
    sb.append("    if (tonumber(redis.call('get',KEYS[1])) >= tonumber(ARGV[1])) then");
    sb.append("        return redis.call('incrby', KEYS[1], 0 - tonumber(ARGV[1]));");
    sb.append("    end;");
    sb.append("    return -2;");
    sb.append("end;");
    sb.append("return -3;");
String  redisScript= sb.toString();
// 脚本里的KEYS参数
List<String> keys = new ArrayList<>();
keys.add(key);
// 脚本里的ARGV参数
List<String> args = new ArrayList<>();
args.add("-1");



// 主要部门如下
Long resultLong = (Long) redisTemplate.execute((RedisCallback) connection -> {
        logger.info("doInRedis方法执行");
        Object nativeConnection = connection.getNativeConnection();
        logger.info("获取原redis对象{}",nativeConnection);
        if(nativeConnection instanceof JedisCluster){
            logger.info("集群情况执行");
            Long result = (Long) ((JedisCluster) nativeConnection).eval(stock_lua, keys, args);
            return result;
        }
        else if (nativeConnection instanceof Jedis){
            logger.info("单机情况执行");
            return (Long) ((Jedis) nativeConnection).eval(stock_lua,keys,args);
        }else{
            logger.info("其它情况执行");
            Long result = (Long) ((JedisCluster) nativeConnection).eval(stock_lua, keys, args);
            return result;
        }
    });

分析:执行后看执行日志步骤发现当前的redis原对象类型RedisAsyncCommands类型,且已知环境是集群模式,所以redis会自动走到“单机情况执行”,最后执行成功

三、lua参数keys引发的问题和解决办法

问题描述

在集群下,它会将数据自动分布到不同的节点(虚拟的16384个slot)
它数据的路由分发,是通过计算key,所以只要key一样,则一定会被分到同一个slot
上面的参数keys的个数为1,肯定存储在一个slot里面,
但是如果是多个key且不存在同一个slot里面则会报错,
redis.clients.jedis.exceptions.JedisDataException: ERR 'EVAL' command keys must in same slot**

解决办法

在设置key的时候使用{xxxxx}的方式可以将该数据存在用一个slot里面,
如下两个key就是存储在同一个key里面。
但是,注意如果同一个slot里面的数据过多且同时操作查询频繁的数据就会影响到效率问题。

补充lua脚本(仅供自己参考)

StringBuilder sb = new StringBuilder();
    sb.append("if ((redis.call('exists', KEYS[1]) == 1) and (redis.call('exists', KEYS[2]) == 1)) then");
    sb.append("    local totalStock = tonumber(redis.call('get',KEYS[1]));");
    sb.append("    local pickPointStock = tonumber(redis.call('hget',KEYS[2], ARGV[3]));");
    sb.append("    local argNum = tonumber(ARGV[1]);");
    sb.append("    local argGoodsId = tonumber(ARGV[2]);");
    sb.append("    if ( totalStock <= 0 or totalStock < argNum ) then");
    sb.append("        return -1;");
    sb.append("    end;");
    sb.append("    if ( argGoodsId == 1 or argGoodsId==2 ) then");
    sb.append("        if (pickPointStock <= 0 or pickPointStock < argNum) then");
    sb.append("            return -2;");
    sb.append("        end;");
    sb.append("        redis.call('hincrby', KEYS[2],ARGV[3], 0 - argNum);");
    sb.append("    end;");
    sb.append("    return redis.call('incrby', KEYS[1], 0 - argNum);");
    sb.append("end;");
    sb.append("return -3;");
    return sb.toString();
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
在Spring Boot框架中,可以通过RedisTemplate执行Lua脚本。具体步骤如下: 1. 创建RedisScript对象 首先,需要创建一个RedisScript对象,用于表示要执行Lua脚本。这个对象包含两个参数:脚本文本和返回值类型。在上面的例子中,脚本文本可以直接使用字符串表示,返回值类型为Boolean.class,表示返回值为布尔类型。可以使用以下代码创建RedisScript对象: ``` RedisScript<Boolean> script = RedisScript.of("上述代码", Boolean.class); ``` 2. 执行Lua脚本 然后,可以使用RedisTemplate的execute方法来执行Lua脚本。这个方法有两个参数:RedisScript对象和RedisSerializer对象。在执行Lua脚本时,需要将Lua脚本的参数传递给execute方法。在上面的例子中,Lua脚本的参数是list集合的键和set集合的键。可以使用以下代码执行Lua脚本: ``` List<String> keys = new ArrayList<>(); keys.add("list_key"); keys.add("set_key"); Boolean result = redisTemplate.execute(script, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), keys); ``` 其中,第一个参数是RedisScript对象,第二个参数和第三个参数分别是键和值的序列化器。最后一个参数是Lua脚本的参数,使用List<String>类型表示。执行结果为布尔类型,表示是否添加了元素。 完整代码示例: ``` // 创建RedisScript对象 RedisScript<Boolean> script = RedisScript.of("上述代码", Boolean.class); // 执行Lua脚本 List<String> keys = new ArrayList<>(); keys.add("list_key"); keys.add("set_key"); Boolean result = redisTemplate.execute(script, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(), keys); ``` 其中,redisTemplate是已经注入的RedisTemplate对象,可以直接使用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值