springboot2.0 集成redis服务详解,以及 (Lettuce & Jedis)

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 jedisLettuce 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配置文件

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

自定义 RedisTemplate

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

  1. @Configuration
  2. public class RedisConfig {
  3.     @Bean
  4.     public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory connectionFactory) {
  5.         RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
  6.         redisTemplate.setKeySerializer(new StringRedisSerializer());
  7.         redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
  8.         redisTemplate.setConnectionFactory(connectionFactory);
  9.         return redisTemplate;
  10.     }
  11. }
  12. 复制代码

定义测试实体类

  1. public class User implements Serializable {
  2.     private static final long serialVersionUID = 4220515347228129741L;
  3.     private Integer id;
  4.     private String username;
  5.     private Integer age;
  6.     public User(Integer id, String username, Integer age) {
  7.         this.id = id;
  8.         this.username = username;
  9.         this.age = age;
  10.     }
  11.     public User() {
  12.     }
  13.     //getter/setter 省略
  14. }
  15. 复制代码

测试

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class RedisTest {
  4.     private Logger logger = LoggerFactory.getLogger(RedisTest.class);
  5.     @Autowired
  6.     private RedisTemplate<String, Serializable> redisTemplate;
  7.     @Test
  8.     public void test() {
  9.         String key = "user:1";
  10.         redisTemplate.opsForValue().set(key, new User(1,"pjmike",20));
  11.         User user = (User) redisTemplate.opsForValue().get(key);
  12.         logger.info("uesr: "+user.toString());
  13.     }
  14. }
  15. 复制代码

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的连接池

  1. spring.redis.host=localhost
  2. spring.redis.port=6379
  3. spring.redis.password=root
  4. spring.redis.jedis.pool.max-idle=8
  5. spring.redis.jedis.pool.max-wait=-1ms
  6. spring.redis.jedis.pool.min-idle=0
  7. spring.redis.jedis.pool.max-active=8

配置 JedisConnectionFactory

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

  1. public class RedisConfig {
  2.     @Bean
  3.     JedisConnectionFactory jedisConnectionFactory() {
  4.         JedisConnectionFactory factory = new JedisConnectionFactory();
  5.         return factory;
  6.     }
  7. }

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

jedis_error

 

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

@Configuration
public class RedisConfig2 {undefined
    
@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) {undefined
        RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(jedisConnectionFactory());
        return redisTemplate;
    }
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {undefined
        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依赖时,可自动导入lettucejedis两种驱动,按理来说,不会同时存在两种驱动,这样没有太大意义,因此,这里的先后顺序就很重要了,为什么这么说呢?

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

  1. //LettuceConnectionConfiguration
  2. @ConditionalOnClass({RedisClient.class})
  3. class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
  4.    ......省略
  5.     @Bean
  6.     @ConditionalOnMissingBean({RedisConnectionFactory.class})
  7.     LettuceConnectionFactory redisConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources) throws UnknownHostException {
  8.         LettuceClientConfiguration clientConfig = this.getLettuceClientConfiguration(builderCustomizers, clientResources, this.getProperties().getLettuce().getPool());
  9.         return this.createLettuceConnectionFactory(clientConfig);
  10.    }
  11. }
  12. //JedisConnectionConfiguration
  13. @ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
  14.  class JedisConnectionConfiguration extends RedisConnectionConfiguration {
  15.    ......省略
  16.     @Bean
  17.     @ConditionalOnMissingBean({RedisConnectionFactory.class})
  18.     JedisConnectionFactory redisConnectionFactory(ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) throws UnknownHostException {
  19.         return this.createJedisConnectionFactory(builderCustomizers);
  20.    }
  21. }

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

      简单点说,对LettuceConnectionConfigurationJedisConnectionConfiguration各自加上 @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的驱动。

lettucejedis两者有什么区别呢?

  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的默认配置已经基本满足需求了,如果有需要可以自行配置

  1. ##端口号
  2. server.port=8888
  3. # Redis数据库索引(默认为0
  4. spring.redis.database=0
  5. # Redis服务器地址
  6. spring.redis.host=localhost
  7. # Redis服务器连接端口
  8. spring.redis.port=6379
  9. # Redis服务器连接密码(默认为空)
  10. spring.redis.password=
  11. ##连接池最大连接数(使用负值表示没有限制) 默认8
  12. #spring.redis.lettuce.pool.max-active=8
  13. ## 连接池中的最大空闲连接 默认8
  14. #spring.redis.lettuce.pool.max-idle=8
  15. ## 连接池中的最小空闲连接 默认0
  16. #spring.redis.lettuce.pool.min-idle=0
  17. ## 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
  18. #spring.redis.lettuce.pool.max-wait=-1
  19. # 连接超时时间(毫秒)
  20. spring.redis.timeout=200

2.2 RredisTemplate 配置

2.2.1 RredisTemplate自动配置

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

  1. @Configuration
  2. @ConditionalOnClass(RedisOperations.class)
  3. @EnableConfigurationProperties(RedisProperties.class)
  4. @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
  5. public class RedisAutoConfiguration {
  6.     @Bean
  7.     @ConditionalOnMissingBean(name = "redisTemplate")
  8.     public RedisTemplate<Object, Object> redisTemplate(
  9.             RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
  10.         RedisTemplate<Object, Object> template = new RedisTemplate<>();
  11.         template.setConnectionFactory(redisConnectionFactory);
  12.         return template;
  13.     }
  14.     @Bean
  15.     @ConditionalOnMissingBean
  16.     public StringRedisTemplate stringRedisTemplate(
  17.             RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
  18.         StringRedisTemplate template = new StringRedisTemplate();
  19.         template.setConnectionFactory(redisConnectionFactory);
  20.         return template;
  21.     }
  22. }

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

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

2.2.2 配置一个RredisTemplate

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

  1. @Configuration
  2. public class RedisConfig {
  3.   @Bean
  4.   public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
  5.     RedisTemplate<String,Object> template = new RedisTemplate <>();
  6.     template.setConnectionFactory(factory);
  7.     Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  8.     ObjectMapper om = new ObjectMapper();
  9.     om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  10.     om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  11.     jackson2JsonRedisSerializer.setObjectMapper(om);
  12.     StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
  13.     // key采用String的序列化方式
  14.     template.setKeySerializer(stringRedisSerializer);
  15.     // hashkey也采用String的序列化方式
  16.     template.setHashKeySerializer(stringRedisSerializer);
  17.     // value序列化方式采用jackson
  18.     template.setValueSerializer(jackson2JsonRedisSerializer);
  19.     // hashvalue序列化方式采用jackson
  20.     template.setHashValueSerializer(jackson2JsonRedisSerializer);
  21.     template.afterPropertiesSet();
  22.     return template;
  23.   }
  24. }

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

2.3 Redis 操作的工具类

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

  1. /**
  2.  * redis 工具类
  3.  *
  4.  * @author simon
  5.  * @date 2018-11-28 10:35
  6.  **/
  7.  @Component
  8.  public class RedisUtils {
  9.    /**
  10.     * 注入redisTemplate bean
  11.     */
  12.    @Autowired
  13.    private RedisTemplate <String,Object> redisTemplate;
  14.    /**
  15.     * 指定缓存失效时间
  16.     *
  17.     * @param key 
  18.     * @param time 时间()
  19.     * @return
  20.     */
  21.    public boolean expire(String key, long time) {
  22.      try {
  23.        if (time > 0) {
  24.          redisTemplate.expire(key, time, TimeUnit.SECONDS);
  25.        }
  26.        return true;
  27.      } catch (Exception e) {
  28.        e.printStackTrace();
  29.        return false;
  30.      }
  31.    }
  32.    /**
  33.     * 根据key获取过期时间
  34.     *
  35.     * @param key 不能为null
  36.     * @return 时间() 返回0代表为永久有效
  37.     */
  38.    public long getExpire(String key) {
  39.      return redisTemplate.getExpire(key, TimeUnit.SECONDS);
  40.    }
  41.    /**
  42.     * 判断key是否存在
  43.     *
  44.     * @param key
  45.     * @return true 存在 false不存在
  46.     */
  47.    public boolean hasKey(String key) {
  48.      try {
  49.        return redisTemplate.hasKey(key);
  50.      } catch (Exception e) {
  51.        e.printStackTrace();
  52.        return false;
  53.      }
  54.    }
  55.    /**
  56.     * 删除缓存
  57.     *
  58.     * @param key 可以传一个值 或多个
  59.     */
  60.    @SuppressWarnings("unchecked")
  61.    public void del(String... key) {
  62.      if (key != null && key.length > 0) {
  63.        if (key.length == 1) {
  64.          redisTemplate.delete(key[0]);
  65.        } else {
  66.          redisTemplate.delete(CollectionUtils.arrayToList(key));
  67.        }
  68.      }
  69.    }
  70.    // ============================String(字符串)=============================
  71.    /**
  72.     * 普通缓存获取
  73.     *
  74.     * @param key
  75.     * @return
  76.     */
  77.    public Object get(String key) {
  78.      return key == null ? null : redisTemplate.opsForValue().get(key);
  79.    }
  80.    /**
  81.     * 普通缓存放入
  82.     *
  83.     * @param key  
  84.     * @param value
  85.     * @return true成功 false失败
  86.     */
  87.    public boolean set(String key, Object value) {
  88.      try {
  89.        redisTemplate.opsForValue().set(key, value);
  90.        return true;
  91.      } catch (Exception e) {
  92.        e.printStackTrace();
  93.        return false;
  94.      }
  95.    }
  96.    /**
  97.     * 普通缓存放入并设置时间
  98.     *
  99.     * @param key  
  100.     * @param value
  101.     * @param time  时间() time要大于0 如果time小于等于0 将设置无限期
  102.     * @return true成功 false 失败
  103.     */
  104.    public boolean set(String key, Object value, long time) {
  105.      try {
  106.        if (time > 0) {
  107.          redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
  108.        } else {
  109.          set(key, value);
  110.        }
  111.        return true;
  112.      } catch (Exception e) {
  113.        e.printStackTrace();
  114.        return false;
  115.      }
  116.    }
  117.    /**
  118.     * 递增
  119.     *
  120.     * @param key  
  121.     * @param delta 要增加几(大于0)
  122.     * @return
  123.     */
  124.    public long incr(String key, long delta) {
  125.      if (delta < 0) {
  126.        throw new RuntimeException("递增因子必须大于0");
  127.      }
  128.      return redisTemplate.opsForValue().increment(key, delta);
  129.    }
  130.    /**
  131.     * 递减
  132.     *
  133.     * @param key  
  134.     * @param delta 要减少几(小于0)
  135.     * @return
  136.     */
  137.    public long decr(String key, long delta) {
  138.      if (delta < 0) {
  139.        throw new RuntimeException("递减因子必须大于0");
  140.      }
  141.      return redisTemplate.opsForValue().increment(key, -delta);
  142.    }
  143.    // ================================Hash(哈希)=================================
  144.    /**
  145.     * HashGet
  146.     *
  147.     * @param key  不能为null
  148.     * @param item 不能为null
  149.     * @return
  150.     */
  151.    public Object hget(String key, String item) {
  152.      return redisTemplate.opsForHash().get(key, item);
  153.    }
  154.    /**
  155.     * 获取hashKey对应的所有键值
  156.     *
  157.     * @param key
  158.     * @return 对应的多个键值
  159.     */
  160.    public Map <Object, Object> hmget(String key) {
  161.      return redisTemplate.opsForHash().entries(key);
  162.    }
  163.    /**
  164.     * HashSet
  165.     *
  166.     * @param key
  167.     * @param map 对应多个键值
  168.     * @return true 成功 false 失败
  169.     */
  170.    public boolean hmset(String key, Map <String, Object> map) {
  171.      try {
  172.        redisTemplate.opsForHash().putAll(key, map);
  173.        return true;
  174.      } catch (Exception e) {
  175.        e.printStackTrace();
  176.        return false;
  177.      }
  178.    }
  179.    /**
  180.     * HashSet 并设置时间
  181.     *
  182.     * @param key 
  183.     * @param map  对应多个键值
  184.     * @param time 时间()
  185.     * @return true成功 false失败
  186.     */
  187.    public boolean hmset(String key, Map <String, Object> map, long time) {
  188.      try {
  189.        redisTemplate.opsForHash().putAll(key, map);
  190.        if (time > 0) {
  191.          expire(key, time);
  192.        }
  193.        return true;
  194.      } catch (Exception e) {
  195.        e.printStackTrace();
  196.        return false;
  197.      }
  198.    }
  199.    /**
  200.     * 向一张hash表中放入数据,如果不存在将创建
  201.     *
  202.     * @param key  
  203.     * @param item 
  204.     * @param value
  205.     * @return true 成功 false失败
  206.     */
  207.    public boolean hset(String key, String item, Object value) {
  208.      try {
  209.        redisTemplate.opsForHash().put(key, item, value);
  210.        return true;
  211.      } catch (Exception e) {
  212.        e.printStackTrace();
  213.        return false;
  214.      }
  215.    }
  216.    /**
  217.     * 向一张hash表中放入数据,如果不存在将创建
  218.     *
  219.     * @param key  
  220.     * @param item 
  221.     * @param value
  222.     * @param time  时间() 注意:如果已存在的hash表有时间,这里将会替换原有的时间
  223.     * @return true 成功 false失败
  224.     */
  225.    public boolean hset(String key, String item, Object value, long time) {
  226.      try {
  227.        redisTemplate.opsForHash().put(key, item, value);
  228.        if (time > 0) {
  229.          expire(key, time);
  230.        }
  231.        return true;
  232.      } catch (Exception e) {
  233.        e.printStackTrace();
  234.        return false;
  235.      }
  236.    }
  237.    /**
  238.     * 删除hash表中的值
  239.     *
  240.     * @param key  不能为null
  241.     * @param item 可以使多个 不能为null
  242.     */
  243.    public void hdel(String key, Object... item) {
  244.      redisTemplate.opsForHash().delete(key, item);
  245.    }
  246.    /**
  247.     * 判断hash表中是否有该项的值
  248.     *
  249.     * @param key  不能为null
  250.     * @param item 不能为null
  251.     * @return true 存在 false不存在
  252.     */
  253.    public boolean hHasKey(String key, String item) {
  254.      return redisTemplate.opsForHash().hasKey(key, item);
  255.    }
  256.    /**
  257.     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
  258.     *
  259.     * @param key 
  260.     * @param item
  261.     * @param by   要增加几(大于0)
  262.     * @return
  263.     */
  264.    public double hincr(String key, String item, double by) {
  265.      return redisTemplate.opsForHash().increment(key, item, by);
  266.    }
  267.    /**
  268.     * hash递减
  269.     *
  270.     * @param key 
  271.     * @param item
  272.     * @param by   要减少记(小于0)
  273.     * @return
  274.     */
  275.    public double hdecr(String key, String item, double by) {
  276.      return redisTemplate.opsForHash().increment(key, item, -by);
  277.    }
  278.    // ============================Set(集合)=============================
  279.    /**
  280.     * 根据key获取Set中的所有值
  281.     *
  282.     * @param key
  283.     * @return
  284.     */
  285.    public Set <Object> sGet(String key) {
  286.      try {
  287.        return redisTemplate.opsForSet().members(key);
  288.      } catch (Exception e) {
  289.        e.printStackTrace();
  290.        return null;
  291.      }
  292.    }
  293.    /**
  294.     * 根据value从一个set中查询,是否存在
  295.     *
  296.     * @param key  
  297.     * @param value
  298.     * @return true 存在 false不存在
  299.     */
  300.    public boolean sHasKey(String key, Object value) {
  301.      try {
  302.        return redisTemplate.opsForSet().isMember(key, value);
  303.      } catch (Exception e) {
  304.        e.printStackTrace();
  305.        return false;
  306.      }
  307.    }
  308.    /**
  309.     * 将数据放入set缓存
  310.     *
  311.     * @param key   
  312.     * @param values 可以是多个
  313.     * @return 成功个数
  314.     */
  315.    public long sSet(String key, Object... values) {
  316.      try {
  317.        return redisTemplate.opsForSet().add(key, values);
  318.      } catch (Exception e) {
  319.        e.printStackTrace();
  320.        return 0;
  321.      }
  322.    }
  323.    /**
  324.     * set数据放入缓存
  325.     *
  326.     * @param key   
  327.     * @param time   时间()
  328.     * @param values 可以是多个
  329.     * @return 成功个数
  330.     */
  331.    public long sSetAndTime(String key, long time, Object... values) {
  332.      try {
  333.        Long count = redisTemplate.opsForSet().add(key, values);
  334.        if (time > 0)
  335.          expire(key, time);
  336.        return count;
  337.      } catch (Exception e) {
  338.        e.printStackTrace();
  339.        return 0;
  340.      }
  341.    }
  342.    /**
  343.     * 获取set缓存的长度
  344.     *
  345.     * @param key
  346.     * @return
  347.     */
  348.    public long sGetSetSize(String key) {
  349.      try {
  350.        return redisTemplate.opsForSet().size(key);
  351.      } catch (Exception e) {
  352.        e.printStackTrace();
  353.        return 0;
  354.      }
  355.    }
  356.    /**
  357.     * 移除值为value
  358.     *
  359.     * @param key   
  360.     * @param values 可以是多个
  361.     * @return 移除的个数
  362.     */
  363.    public long setRemove(String key, Object... values) {
  364.      try {
  365.        Long count = redisTemplate.opsForSet().remove(key, values);
  366.        return count;
  367.      } catch (Exception e) {
  368.        e.printStackTrace();
  369.        return 0;
  370.      }
  371.    }
  372.    // ===============================List(列表)=================================
  373.    /**
  374.     * 获取list缓存的内容
  375.     *
  376.     * @param key  
  377.     * @param start 开始
  378.     * @param end   结束 0 -1代表所有值
  379.     * @return
  380.     */
  381.    public List <Object> lGet(String key, long start, long end) {
  382.      try {
  383.        return redisTemplate.opsForList().range(key, start, end);
  384.      } catch (Exception e) {
  385.        e.printStackTrace();
  386.        return null;
  387.      }
  388.    }
  389.    /**
  390.     * 获取list缓存的长度
  391.     *
  392.     * @param key
  393.     * @return
  394.     */
  395.    public long lGetListSize(String key) {
  396.      try {
  397.        return redisTemplate.opsForList().size(key);
  398.      } catch (Exception e) {
  399.        e.printStackTrace();
  400.        return 0;
  401.      }
  402.    }
  403.    /**
  404.     * 通过索引 获取list中的值
  405.     *
  406.     * @param key  
  407.     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
  408.     * @return
  409.     */
  410.    public Object lGetIndex(String key, long index) {
  411.      try {
  412.        return redisTemplate.opsForList().index(key, index);
  413.      } catch (Exception e) {
  414.        e.printStackTrace();
  415.        return null;
  416.      }
  417.    }
  418.    /**
  419.     * list放入缓存
  420.     *
  421.     * @param key  
  422.     * @param value
  423.     * @return
  424.     */
  425.    public boolean lSet(String key, Object value) {
  426.      try {
  427.        redisTemplate.opsForList().rightPush(key, value);
  428.        return true;
  429.      } catch (Exception e) {
  430.        e.printStackTrace();
  431.        return false;
  432.      }
  433.    }
  434.    /**
  435.     * list放入缓存
  436.     *
  437.     * @param key  
  438.     * @param value
  439.     * @param time  时间()
  440.     * @return
  441.     */
  442.    public boolean lSet(String key, Object value, long time) {
  443.      try {
  444.        redisTemplate.opsForList().rightPush(key, value);
  445.        if (time > 0)
  446.          expire(key, time);
  447.        return true;
  448.      } catch (Exception e) {
  449.        e.printStackTrace();
  450.        return false;
  451.      }
  452.    }
  453.    /**
  454.     * list放入缓存
  455.     *
  456.     * @param key  
  457.     * @param value
  458.     * @return
  459.     */
  460.    public boolean lSet(String key, List <Object> value) {
  461.      try {
  462.        redisTemplate.opsForList().rightPushAll(key, value);
  463.        return true;
  464.      } catch (Exception e) {
  465.        e.printStackTrace();
  466.        return false;
  467.      }
  468.    }
  469.    /**
  470.     * list放入缓存
  471.     *
  472.     * @param key  
  473.     * @param value
  474.     * @param time  时间()
  475.     * @return
  476.     */
  477.    public boolean lSet(String key, List <Object> value, long time) {
  478.      try {
  479.        redisTemplate.opsForList().rightPushAll(key, value);
  480.        if (time > 0)
  481.          expire(key, time);
  482.        return true;
  483.      } catch (Exception e) {
  484.        e.printStackTrace();
  485.        return false;
  486.      }
  487.    }
  488.    /**
  489.     * 根据索引修改list中的某条数据
  490.     *
  491.     * @param key  
  492.     * @param index 索引
  493.     * @param value
  494.     * @return
  495.     */
  496.    public boolean lUpdateIndex(String key, long index, Object value) {
  497.      try {
  498.        redisTemplate.opsForList().set(key, index, value);
  499.        return true;
  500.      } catch (Exception e) {
  501.        e.printStackTrace();
  502.        return false;
  503.      }
  504.    }
  505.    /**
  506.     * 移除N个值为value
  507.     *
  508.     * @param key  
  509.     * @param count 移除多少个
  510.     * @param value
  511.     * @return 移除的个数
  512.     */
  513.    public long lRemove(String key, long count, Object value) {
  514.      try {
  515.        Long remove = redisTemplate.opsForList().remove(key, count, value);
  516.        return remove;
  517.      } catch (Exception e) {
  518.        e.printStackTrace();
  519.        return 0;
  520.      }
  521.    }
  522.  }

具体测试部分就省略了。

源代码下载


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框架的事件驱动的通信层,其方法调用是异步的。RedissonAPI是线程安全的,所以可以操作单个Redisson连接来完成各种操作

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

pipeline 的支持

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

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

  1. 有时完全是一条一条命令地发送
  2. 有时全合并几条命令发送
  3. 但跟完全 pipeline 的方式不同, 测试多次, 但没发现有一次是完整 pipeline
  4. 复制代码

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

Lettuce 接入

单机版

配置文件

  1.     host: 192.168.13118
  2.     port: 4884
  3.     password: dsgs548
  4.     database: 0
  5.     # lettuce简单配置
  6.     lettuce:
  7.       pool:
  8.         # 最大活跃链接数 默认8
  9.         max-active: 5
  10.         # 最大空闲连接数 默认8
  11.         max-idle: 10
  12.         # 最小空闲连接数 默认0
  13.         min-idle: 0
  14. 复制代码

redis配置类

  1. @Configuration
  2. public class RedisConfig {
  3.     @Bean
  4.     public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
  5.         RedisTemplate<String, Object> template = new RedisTemplate<>();
  6.         template.setConnectionFactory(factory);
  7.         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  8.         ObjectMapper om = new ObjectMapper();
  9.         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  10.         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  11.         jackson2JsonRedisSerializer.setObjectMapper(om);
  12.         StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
  13.         // key采用String的序列化方式
  14.         template.setKeySerializer(stringRedisSerializer);
  15.         // hashkey也采用String的序列化方式
  16.         template.setHashKeySerializer(stringRedisSerializer);
  17.         // value序列化方式采用jackson
  18.         template.setValueSerializer(jackson2JsonRedisSerializer);
  19.         // hashvalue序列化方式采用jackson
  20.         template.setHashValueSerializer(jackson2JsonRedisSerializer);
  21.         template.afterPropertiesSet();
  22.         return template;
  23.     }
  24. }
  25. 复制代码

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

集群版+多数据源

配置文件

  1. spring:
  2.   redis:
  3.     cluster:
  4.       nodes: 192.168.131.118:4883,192.168.131.118:4884,192.168.131.118:4885
  5. #      nodes:
  6. #        - 192.168.131.118:4883
  7. #        - 1192.168.131.118:4884
  8. #        - 192.16131.118:4885
  9.     password: adfafsas
  10.     lettuce:
  11.       pool:
  12.         # 最大活跃链接数 默认8
  13.         max-active: 5
  14.         # 最大空闲连接数 默认8
  15.         max-idle: 10
  16.         # 最小空闲连接数 默认0
  17.         min-idle: 0
  18.   secondaryRedis:
  19.     cluster:
  20.       nodes: 192.168.131.118:4883,192.168.131.118:4884,192.168.131.118:4885
  21. #      nodes:
  22. #        - 192.168.131.118:4883
  23. #        - 192.168.131.118:4884
  24. #        - 192.168.131.118:4885
  25.     password: advfafasfsa
  26. 复制代码

redis配置类

  1. @Configuration
  2. public class RedisConfig {
  3.     @Autowired
  4.     private Environment environment;
  5.     /**
  6.      * 配置lettuce连接池
  7.      *
  8.      * @return
  9.      */
  10.     @Bean
  11.     @Primary
  12.     @ConfigurationProperties(prefix = "spring.redis.cluster.lettuce.pool")
  13.     public GenericObjectPoolConfig redisPool() {
  14.         return new GenericObjectPoolConfig();
  15.     }
  16.     /**
  17.      * 配置第一个数据源的
  18.      *
  19.      * @return
  20.      */
  21.     @Bean("redisClusterConfig")
  22.     @Primary
  23.     public RedisClusterConfiguration redisClusterConfig() {
  24.         Map<String, Object> source = new HashMap<>(8);
  25.         source.put("spring.redis.cluster.nodes", environment.getProperty("spring.redis.cluster.nodes"));
  26.         RedisClusterConfiguration redisClusterConfiguration;
  27.         redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
  28.         redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
  29.         return redisClusterConfiguration;
  30.     }
  31.     /**
  32.      * 配置第一个数据源的连接工厂
  33.      * 这里注意:需要添加@Primary 指定bean的名称,目的是为了创建两个不同名称的LettuceConnectionFactory
  34.      *
  35.      * @param redisPool
  36.      * @param redisClusterConfig
  37.      * @return
  38.      */
  39.     @Bean("lettuceConnectionFactory")
  40.     @Primary
  41.     public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig redisPool, @Qualifier("redisClusterConfig") RedisClusterConfiguration redisClusterConfig) {
  42.         LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(redisPool).build();
  43.         return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
  44.     }
  45.     /**
  46.      * 配置第一个数据源的RedisTemplate
  47.      * 注意:这里指定使用名称=factory RedisConnectionFactory
  48.      * 并且标识第一个数据源是默认数据源 @Primary
  49.      *
  50.      * @param redisConnectionFactory
  51.      * @return
  52.      */
  53.     @Bean("redisTemplate")
  54.     @Primary
  55.     public RedisTemplate redisTemplate(@Qualifier("lettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
  56.         return getRedisTemplate(redisConnectionFactory);
  57.     }
  58.     /**
  59.      * 配置第二个数据源
  60.      *
  61.      * @return
  62.      */
  63.     @Bean("secondaryRedisClusterConfig")
  64.     public RedisClusterConfiguration secondaryRedisConfig() {
  65.         Map<String, Object> source = new HashMap<>(8);
  66.         source.put("spring.redis.cluster.nodes", environment.getProperty("spring.secondaryRedis.cluster.nodes"));
  67.         RedisClusterConfiguration redisClusterConfiguration;
  68.         redisClusterConfiguration = new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
  69.         redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
  70.         return redisClusterConfiguration;
  71.     }
  72.     @Bean("secondaryLettuceConnectionFactory")
  73.     public LettuceConnectionFactory secondaryLettuceConnectionFactory(GenericObjectPoolConfig redisPool, @Qualifier("secondaryRedisClusterConfig")RedisClusterConfiguration secondaryRedisClusterConfig) {
  74.         LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder().poolConfig(redisPool).build();
  75.         return new LettuceConnectionFactory(secondaryRedisClusterConfig, clientConfiguration);
  76.     }
  77.     /**
  78.      * 配置第一个数据源的RedisTemplate
  79.      * 注意:这里指定使用名称=factory2 RedisConnectionFactory
  80.      *
  81.      * @param redisConnectionFactory
  82.      * @return
  83.      */
  84.     @Bean("secondaryRedisTemplate")
  85.     public RedisTemplate secondaryRedisTemplate(@Qualifier("secondaryLettuceConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
  86.         return getRedisTemplate(redisConnectionFactory);
  87.     }
  88.     private RedisTemplate getRedisTemplate(RedisConnectionFactory factory) {
  89.         RedisTemplate<String, Object> template = new RedisTemplate<>();
  90.         template.setConnectionFactory(factory);
  91.         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
  92.         ObjectMapper om = new ObjectMapper();
  93.         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  94.         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
  95.         jackson2JsonRedisSerializer.setObjectMapper(om);
  96.         StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
  97.         // key采用String的序列化方式
  98.         template.setKeySerializer(stringRedisSerializer);
  99.         // hashkey也采用String的序列化方式
  100.         template.setHashKeySerializer(stringRedisSerializer);
  101.         // value序列化方式采用jackson
  102.         template.setValueSerializer(jackson2JsonRedisSerializer);
  103.         // hashvalue序列化方式采用jackson
  104.         template.setHashValueSerializer(jackson2JsonRedisSerializer);
  105.         template.afterPropertiesSet();
  106.         return template;
  107.     }
  108. }


参考:

Spring Boot 2.0 整合 Redis(Lettuce) - 简书
springboot整合redis - 烦嚣的人

https://www.nonelonely.com/article/1556289630491

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

Springboot2.x集成lettuce连接redis集群报超时异常Command timed out after 6 second(s) - 朱季谦 - 博客园

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值