SpringBoot整合Redis

一、概述

这里关于为什么要使用redis不做赘述,主要介绍SpringBoot集成redis做缓存,采用Spring Boot 框架提供的一个快速集成 Redis 数据库的启动器spring-boot-starter-data-redis

spring-boot-starter-data-redis 主要引入了三个依赖:

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.7.5</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>2.7.5</version>
      <scope>compile</scope>
    </dependency>
    <!--引入的lettuce实现,这就是为啥默认是lettuce的原因-->
    <dependency>
      <groupId>io.lettuce</groupId>
      <artifactId>lettuce-core</artifactId>
      <version>6.1.10.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

1.spring-boot-starter:自动装配的核心逻辑,SpringBoot中默认可以自动装配的starter也定义在其中的spring-boot-autoconfigure中的spring.factories文件中,本文要讲的RedisAutoConfiguration就定义在其中。有了这个包后,即拥有了自动装配的能力了,例如新建一个springboot项目,pom中只引入spring-boot-starter-data-redis包,其就能自动装配了。

2.spring-data-redisspringredis相关操作的人性化封装,使得redis的操作只需简单的调用接口即可,redis的操作的实现过程则有lettucejedis驱动(客户端)实现,spring-data-redis只是在接口层对他们做了统一。spring-data-redis包依赖了jedislettuce-core这两个驱动,但是optionaltrue,即这两个依赖不会传递到spring-boot-starter-data-redis中。
lettuce-core就是spring-boot-starter-data-redis选择的默然驱动

<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>${jedis}</version>
	<optional>true</optional>
</dependency>

<dependency>
	<groupId>io.lettuce</groupId>
	<artifactId>lettuce-core</artifactId>
	<version>${lettuce}</version>
	<optional>true</optional>
</dependency>

二、快速入门

2.1 pom文件中引入Redis依赖

 <!-- spring-boot redis -->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
     <exclusions>
         <!-- 去掉对 Lettuce 的依赖,因为 Spring Boot默认使用 Lettuce 作为 Redis 客户端 -->
         <exclusion>
             <groupId>io.lettuce</groupId>
             <artifactId>lettuce-core</artifactId>
         </exclusion>
     </exclusions>
 </dependency>

 <!-- 引入 Jedis 的依赖,这样 Spring Boot 实现对 Jedis 的自动化配置 -->
 <dependency>
     <groupId>redis.clients</groupId>
     <artifactId>jedis</artifactId>
 </dependency>
 
 <!-- 阿里JSON解析器 Redis序列化使用 -->
 <dependency>
     <groupId>com.alibaba.fastjson2</groupId>
     <artifactId>fastjson2</artifactId>
     <version>2.0.25</version>
 </dependency>

2.2 application.yml中添加Redis配置

spring:
  # 对应 RedisProperties 类
  redis:
    host: 192.168.10.110
    port: 6379
    database: 0
    # Redis 连接超时时间,单位:毫秒。
    timeout: 0
    # 对应 RedisProperties.Jedis 内部类
    jedis:
      pool:
        # 连接池最大连接数,默认为 8 。使用负数表示没有限制。
        max-active:  8
        # 默认连接数最小空闲的连接数,默认为 8 。使用负数表示没有限制。
        max-idle:    8
        # 默认连接池最小空闲的连接数,默认为 0 。允许设置 0 和 正数。
        min-idle: 0
        # 连接池最大阻塞等待时间,单位:毫秒。默认为 -1 ,表示不限制。
        max-wait: -1

2.3 序列化

@Configuration
public class RedisConfig {


    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    /**
     * 限流脚本
     */
    private String limitScriptText() {
        return "local key = KEYS[1]\n" +
                "local count = tonumber(ARGV[1])\n" +
                "local time = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key);\n" +
                "if current and tonumber(current) > count then\n" +
                "    return tonumber(current);\n" +
                "end\n" +
                "current = redis.call('incr', key)\n" +
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "return tonumber(current);";
    }
}



public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
    }
}

2.4 进一步封装RedisTemplate

package com.xmc.cache;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @Title: RedisCache
 * @Package: com.xmc.cache
 * @Description: spring redis 工具类
 * @Author: 李建奎
 * @Date: 创建时间 2023-06-21
 */
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Resource
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key)
    {
        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key)
    {
        return redisTemplate.hasKey(key);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection) > 0;
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey)
    {
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

2.5 测试

@SpringBootTest(classes = SpringBootCacheRedisApplication.class)
public class SpringBootCacheRedisApplicationTest {

    @Resource
    private RedisCache redisCache;

    @Test
    public void test() {
        redisCache.setCacheObject("name", "小木虫");
        final Object name = redisCache.getCacheObject("name");
        System.out.println(name);

        final ArrayList<User> users = ListUtil.toList(User.builder().name("张三").age(12).build()
                , User.builder().name("里斯").age(16).build());
        redisCache.setCacheList("users", users);
        final List<Object> users1 = redisCache.getCacheList("users");
        System.out.println(users1);

        final HashMap<String, Object> maps = MapUtil.newHashMap();
        maps.put("key1",users);
        maps.put("key2",users);
        redisCache.setCacheMap("maps", maps);
        final Map<String, Object> maps1 = redisCache.getCacheMap("maps");
        System.out.println(maps1);
    }

}

三、理论进阶

3.1 Jedis和Lettuce的区别

  • Lettuce 是一个可伸缩的线程安全的 Redis 客户端,支持同步、异步和响应式模式。多个线程可以共享一个连接实例,而不必担心多线程并发问题。它基于优秀 Netty NIO 框架构建,支持 Redis 的高级功能,如 Sentinel,集群,流水线,自动重新连接和 Redis 数据模型

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

  • 从 Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。当然 Spring Boot 2.x 仍然支持 Jedis,并且你可以任意切换客户端,只需排除io.lettuce:lettuce-core并添加redis.clients:jedis即可。就稳定性而言,个人建议使用Jedis

总之,Lettuce 和 Jedis 都有其优缺点,具体选择哪个取决于具体的应用场景和需求。如果需要处理高并发、多线程、事务等高级功能,或者需要支持 Sentinel 和 Cluster 集群模式,建议使用 Lettuce;如果对性能要求不太高,API 设计要求简单易用,可以考虑使用 Jedis。

3.2 Redis序列化


主要分成四类:

  • JDK 序列化方式
  • String 序列化方式
  • JSON 序列化方式
  • XML 序列化方式

默认使用的JdkSerializationRedisSerializer序列化,key和value都会拼接一连串16进制字符(实际就是标志位 + 字符串长度 + 字符串内容),这样势必会对读取造成很大不方便,实际的使用场景还是JSON序列化便捷很多

实际使用场景中我们一般key采用StringRedisSerializer进行序列化,value采用JSON序列化

四、项目中遇到的问题

主要记录个人使用过程中发现的问题,但具体问题需要具体分析

4.1 读取数据的时候提示缓存区不足

nested exception is io.lettuce.core.RedisException: java.lang.IndexOutOfB
oundsException: writerIndex(2147419686) + minWritableBytes(65536) exceeds maxCapacity(2147483647): PooledUnsafeDirectByteBuf(ridx: 2147419583, 
widx: 2147419686, cap: 2147483647)

这个异常通常是由于 Redis 返回的数据超过了 Lettuce 的缓冲区容量所导致的。具体来说,Lettuce 在处理 Redis 响应时,会将响应数据暂存到 ByteBuf 缓冲区中,当数据量超过缓冲区容量时就会抛出 IndexOutOfBoundsException 异常。

我在项目中开启了2000多个任务,其中都会多次不断的访问redis,频繁的访问redis导致Lettuce 的缓冲区容量爆满所致

科普一下Lettuce的缓存区

在 Lettuce 中,缓冲区是一个 ByteBuf 对象,用于暂存从 Redis 服务器返回的响应数据。Lettuce 使用 Netty 作为底层网络通信框架,Netty 的 ByteBuf 是一个可扩展的、读写分离的缓冲区实现,可以高效地进行数据读写操作。

在 Lettuce 的缓冲区中,有以下几个重要的属性:

- readerIndex:表示当前缓冲区的读取位置。
- writerIndex:表示当前缓冲区的写入位置。
- capacity:表示当前缓冲区的容量大小。

当从 Redis 服务器返回的响应数据量较大时,如果超过了 Lettuce 缓冲区的容量大小,就会抛出 IndexOutOfBoundsException 异常。

4.2 redis命令超时

Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeOut 

RedisCommandTimeoutException 异常表示 Redis 命令执行超时,通常出现在 Redis 服务器负载过高、网络状况不佳或者 Redis 命令执行时间过长等情况下。当 Redis 命令执行超时时,Lettuce 客户端会抛出该异常,以提示开发人员需要采取相应的措施来优化 Redis 操作

以下是一些可能导致 Redis 命令执行超时的原因和解决方法:

  • Redis 命令执行时间过长
    如果 Redis 命令的执行时间过长,可能会导致连接池中的连接被占用太久,从而影响后续请求的处理。可以通过优化 Redis 查询语句、增加 Redis 实例的内存和 CPU 资源、分片等方式来减少 Redis 命令的执行时间。

  • 网络状况不佳
    如果网络延迟或带宽不足,可能会导致 Redis 命令的执行时间变长,甚至超时。可以尝试优化网络拓扑、调整网络参数、使用更高带宽的网络服务商等方式来改善网络状况。

  • Redis 服务器负载过高
    如果 Redis 服务器负载过高,可能会导致 Redis 命令的执行时间变长,甚至超时。可以考虑扩容 Redis 集群、增加 Redis 实例的内存和 CPU 资源、优化 Redis 配置文件等方式来降低 Redis 服务器的负载。

  • Lettuce 客户端超时设置过短
    Lettuce 客户端提供了多种超时设置,如连接超时、命令执行超时等。如果超时时间设置过短,可能会导致 Redis 命令执行超时。可以根据实际情况适当延长 Lettuce 的超时时间。

总之,RedisCommandTimeoutException 异常通常是由于 Redis 命令执行超时所导致的,可以通过优化 Redis 查询语句、改善网络状况、增加 Redis 实例的资源、优化 Redis 配置文件等方式来减少 Redis 命令的执行时间,从而避免此类异常的出现。

网上很多解决方案是采用Jedis客户端解决的,我这里也是直接切换的Jedis

4.3 java文件GBK写入redis中文乱码

idea中Java文件编码是GBK,将项目的编码方式修改程UTF-8

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值