springboot2.0 集成redis服务详解,以及 (Lettuce & Jedis)_zzhongcy的博客-CSDN博客_spring.redis.lettuce.pool.max前言在实际项目开发过程中,相信很多人都有用到过 redis 这个NoSQL,这篇文章就详细讲讲springboot如何整合 redisRedis 简介简单介绍下Redis:Redis是一个开源的使用 ANSI C语言编写,支持网络,可基于内存也可持久化的日志型,Key-Value数据库,并提供了多种语言的 API ,相比 Memcached 它支持存储的类型相对更多 (字符,哈希...https://blog.csdn.net/zzhongcy/article/details/102584028

 请原创作者喝咖啡

如果觉得文章写得不错,能对你有帮助,请支持下作者~~感谢~~

前言

在实际项目开发过程中,相信很多人都有用到过 redis 这个NoSQL,这篇文章就详细讲讲springboot如何整合 redis

Redis 简介

简单介绍下Redis:

Redis是一个开源的使用 ANSI C语言编写,支持网络,可基于内存也可持久化的日志型,Key-Value数据库,并提供了多种语言的 API ,相比 Memcached 它支持存储的类型相对更多 (字符,哈希,集合,有序集合,列表等),同时Redis是线程安全的。

Redis 连接池简介

在后面 springboot 整合 redis 的时候会用到连接池,所以这里先来介绍下 Redis中的连接池:

客户端连接 Redis 使用的是 TCP协议,直连的方式每次需要建立 TCP连接,而连接池的方式是可以预先初始化好客户端连接,所以每次只需要从 连接池借用即可,而借用和归还操作是在本地进行的,只有少量的并发同步开销,远远小于新建TCP连接的开销。另外,直连的方式无法限制 redis客户端对象的个数,在极端情况下可能会造成连接泄漏,而连接池的形式可以有效的保护和控制资源的使用。

下面以Jedis客户端为例,再来总结下 客户端直连方式和连接池方式的对比

优点缺点
直连简单方便,适用于少量长期连接的场景1. 存在每次新建/关闭TCP连接开销 2. 资源无法控制,极端情况下出现连接泄漏 3. Jedis对象线程不安全(Lettuce对象是线程安全的)
连接池1. 无需每次连接生成Jedis对象,降低开销 2. 使用连接池的形式保护和控制资源的使用相对于直连,使用更加麻烦,尤其在资源的管理上需要很多参数来保证,一旦规划不合理也会出现问题

Jedis vs Lettuce

redis官方提供的java client有如图所示几种:

redis

比较突出的是 Lettuce 和 jedis。Lettuce 和 jedis 的都是连接 Redis Server的客户端,Jedis 在实现上是直连 redis server,多线程环境下非线程安全,除非使用连接池,为每个 redis实例增加 物理连接。

Lettuce 是 一种可伸缩,线程安全,完全非阻塞的Redis客户端,多个线程可以共享一个RedisConnection,它利用Netty NIO 框架来高效地管理多个连接,从而提供了异步和同步数据访问方式,用于构建非阻塞的反应性应用程序。

在 springboot 1.5.x版本的默认的Redis客户端是 Jedis实现的,springboot 2.x版本中默认客户端是用 lettuce实现的。

Jedis 和 Lettuce 是 Java 操作 Redis 的客户端。在 Spring Boot 1.x 版本默认使用的是 jedis ,而在 Spring Boot 2.x 版本默认使用的就是Lettuce。关于 Jedis 跟 Lettuce 的区别如下:

  1. Jedis在实现上是直接连接的redis server,如果在多线程环境下是非线程安全的,这个时候只有使用连接池,为每个Jedis实例增加物理连接
  2. Lettuce的连接是基于Netty的,连接实例(StatefulRedisConnection)可以在多个线程间并发访问,应为StatefulRedisConnection是线程安全的,所以一个连接实例(StatefulRedisConnection)就可以满足多线程环境下的并发访问,当然这个也是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。

springboot 2.0 通过 lettuce集成Redis服务

导入依赖

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

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

</dependencies>

application.properties配置文件

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=root
# 连接池最大连接数(使用负值表示没有限制) 默认为8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
spring.redis.lettuce.pool.max-wait=-1ms
# 连接池中的最大空闲连接 默认为8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认为 0
spring.redis.lettuce.pool.min-idle=0
复制代码

自定义 RedisTemplate

默认情况下的模板只能支持 RedisTemplate<String,String>,只能存入字符串,很多时候,我们需要自定义 RedisTemplate ,设置序列化器,这样我们可以很方便的操作实例对象。如下所示:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}
复制代码

定义测试实体类

public class User implements Serializable {
    private static final long serialVersionUID = 4220515347228129741L;
    private Integer id;
    private String username;
    private Integer age;

    public User(Integer id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    public User() {
    }
    //getter/setter 省略
}
复制代码

测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisTest {
    private Logger logger = LoggerFactory.getLogger(RedisTest.class);
    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    @Test
    public void test() {
        String key = "user:1";
        redisTemplate.opsForValue().set(key, new User(1,"pjmike",20));
        User user = (User) redisTemplate.opsForValue().get(key);
        logger.info("uesr: "+user.toString());
    }
}
复制代码

springboot 2.0 通过 jedis 集成Redis服务

导入依赖

因为 springboot2.0中默认是使用 Lettuce来集成Redis服务,spring-boot-starter-data-redis默认只引入了 Lettuce包,并没有引入 jedis包支持。所以在我们需要手动引入 jedis的包,并排除掉 lettuce的包,pom.xml配置如下:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

application.properties配置

使用jedis的连接池

spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=root
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
spring.redis.jedis.pool.max-active=8

配置 JedisConnectionFactory

因为在 springoot 2.x版本中,默认采用的是 Lettuce实现的,所以无法初始化出 Jedis的连接对象 JedisConnectionFactory,所以我们需要手动配置并注入

public class RedisConfig {
    @Bean
    JedisConnectionFactory jedisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        return factory;
    }
}

但是启动项目后发现报出了如下的异常:

jedis_error

redis连接失败,springboot2.x通过以上方式集成Redis并不会读取配置文件中的 spring.redis.host等这样的配置,需要手动配置,如下:

@Configuration
public class RedisConfig2 {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;

@Bean
public RedisTemplate<String, Serializable> redisTemplate(JedisConnectionFactory connectionFactory) {
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(jedisConnectionFactory());
return redisTemplate;
}
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setPassword(RedisPassword.of(password));
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(config);
return connectionFactory;
}
}

通过以上方式就可以连接上 redis了,不过这里要提醒的一点就是,在springboot 2.x版本中 JedisConnectionFactory设置连接的方法已过时,如图所示:

jedis_timeout

在 springboot 2.x版本中推荐使用 RedisStandaloneConfiguration类来设置连接的端口,地址等属性

那么问题来了,

Springboot2.x是如何默认使用了lettuce,这得去研究下里面的部分代码。我们可以可进入到Springboot2.x自动装配模块的redis部分,其中有一个RedisAutoConfiguration类,其主要作用是对Springboot自动配置连接redis类:

  @Configuration( proxyBeanMethods = false )
  @ConditionalOnClass({RedisOperations.class})
  @EnableConfigurationProperties({RedisProperties.class})
  @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
  public class RedisAutoConfiguration {
      public RedisAutoConfiguration() {
     }
     ......省略
  }

这里只需要关注里面的一行注解:

@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})

这就意味着使用spring-boot-starter-data-redis依赖时,可自动导入lettuce和jedis两种驱动,按理来说,不会同时存在两种驱动,这样没有太大意义,因此,这里的先后顺序就很重要了,为什么这么说呢?

分别进入到LettuceConnectionConfiguration.class与JedisConnectionConfiguration.class当中,各自展示本文需要涉及到的核心代码:

//LettuceConnectionConfiguration
@ConditionalOnClass({RedisClient.class})
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
   ......省略
    @Bean
    @ConditionalOnMissingBean({RedisConnectionFactory.class})
    LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources) throws UnknownHostException {
        LettuceClientConfiguration clientConfig = this.getLettuceClientConfiguration(builderCustomizers, clientResources, this.getProperties().getLettuce().getPool());
        return this.createLettuceConnectionFactory(clientConfig);
   }
}


//JedisConnectionConfiguration
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
 class JedisConnectionConfiguration extends RedisConnectionConfiguration {
   ......省略
    @Bean
    @ConditionalOnMissingBean({RedisConnectionFactory.class})
    JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) throws UnknownHostException {
        return this.createJedisConnectionFactory(builderCustomizers);
   }
}

可见,LettuceConnectionConfiguration.class与JedisConnectionConfiguration.class当中都有一个相同的注解 @ConditionalOnMissingBean({RedisConnectionFactory.class}),这是说,假如RedisConnectionFactory这个bean已经被注册到容器里,那么与它相似的其他Bean就不会再被加载注册,

简单点说,对LettuceConnectionConfiguration与JedisConnectionConfiguration各自加上 @ConditionalOnMissingBean({RedisConnectionFactory.class})注解,两者当中只能加载注册其中一个到容器里,另外一个就不会再进行加载注册。

那么,问题就来了,谁会先被注册呢?

这就回到了上面提到的一句,

@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})

这一句里的先后顺序很关键,LettuceConnectionConfiguration在前面,就意味着,LettuceConnectionConfiguration将会被注册。

可见,Springboot默认是使用lettuce来连接redis的。

当我们引入spring-boot-starter-data-redis依赖包时,其实就相当于引入lettuce包,这时就会使用lettuce驱动,若不想使用该默认的lettuce驱动,直接将lettuce依赖排除即可。

  1 <dependency>
  2     <groupId>org.springframework.boot</groupId>
  3     <artifactId>spring-boot-starter-data-redis</artifactId>
  4     <exclusions>
  5         <exclusion>
  6             <groupId>io.lettuce</groupId>
  7             <artifactId>lettuce-core</artifactId>
  8         </exclusion>
  9     </exclusions>
 10 </dependency>

然后再引入jedis依赖——

  1 <dependency>
  2     <groupId>redis.clients</groupId>
  3     <artifactId>jedis</artifactId>
  4 </dependency>

这样,在进行RedisAutoConfiguration的导入注解时,因为没有找到lettuce依赖,故而这注解@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})的第二个位置上的JedisConnectionConfiguration就有效了,就可以被注册到容器了,当做springboot操作redis的驱动。

lettuce与jedis两者有什么区别呢?

  1. lettuce:底层是用netty实现,线程安全,默认只有一个实例。
  2. jedis:可直连redis服务端,配合连接池使用,可增加物理连接。

根据异常提示找到出现错误的方法,在下列代码里的LettuceConverters.toBoolean(this.getConnection().zadd(key, score, value))——

  1 public Boolean zAdd(byte[] key, double score, byte[] value) {
  2     Assert.notNull(key, "Key must not be null!");
  3     Assert.notNull(value, "Value must not be null!");
  4 ​
  5     try {
  6         if (this.isPipelined()) {
  7             this.pipeline(this.connection.newLettuceResult(this.getAsyncConnection().zadd(key, score, value), LettuceConverters.longToBoolean()));
  8             return null;
  9        } else if (this.isQueueing()) {
 10             this.transaction(this.connection.newLettuceResult(this.getAsyncConnection().zadd(key, score, value), LettuceConverters.longToBoolean()));
 11             return null;
 12        } else {
 13             return LettuceConverters.toBoolean(this.getConnection().zadd(key, score, value));
 14        }
 15    } catch (Exception var6) {
 16         throw this.convertLettuceAccessException(var6);
 17    }
 18 }

LettuceConverters.toBoolean()是将long转为Boolean,正常情况下,this.getConnection().zadd(key, score, value)如果新增成功话,那么返回1,这样LettuceConverters.toBoolean(1)得到的是true,反之,如果新增失败,则返回0,即LettuceConverters.toBoolean(0),还有第三种情况,就是这个this.getConnection().zadd(key, score, value)方法出现异常,什么情况下会出现异常呢?

应该是,connection连接失败的时候。

这就意味着,以lettuce驱动连接redis的过程当中,会出现连接断开的情况,导致无法新增成功,超过一定时间还没有正常,就会出现连接超时的情况。

最后发现,阿里云官网上关于redis的客户端连接,是不推荐使用Lettuce客户端——

但是Lettuce后续版本可能会解决这个问题,让我们拭目以待,关注版本更新吧。

其他例子

2.1 配置文件

lettuce的默认配置已经基本满足需求了,如果有需要可以自行配置

##端口号
server.port=8888

# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
##连接池最大连接数(使用负值表示没有限制) 默认8
#spring.redis.lettuce.pool.max-active=8
## 连接池中的最大空闲连接 默认8
#spring.redis.lettuce.pool.max-idle=8
## 连接池中的最小空闲连接 默认0
#spring.redis.lettuce.pool.min-idle=0
## 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
#spring.redis.lettuce.pool.max-wait=-1
# 连接超时时间(毫秒)
spring.redis.timeout=200

2.2 RredisTemplate 配置

2.2.1 RredisTemplate自动配置

在引入redis的依赖后,RredisTemplate会自动配置,可以直接注入RedisTemplate使用。

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(
            RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

}

Spring Boot 自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。但是,这个RedisTemplate的泛型是<Object,Object>。这样在写代码就很不方便,要写好多类型转换的代码。

因为有@ConditionalOnMissingBean(name = "redisTemplate")注解,所以如果Spring容器中有一个name 为redisTemplate 的 RedisTemplate 对象那么这个自动配置的RedisTemplate就不会实例化。

2.2.2 配置一个RredisTemplate

我们需要一个泛型为<String,Object>形式的RedisTemplate,并且设置这个RedisTemplate在数据存在Redis时key及value的序列化方式(默认使用的JdkSerializationRedisSerializer 这样的会导致我们通过redis desktop manager显示的我们key跟value的时候显示不是正常字符)。

@Configuration
public class RedisConfig {
  @Bean
  public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
    RedisTemplate<String,Object> template = new RedisTemplate <>();
    template.setConnectionFactory(factory);

    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    // key采用String的序列化方式
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash的value序列化方式采用jackson
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    template.afterPropertiesSet();

    return template;
  }
}

注意
方法名一定要叫redisTemplate 因为@Bean注解是根据方法名配置这个bean的name的,覆盖默认配置

2.3 Redis 操作的工具类

下面这个工具类包含Redis的一些基本操作,大家可以参考

/**
 * redis 工具类
 *
 * @author simon
 * @date 2018-11-28 10:35
 **/
 @Component
 public class RedisUtils {
   /**
    * 注入redisTemplate bean
    */
   @Autowired
   private RedisTemplate <String,Object> redisTemplate;

   /**
    * 指定缓存失效时间
    *
    * @param key  键
    * @param time 时间(秒)
    * @return
    */
   public boolean expire(String key, long time) {
     try {
       if (time > 0) {
         redisTemplate.expire(key, time, TimeUnit.SECONDS);
       }
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 根据key获取过期时间
    *
    * @param key 键 不能为null
    * @return 时间(秒) 返回0代表为永久有效
    */
   public long getExpire(String key) {
     return redisTemplate.getExpire(key, TimeUnit.SECONDS);
   }

   /**
    * 判断key是否存在
    *
    * @param key 键
    * @return true 存在 false不存在
    */
   public boolean hasKey(String key) {
     try {
       return redisTemplate.hasKey(key);
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 删除缓存
    *
    * @param key 可以传一个值 或多个
    */
   @SuppressWarnings("unchecked")
   public void del(String... key) {
     if (key != null && key.length > 0) {
       if (key.length == 1) {
         redisTemplate.delete(key[0]);
       } else {
         redisTemplate.delete(CollectionUtils.arrayToList(key));
       }
     }
   }
   // ============================String(字符串)=============================

   /**
    * 普通缓存获取
    *
    * @param key 键
    * @return 值
    */
   public Object get(String key) {
     return key == null ? null : redisTemplate.opsForValue().get(key);
   }

   /**
    * 普通缓存放入
    *
    * @param key   键
    * @param value 值
    * @return true成功 false失败
    */
   public boolean set(String key, Object value) {
     try {
       redisTemplate.opsForValue().set(key, value);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 普通缓存放入并设置时间
    *
    * @param key   键
    * @param value 值
    * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
    * @return true成功 false 失败
    */
   public boolean set(String key, Object value, long time) {
     try {
       if (time > 0) {
         redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
       } else {
         set(key, value);
       }
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 递增
    *
    * @param key   键
    * @param delta 要增加几(大于0)
    * @return
    */
   public long incr(String key, long delta) {
     if (delta < 0) {
       throw new RuntimeException("递增因子必须大于0");
     }
     return redisTemplate.opsForValue().increment(key, delta);
   }

   /**
    * 递减
    *
    * @param key   键
    * @param delta 要减少几(小于0)
    * @return
    */
   public long decr(String key, long delta) {
     if (delta < 0) {
       throw new RuntimeException("递减因子必须大于0");
     }
     return redisTemplate.opsForValue().increment(key, -delta);
   }
   // ================================Hash(哈希)=================================

   /**
    * HashGet
    *
    * @param key  键 不能为null
    * @param item 项 不能为null
    * @return 值
    */
   public Object hget(String key, String item) {
     return redisTemplate.opsForHash().get(key, item);
   }

   /**
    * 获取hashKey对应的所有键值
    *
    * @param key 键
    * @return 对应的多个键值
    */
   public Map <Object, Object> hmget(String key) {
     return redisTemplate.opsForHash().entries(key);
   }

   /**
    * HashSet
    *
    * @param key 键
    * @param map 对应多个键值
    * @return true 成功 false 失败
    */
   public boolean hmset(String key, Map <String, Object> map) {
     try {
       redisTemplate.opsForHash().putAll(key, map);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * HashSet 并设置时间
    *
    * @param key  键
    * @param map  对应多个键值
    * @param time 时间(秒)
    * @return true成功 false失败
    */
   public boolean hmset(String key, Map <String, Object> map, long time) {
     try {
       redisTemplate.opsForHash().putAll(key, map);
       if (time > 0) {
         expire(key, time);
       }
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 向一张hash表中放入数据,如果不存在将创建
    *
    * @param key   键
    * @param item  项
    * @param value 值
    * @return true 成功 false失败
    */
   public boolean hset(String key, String item, Object value) {
     try {
       redisTemplate.opsForHash().put(key, item, value);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 向一张hash表中放入数据,如果不存在将创建
    *
    * @param key   键
    * @param item  项
    * @param value 值
    * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
    * @return true 成功 false失败
    */
   public boolean hset(String key, String item, Object value, long time) {
     try {
       redisTemplate.opsForHash().put(key, item, value);
       if (time > 0) {
         expire(key, time);
       }
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 删除hash表中的值
    *
    * @param key  键 不能为null
    * @param item 项 可以使多个 不能为null
    */
   public void hdel(String key, Object... item) {
     redisTemplate.opsForHash().delete(key, item);
   }

   /**
    * 判断hash表中是否有该项的值
    *
    * @param key  键 不能为null
    * @param item 项 不能为null
    * @return true 存在 false不存在
    */
   public boolean hHasKey(String key, String item) {
     return redisTemplate.opsForHash().hasKey(key, item);
   }

   /**
    * hash递增 如果不存在,就会创建一个 并把新增后的值返回
    *
    * @param key  键
    * @param item 项
    * @param by   要增加几(大于0)
    * @return
    */
   public double hincr(String key, String item, double by) {
     return redisTemplate.opsForHash().increment(key, item, by);
   }

   /**
    * hash递减
    *
    * @param key  键
    * @param item 项
    * @param by   要减少记(小于0)
    * @return
    */
   public double hdecr(String key, String item, double by) {
     return redisTemplate.opsForHash().increment(key, item, -by);
   }
   // ============================Set(集合)=============================

   /**
    * 根据key获取Set中的所有值
    *
    * @param key 键
    * @return
    */
   public Set <Object> sGet(String key) {
     try {
       return redisTemplate.opsForSet().members(key);
     } catch (Exception e) {
       e.printStackTrace();
       return null;
     }
   }

   /**
    * 根据value从一个set中查询,是否存在
    *
    * @param key   键
    * @param value 值
    * @return true 存在 false不存在
    */
   public boolean sHasKey(String key, Object value) {
     try {
       return redisTemplate.opsForSet().isMember(key, value);
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 将数据放入set缓存
    *
    * @param key    键
    * @param values 值 可以是多个
    * @return 成功个数
    */
   public long sSet(String key, Object... values) {
     try {
       return redisTemplate.opsForSet().add(key, values);
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }

   /**
    * 将set数据放入缓存
    *
    * @param key    键
    * @param time   时间(秒)
    * @param values 值 可以是多个
    * @return 成功个数
    */
   public long sSetAndTime(String key, long time, Object... values) {
     try {
       Long count = redisTemplate.opsForSet().add(key, values);
       if (time > 0)
         expire(key, time);
       return count;
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }

   /**
    * 获取set缓存的长度
    *
    * @param key 键
    * @return
    */
   public long sGetSetSize(String key) {
     try {
       return redisTemplate.opsForSet().size(key);
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }

   /**
    * 移除值为value的
    *
    * @param key    键
    * @param values 值 可以是多个
    * @return 移除的个数
    */
   public long setRemove(String key, Object... values) {
     try {
       Long count = redisTemplate.opsForSet().remove(key, values);
       return count;
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }
   // ===============================List(列表)=================================

   /**
    * 获取list缓存的内容
    *
    * @param key   键
    * @param start 开始
    * @param end   结束 0 到 -1代表所有值
    * @return
    */
   public List <Object> lGet(String key, long start, long end) {
     try {
       return redisTemplate.opsForList().range(key, start, end);
     } catch (Exception e) {
       e.printStackTrace();
       return null;
     }
   }

   /**
    * 获取list缓存的长度
    *
    * @param key 键
    * @return
    */
   public long lGetListSize(String key) {
     try {
       return redisTemplate.opsForList().size(key);
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }

   /**
    * 通过索引 获取list中的值
    *
    * @param key   键
    * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
    * @return
    */
   public Object lGetIndex(String key, long index) {
     try {
       return redisTemplate.opsForList().index(key, index);
     } catch (Exception e) {
       e.printStackTrace();
       return null;
     }
   }

   /**
    * 将list放入缓存
    *
    * @param key   键
    * @param value 值
    * @return
    */
   public boolean lSet(String key, Object value) {
     try {
       redisTemplate.opsForList().rightPush(key, value);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 将list放入缓存
    *
    * @param key   键
    * @param value 值
    * @param time  时间(秒)
    * @return
    */
   public boolean lSet(String key, Object value, long time) {
     try {
       redisTemplate.opsForList().rightPush(key, value);
       if (time > 0)
         expire(key, time);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 将list放入缓存
    *
    * @param key   键
    * @param value 值
    * @return
    */
   public boolean lSet(String key, List <Object> value) {
     try {
       redisTemplate.opsForList().rightPushAll(key, value);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 将list放入缓存
    *
    * @param key   键
    * @param value 值
    * @param time  时间(秒)
    * @return
    */
   public boolean lSet(String key, List <Object> value, long time) {
     try {
       redisTemplate.opsForList().rightPushAll(key, value);
       if (time > 0)
         expire(key, time);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 根据索引修改list中的某条数据
    *
    * @param key   键
    * @param index 索引
    * @param value 值
    * @return
    */
   public boolean lUpdateIndex(String key, long index, Object value) {
     try {
       redisTemplate.opsForList().set(key, index, value);
       return true;
     } catch (Exception e) {
       e.printStackTrace();
       return false;
     }
   }

   /**
    * 移除N个值为value
    *
    * @param key   键
    * @param count 移除多少个
    * @param value 值
    * @return 移除的个数
    */
   public long lRemove(String key, long count, Object value) {
     try {
       Long remove = redisTemplate.opsForList().remove(key, count, value);
       return remove;
     } catch (Exception e) {
       e.printStackTrace();
       return 0;
     }
   }
 }

具体测试部分就省略了。

源代码下载


SpringBoot2.X整合Redis(单机+集群+多数据源)-Lettuce版

Redis 三大客户端

简介

Jedis:是Redis 老牌的Java实现客户端,提供了比较全面的Redis命令的支持,

Redisson:实现了分布式和可扩展的Java数据结构。

Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。

优点:

Jedis:比较全面的提供了Redis的操作特性

Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如分布式锁,分布式集合,可通过Redis支持延迟队列

Lettuce:主要在一些分布式缓存框架上使用比较多

可伸缩:

Jedis:使用阻塞的I/O,且其方法调用都是同步的,程序流需要等到sockets处理完I/O才能执行,不支持异步。Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis

Redisson:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作

Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操作单个Lettuce连接来完成各种操作

pipeline 的支持

jedis 通过一定的改造后可以支持pipeline, 具体可以看 Redis 批量操作之 pipeline

但是 Lettuce 的pipeline行为很奇怪. 在 Spring RedisTemplate 中的 executePipelined 方法中的情况:

有时完全是一条一条命令地发送

有时全合并几条命令发送

但跟完全 pipeline 的方式不同, 测试多次, 但没发现有一次是完整 pipeline 的
复制代码

所以如果需要使用pipeline的话, 建议还是使用Jedis

Lettuce 接入

单机版

配置文件

    host: 192.168.131.118
    port: 4884
    password: dsgs548
    database: 0
    # lettuce简单配置
    lettuce:
      pool:
        # 最大活跃链接数 默认8
        max-active: 5
        # 最大空闲连接数 默认8
        max-idle: 10
        # 最小空闲连接数 默认0
        min-idle: 0
复制代码

redis配置类

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }
}
复制代码

直接引入RedisTemplate 即可, 单机版比较简单

集群版+多数据源

配置文件

spring:
  redis:
    cluster:
      nodes: 192.168.131.118:4883,192.168.131.118:4884,192.168.131.118:4885
#      nodes:
#        - 192.168.131.118:4883
#        - 1192.168.131.118:4884
#        - 192.168.131.118:4885
    password: adfafsas
    lettuce:
      pool:
        # 最大活跃链接数 默认8
        max-active: 5
        # 最大空闲连接数 默认8
        max-idle: 10
        # 最小空闲连接数 默认0
        min-idle: 0
  secondaryRedis:
    cluster:
      nodes: 192.168.131.118:4883,192.168.131.118:4884,192.168.131.118:4885
#      nodes:
#        - 192.168.131.118:4883
#        - 192.168.131.118:4884
#        - 192.168.131.118:4885
    password: advfafasfsa
复制代码

redis配置类

@Configuration
public class RedisConfig {

    @Autowired
    private Environment environment;
    /**
     * 配置lettuce连接池
     *
     * @return
     */
    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.redis.cluster.lettuce.pool")
    public GenericObjectPoolConfig redisPool() {
        return new GenericObjectPoolConfig();
    }

    /**
     * 配置第一个数据源的
     *
     * @return
     */
    @Bean("redisClusterConfig")
    @Primary
    public RedisClusterConfiguration redisClusterConfig() {

        Map<String, Object> source = new HashMap<>(8);
        source.put("spring.redis.cluster.nodes", environment.getProperty("spring.redis.cluster.nodes"));
        RedisClusterConfiguration redisClusterConfiguration;
        redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
        redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
        return redisClusterConfiguration;

    }


    /**
     * 配置第一个数据源的连接工厂
     * 这里注意:需要添加@Primary 指定bean的名称,目的是为了创建两个不同名称的LettuceConnectionFactory
     *
     * @param redisPool
     * @param redisClusterConfig
     * @return
     */
    @Bean("lettuceConnectionFactory")
    @Primary
    public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig redisPool, @Qualifier("redisClusterConfig") RedisClusterConfiguration redisClusterConfig) {
        LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(redisPool).build();
        return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
    }


    /**
     * 配置第一个数据源的RedisTemplate
     * 注意:这里指定使用名称=factory 的 RedisConnectionFactory
     * 并且标识第一个数据源是默认数据源 @Primary
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean("redisTemplate")
    @Primary
    public RedisTemplate redisTemplate(@Qualifier("lettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        return getRedisTemplate(redisConnectionFactory);

    }




    /**
     * 配置第二个数据源
     *
     * @return
     */
    @Bean("secondaryRedisClusterConfig")
    public RedisClusterConfiguration secondaryRedisConfig() {

        Map<String, Object> source = new HashMap<>(8);
        source.put("spring.redis.cluster.nodes", environment.getProperty("spring.secondaryRedis.cluster.nodes"));
        RedisClusterConfiguration redisClusterConfiguration;
        redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
        redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));

        return redisClusterConfiguration;
    }

    @Bean("secondaryLettuceConnectionFactory")
    public LettuceConnectionFactory secondaryLettuceConnectionFactory(GenericObjectPoolConfig redisPool, @Qualifier("secondaryRedisClusterConfig")RedisClusterConfiguration secondaryRedisClusterConfig) {
        LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(redisPool).build();
        return new LettuceConnectionFactory(secondaryRedisClusterConfig, clientConfiguration);
    }

    /**
     * 配置第一个数据源的RedisTemplate
     * 注意:这里指定使用名称=factory2 的 RedisConnectionFactory
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean("secondaryRedisTemplate")
    public RedisTemplate secondaryRedisTemplate(@Qualifier("secondaryLettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        return getRedisTemplate(redisConnectionFactory);
    }



    private RedisTemplate getRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;
    }

}


参考:

https://www.jianshu.com/p/071bae3834b0icon-default.png?t=M85Bhttps://www.jianshu.com/p/071bae3834b0
https://www.tapme.top/blog/detail/2019-02-22-14-59/icon-default.png?t=M85Bhttps://www.tapme.top/blog/detail/2019-02-22-14-59/

https://www.nonelonely.com/article/1556289630491icon-default.png?t=M85Bhttps://www.nonelonely.com/article/1556289630491

https://juejin.im/post/5d65eb88e51d4561db5e3a79

Springboot2.x集成lettuce连接redis集群报超时异常Command timed out after 6 second(s) - 朱季谦 - 博客园文/朱季谦 背景:最近在对一新开发Springboot系统做压测,发现刚开始压测时,可以正常对redis集群进行数据存取,但是暂停几分钟后,接着继续用jmeter进行压测时,发现redis就开始突然疯https://www.cnblogs.com/zhujiqian/p/14552873.html

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐