一、问题环境介绍
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();