深入浅出Spring Boot整合Redis

网址

https://docs.spring.io/spring-data/redis/docs/2.4.2/reference/html/#reference

环境安装

Wndows环境

https://github.com/tporadowski/redis/releases

在这里插入图片描述
第一步:下载 zip 压缩包,第二步双击 redis-server.exe 启动 redis 服务。
在这里插入图片描述
当看到下面的截图代表启动成功
在这里插入图片描述

快速上手

第一步:添加依赖

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

第二步:配置文件修改

application.yml

spring:
  redis:
    database: 0
    host: 127.0.0.1
    password:
    port: 6379

第三步:开发一个简单的 controller

@RequestMapping("quick")
@RestController
public class QuickController {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @GetMapping("hello")
    public void hello() {
        stringRedisTemplate.opsForValue().set("k", "v");
    }
}

第四步:使用 Redis 可视化界面查看 key
在这里插入图片描述

StringRedisTemplate 自动装配原理

在这里插入图片描述

在这里插入图片描述
在条件注解的作用下,默认情况下会为我们创建两个可以直接注入的Bean对象,由于 @Import 导入在先,所以 LettuceConnectionConfiguration 先进行装配在容器中创建这两个 bean 对象。

在这里插入图片描述
当走到 Jedis 这里时,有两个条件注解不满足,第一个是缺失 Jedis 类,第二个是 容器中没有 连接工厂 这个 Bean,所以 Jedis 配置并不会生效,最终使用 Lettuce 客户端。
在这里插入图片描述

commons-pool2 报错原理分析

问题复现:当我们尝试配置 lettuce pool 的时候会报错。

    lettuce:
      pool:
        min-idle: 1

为什么:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'redisCacheAop': Unsatisfied dependency expressed through field 'stringRedisTemplate'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'stringRedisTemplate' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/RedisAutoConfiguration.class]: Unsatisfied dependency expressed through method 'stringRedisTemplate' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'redisConnectionFactory' defined in class path resource [org/springframework/boot/autoconfigure/data/redis/LettuceConnectionConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory]: Factory method 'redisConnectionFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig

首先对错误进行一个定性,NoClassDefFoundError 说明没有找到 GenericObjectPoolConfig ,没有这个类
会导致 redisConnectionFactory 创建失败,又会导致 stringRedisTemplate 创建失败,最终导致注入失败。

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	LettuceConnectionFactory redisConnectionFactory(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
				getProperties().getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}

在 LettuceConnectionFactory 创建过程中会执行 getLettuceClientConfiguration 这个方法,一路点进去在

在这里插入图片描述
这个里面用到了 GenericObjectPoolConfig 这个类,因为找不到类所以最终 LettuceConnectionFactory 创建失败。

怎么解决:

  1. 保留池相关的配置,新增 commons-pool2 的依赖提供 GenericObjectPoolConfig 这个类。
  2. 删除池相关的配置。

验证:

当池配置存在的时候,getLettuceClientConfiguration 的 pool 参数正常。
在这里插入图片描述

当池配置不存在的时候,getLettuceClientConfiguration 的 pool 参数变为 null ;
在这里插入图片描述
由于在 createbuilder 中存在两种不同的实现,所以在配置池的过程中会抛出异常。
在这里插入图片描述
为什么 Pool 在参数配置时会产生两种效果呢,因为静态内部类只有在使用过程中初始化,所以没有配置就不会创建内部类对象。

Spring Boot 操作 Redis 的几种方式

编程式用法

StringRedisTemplate

    @Autowired
    StringRedisTemplate stringRedisTemplate;

默认 RedisTemplate

    @Autowired
    RedisTemplate<Object, Object> redisTemplate;

自定义 RedisTemplate

如果觉得RedisTemplate不好用可以自定义,例如替换序列化方式等。

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        // 使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //不加会生成LinkedHashMap
        // objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        //采用jackson
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        return redisTemplate;
    }

总结

我会更推荐使用 StringRedisTemplate 这种方式,存储 String ,Json 格式的内容,这样无论是 Java 还是其他语言均可以从Redis中读到内容并解析,跨语言方面更好。

声明式用法

自定义注解

第一步:声明一个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisCache {

    /**
     * 过期时间
     */
    int expired() default 60;

    /**
     * 过期时间单位
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 键
     */
    String key() default "";

    /**
     * 是否开启异步
     */
    boolean async() default false;

}

第二步:使用 AOP 拦截注解并在目标方法执行前后进行相应操作

@Aspect
@Component
public class RedisCacheAop {

    private final static Logger logger = LoggerFactory.getLogger(RedisCacheAop.class);

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    /**
     * 空值缓存-缓存穿透
     *
     * @param joinPoint
     * @param redisCache
     * @return
     */
    @Around("@annotation(redisCache)")
    public Object fastjsonSerialStringRedisTemplate(ProceedingJoinPoint joinPoint, RedisCache redisCache) {
        //
        String key = redisCache.key();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        if (key == null || key.length() == 0) {
            String[] split = method.getDeclaringClass().getName().split("\\.");
            String className = split[split.length - 1];
            String methodName = method.getName();
            key = className + ":" + methodName;
        }
        //
        String value = null;
        try {
            value = stringRedisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            logger.error(e.toString());
        }

        Object result;
        // value = null , execute method get value
        if (ObjectUtils.isEmpty(value)) {
            try {
                //执行目标方法
                result = joinPoint.proceed();
                //after
                value = JSON.toJSONString(result);
                TimeUnit timeUnit = redisCache.timeUnit();
                Integer expired = expireCheck(redisCache.expired());
                stringRedisTemplate.opsForValue().set(key, value, expired, timeUnit);
            } catch (Throwable throwable) {
                logger.error(throwable.toString());
            }
        }
        // value live
        Class<?> returnType = method.getReturnType();
        result = JSON.parseObject(value, returnType);
        return result;
    }



    /**
     * 检验是否规范
     *
     * @param expire
     * @return
     */
    private Integer expireCheck(int expire) {
        if (expire < 0) {
            expire = 60;
        }
        return expire;
    }

}

第三部:测试类

@RestController
public class TestController {

    @RedisCache(expired = 300)
    @GetMapping("list")
    public ArrayList<Integer> test() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        return list;
    }

    @RedisCache(expired = 300)
    @GetMapping("map")
    public Map<String, String> map() {
        HashMap<String, String> map = new HashMap<>();
        map.put("s1", "s1");
        map.put(null, "s2");
        map.put("s3", null);
        map.put(null, null);
        return map;
    }


    @RedisCache(expired = 30)
    @GetMapping("a")
    public A a() {
        A a = new A();
        a.setA("sdads");
        return a;
    }

    @Data
    class A{
        String a;
    }

}

Spring Boot SPI Cache

路人甲 Spring Cache

https://docs.spring.io/spring-framework/docs/5.3.3/reference/html/integration.html#cache-annotations

https://docs.spring.io/spring-data/redis/docs/current/reference/html/

Spring Boot 为我们提供了一个 Cache 包,可以快速的实现缓存功能。

在这里插入图片描述

快速上手

第一步:修改配置文件

spring:
  cache:
    type: redis

第二步:编写配置类,使用 EnableCaching 注解开启缓存。

@Configuration
@EnableCaching
public class SpringCacheConfig { 
}

第三步:写测试接口

@RestController
@RequestMapping("spring-cache")
public class SpringCacheController {

    @GetMapping("list")
    @Cacheable(cacheNames = "list")
    public List<String> list() {
        System.out.println("---方法返回数据---");
        return Arrays.asList("abcde", "12345", "!@#$%");
    }
}

多次请求接口,日志只有第一次输出结果,打开缓存工具发现已经具有了缓存功能。

在这里插入图片描述

不同的缓存注解

在这里插入图片描述

Cacheable
CachePut
CacheEvict
Caching
CacheConfig
解决乱码问题

Redisson

第一步:添加依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.17.7</version>
        </dependency>

第二步:封装为一个Bean对象

@Configuration(proxyBeanMethods = false)
public class RedissonConfig {

    private static final Logger logger = LoggerFactory.getLogger(RedissonConfig.class);

    @Bean
    public RedissonClient redissonClient(RedisProperties properties) {
        Config config = new Config();
        SingleServerConfig serverConfig = config.useSingleServer();
        serverConfig.setAddress("redis://" + properties.getHost() + ":" + properties.getPort());
        String password = ObjectUtils.isEmpty(properties.getPassword())
                ? null
                : properties.getPassword();
        serverConfig.setPassword(password);
        config.setCodec(new org.redisson.codec.JsonJacksonCodec());
        RedissonClient redissonClient = Redisson.create(config);
        logger.info("RedissonClient Create Success ...... ....... ");
        return redissonClient;
    }
}    

发布、订阅

官方文档参考链接 https://redis.io/docs/manual/pubsub/

RedisPubSubCommands 是一个接口包含发布订阅抽象实验。

keepttl(KEEPTTL )

https://redis.io/commands/set/

KEEPTTL – Retain the time to live associated with the key.

Starting with Redis version 6.0.0: Added the KEEPTTL option.

自从6.0版本开始Redis新增命令keepttl保留上一个key的过期时间。

可以通过 stringRedisTemplate 去自定义实现,

    /**
     * 如果过期时间不存在,永久有效,Redis version > 6.0
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean setKeepTtl(byte[] key, byte[] value) {
        return stringRedisTemplate.execute((connection) -> {
            return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.UPSERT);
        }, false);
    }

    /**
     * XX -- Only set the key if it already exist.,Redis version > 6.0
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean setExKeepTtl(byte[] key, byte[] value) {
        return stringRedisTemplate.execute((connection) -> {
            return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.SET_IF_PRESENT);
        }, false);
    }

    /**
     * NX -- Only set the key if it does not already exist.,Redis version > 6.0
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean setNxKeepTtl(byte[] key, byte[] value) {
        return stringRedisTemplate.execute((connection) -> {
            return connection.stringCommands().set(key, value, Expiration.keepTtl(), RedisStringCommands.SetOption.SET_IF_ABSENT);
        }, false);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值