背景
项目试用SpringBoot+redisTemplate执行redis的lua脚本,实现令牌桶;redis结构使用的是1主2从3哨兵模式+读写分离;
问题分析
READONLY You can't write against a read
报这个错的含义在从节点执行了写操作,也就是说我执行Lua脚本是在从节点上执行的,那么问题来了,为什么的我LUA脚本会在从节点执行呢?
我们都知道,redis的主从哨兵模式,再加上配置读写分离,会将读操作优先分配到从节点,也就是说它认为LUA脚本是读操作,看一下具体的报错信息:
1、我们从这一行开始看起,找找是哪一步给我分配到了从节点
at org.springframework.data.redis.core.script.DefaultScriptExecutor.eval(DefaultScriptExecutor.java:77)
之后一步一步点击调用,到下面这一步,调用了get方法获取前面的返回值,那就继续看前面是怎么调用的,点击RedisScriptingAsyncCommands::evalsha
方法继续向下寻找
2、再继续
3、再继续,可以看到,命令类型给了一个 EVALSHA
4、回到第二部看 dispatch 方法的调用,找到处理链接的地方
5、可以看到write方法中,有一个步骤判断操作类型是读还是写
6、经过查找,CommandType类中包含 EVALSHA 类型,所以判断为ReadOlny,由此可以验证,确实是使用了从节点执行lua脚本。
解决问题
既然它使用的是从节点,那我就想办法让他在执行脚本的时候,强制选择主节点就能解决问题了呀。
修改redisTemplate注入配置
@Configuration
public class MyRedisConfig {
@Value("${spring.redis.sentinel.master}")
private String masterName;
@Value("${spring.redis.sentinel.nodes}")
private String sentinelNodes;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.database}")
private Integer database;
@Bean(value = "qpsRedisTemplate")
public RedisTemplate qpsRedisTemplate() {
List<String> sentinels = Arrays.asList(sentinelNodes.split(","));
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
sentinelConfig.master(masterName);
Set<RedisNode> sentinelNodes = new HashSet<>();
for (String sentinel : sentinels) {
String[] split = sentinel.split(":");
sentinelNodes.add(new RedisNode(split[0],Integer.parseInt(split[1])));
}
sentinelConfig.setSentinels(sentinelNodes);
sentinelConfig.setDatabase(database);
sentinelConfig.setPassword(password);
LettuceConnectionFactory factory = new LettuceConnectionFactory(sentinelConfig);
factory.afterPropertiesSet();
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
//读写分离配置
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
}