【Redis】06-RedisTemplate及在SpringBoot工程中的综合应用

本文详细介绍了如何在SpringBoot项目中整合Redis进行缓存操作,包括RedisTemplate和StringRedisTemplate的使用,缓存命中率、移除策略等基础知识,以及缓存的连接测试、数据存取、序列化配置。此外,还展示了如何通过AOP实现缓存的一致性,并探讨了Redis集群配置。通过对缓存的深入理解和实践,提升系统的查询性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RedisTemplate应用

简介

RedisTemplate为SpringBoot工程中操作redis数据库的一个Java对象,此对象封装了对redis的一些基本操作。

准备工作

第一步:创建工程配置文件application.yml,其内容如下:

单机(非集群)模式配置

spring:
  redis:
    host: 192.168.64.128  #写自己的ip
    port: 6379 #写自己的port

在这里插入图片描述

在这里插入图片描述

第二步:创建工程启动类,例如:

package com.jt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class,args);
    }
}

第三步:创建工程测试类,添加测试连接方法 testConnection,例如:

@SpringBootTest
public class StringRedisTemplateTests {
  	@Autowired
    private StringRedisTemplate redisTemplate;

	@Test
	void testConnection() {
	    RedisConnection connection =
	         redisTemplate.getConnectionFactory().getConnection();
	    String ping = connection.ping();
	    System.out.println(ping);
	}
}


启动测试,连接成功!

在这里插入图片描述

缓存名词的介绍

缓存命中率

即从缓存中 读取数据的次数总读取次数 的比率,命中率越高越好:

命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好;

缓存策略

Eviction policy:移除策略,即如果缓存满了,从缓存中移除数据的策略;常见的有LFU、LRU、FIFO;

FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除;

LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除;

LFU(Least Frequently Used):最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除;

TTL(Time To Live ):存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)

TTI(Time To Idle):空闲期,即一个数据多久没被访问将从缓存中移除的时间。

快速入门实现

StringRedisTemplate 应用

StringRedisTemplate 是一个专门用于操作redis字符串类型数据的一个对象,其应用方式如下:

opsForValue() 操作redis中String类型

StringRedisTemplate 对象是spring data模块推出的一个用于操作redis字符串类型数据的一个API对象,

底层对key/value采用了字符串的序列化(StringRedisSerializer)方式进行redis数据的读写操作

实例:向redis中存值

@SpringBootTest
public class StringRedisTemplateTests {
    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 字符串数据基本操作实践
     */
    @Test
    void testStringOper01() throws InterruptedException {
        //1.获取一个操作redis字符串的值对象 ValueOperations值操作
        ValueOperations<String, String> vo = redisTemplate.opsForValue();
        //2.基于这个值对象操作redis中的字符串数据
        vo.set("x", "100");
        vo.increment("x");
        vo.set("y", "200", 1, TimeUnit.SECONDS);//设置key的失效时间
        TimeUnit.SECONDS.sleep(1);//阻塞1秒
        String x = vo.get("x");
        System.out.println(x);
        String y = vo.get("y");
        System.out.println(y);
    }
}

在这里插入图片描述

使用readValue()存储对象,需要先把对象转化为Json串,提示:不能使用Gson,可以使用jackson

实例:使用 StringRedisTemplate 方式向redis存储对象

    /**
     * 以json格式存储一个对象到redis数据库
     * 
     */
    @Test
    void testStringOper02() throws JsonProcessingException {
        //1.创建一个map,用于存储对象数据
        Map<String, String> map = new HashMap<>();
        map.put("id", "101");
        map.put("username", "jack");
        //2.将map转换为json格式字符串
        String jsonStr =
                new ObjectMapper().writeValueAsString(map);
       
        //3.将json字符串借助StringRedisTemplate存储到redis,并读取
        ValueOperations<String, String> vo =
                redisTemplate.opsForValue();
        vo.set("userJsonStr", jsonStr);
        jsonStr = vo.get("userJsonStr");
        
        map = new ObjectMapper().readValue(jsonStr, Map.class);
        System.out.println(map);
    }

在这里插入图片描述

opsForHash() 操作redis中Hash的类型

实例:基于StringRedisTemplate对象操作hash数据类型

 @Test
    void testHashOper01() {
        //1.获取操作hash类型数据的对象
        HashOperations<String, Object, Object> ho
                = redisTemplate.opsForHash();
        //2.操作hash类型数据
        //2.1存储一个对象
        ho.put("user", "id", "100");
        ho.put("user", "username", "jack");
        ho.put("user", "status", "true");
        //2.2获取一个对象
        //2.2.1获取对象某个属性值
        Object status = ho.get("user", "status");
        System.out.println(status);

        //2.2.2获取对象某个key对应的所有值
        List<Object> user = ho.values("user");
        System.out.println(user);
    }

在这里插入图片描述

RedisTemplate 应用

RedisTemplate对象也Spring data模块提供的一个操作redis数据库数据的API对象,是一个专门用于实现对远端redis中复杂数据的操作的对象,应用案例如下:

实例:测试连接

@SpringBootTest
public class RedisTemplateTests {
    //这个对象在springboot工程的RedisAutoConfiguration类中已经做了配置
    //此对象在基于redis存取数据时默认采用的JDK的序列化方式
    @Autowired
    private RedisTemplate redisTemplate;


    @Test
    void testGetConnection(){
        RedisConnection connection =
                redisTemplate.getConnectionFactory()
                        .getConnection();
        String ping = connection.ping();
        System.out.println(ping);
    }

}

在这里插入图片描述

RedisTemplate 这个对象在springboot工程的RedisAutoConfiguration类中已经做了配置,此对象在基于redis存取数据时默认采用的JDK的序列化方式。默认序列化方式为JdkSerializationRedisSerializer。

在这里插入图片描述

实例:测试字符串数据的存取

    @Test
    void testStringOper01() {
        // 默认采用了JDK的序列化方式
        // 存储key/value默认会将key/value都转换为字节再进行存储
        // redisTemplate.setKeySerializer(new JdkSerializationRedisSerializer());

        // 自己设置序列化方式
        // redisTemplate.setKeySerializer(new StringRedisSerializer());
        // redisTemplate.setValueSerializer(new StringRedisSerializer());
        ValueOperations vo = redisTemplate.opsForValue();
        vo.set("token", UUID.randomUUID().toString());
        Object token = vo.get("token");
        System.out.println(token);
    }

在这里插入图片描述
在这里插入图片描述

实例:测试hash数据的存取

    @Test
    void testHashOper01() {
        //1.获取操作hash类型数据的操作对象
        HashOperations ho = redisTemplate.opsForHash();
        //2.读写redis数据hash类型数据

        //直接存储一个map
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("id", 201);
        map.put("name", "redis");
        ho.putAll("tag", map);//hash 存储,序列化
        Object name = ho.get("tag", "name");
        System.out.println(name);

        //存储单个字段,值
        ho.put("blog", "id", 101);
        ho.put("blog", "title", "redis template");

        //取blog对象中id属性的值
        Object o = ho.get("blog", "id");
        System.out.println("id="+o);

        List lBlog = ho.values("blog");
        System.out.println(lBlog);

        //获取整个blog对象所有属性和值
        Map mBlog = ho.entries("blog");//反序列化
        System.out.println(mBlog);
    }

在这里插入图片描述

实例:测试list类型数据的读写

  @Test
    void testListData() {
        //1.获取操作list类型数据的操作对象
        ListOperations listOperations = redisTemplate.opsForList();
        //2.读写redis数据list类型数据
        //存数据
        listOperations.leftPush("lstKey1", "100"); //lpush
        listOperations.leftPushAll("lstKey1", "200", "300");
        listOperations.rightPushAll("lstKey1", "400", "500");
        listOperations.rightPush("lstKey1", "700");
        //取数据
        Object value = listOperations.range("lstKey1", 0, -1);
        System.out.println(value);

        //从list集合取数据
        Object v1 = listOperations.leftPop("lstKey1");//lpop
        System.out.println("left.pop.0=" + v1);

        value = listOperations.range("lstKey1", 0, -1);
        System.out.println(value);
    }

在这里插入图片描述

实例:测试Set类型数据的读写

    @Test
    void testSetOper01(){
        //1.获取操作Set类型数据的操作对象
        SetOperations so = redisTemplate.opsForSet();
        //2.读写redis数据Set类型数据
        so.add("set1", "A","B","C","C");//不允许重复
        Set set1 = so.members("set1");
        System.out.println(set1);
        //.........
    }

在这里插入图片描述

Redis在SpringBoot工程中的综合应用

业务描述

从一个博客数据库中查询所有的文章标签,然后存储到缓存(Cache),后续查询时可从缓存获取。提高其查询性能。

准备工作

初始化数据

初始化数据库中数据,SQL脚本如下:

DROP DATABASE IF EXISTS `blog`;
CREATE DATABASE `blog` DEFAULT character set utf8mb4;
SET names utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
USE `blog`;

CREATE TABLE `tb_tag` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(255) NOT NULL COMMENT 'data_id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tb_tag';

insert into `tb_tag` values (null,"mysql"),(null,"redis");

在这里插入图片描述

添加项目依赖

在jt-template工程中添加mysql数据库访问依赖,例如:

 <dependencies>
       <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>
       </dependency>

       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-test</artifactId>
           <scope>test</scope>
       </dependency>
       
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
       </dependency>
  
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>3.4.2</version>
       </dependency>
   </dependencies>

添加数据库访问配置

在项目的配置文件(例如application.yml)中添加数据库访问配置,例如:

spring:
  datasource:
    url: jdbc:mysql:///jt-sso?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root

业务逻辑代码设计及实现

Domain对象设计

/**
 * 标签类的设计
 */
@TableName("tb_tag")
public class Tag implements Serializable {
    private static final long serialVersionUID = 4504013456197711455L;
    /**标签id*/
    @TableId(type = IdType.AUTO)
    private Long id;
    /**标签名*/
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Tag{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Dao 逻辑对象设计

@Mapper
public interface TagMapper
        extends BaseMapper<Tag> {
}

编写测试类,测试能否连接成功MySql

@SpringBootTest
public class TagMapperTests {
    @Autowired
    private TagMapper tagMapper;
    @Test
    void testSelectList(){
        List<Tag> tags =
        tagMapper.selectList(null);
        for(Tag t:tags){
            System.out.println(t);
            //System.out.println(t.getId()+"/"+t.getName());
        }
    }
}

连接成功

在这里插入图片描述

Service 逻辑对象设计

设计TagService接口及实现类,定义Tag(标签)业务逻辑。

第一步:定义TagService接口,代码如下:

package com.jt.blog.service;
import com.jt.blog.domain.Tag;
import java.util.List;
public interface TagService {
    /**
     * 查询所有的标签
     * @return
     */
    List<Tag> selectTags();
}

第二步:定义TagServiceImpl类,实现逻辑

  1. 从redis查询Tag信息,redis有则直接返回
  2. 从redis没有获取tag信息,查询mysql
  3. 将从mysql查询到tag信息存储到redis
  4. 返回查询结果

代码如下:

package com.jt.blog.service.impl;

import com.jt.blog.dao.TagMapper;
import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class TagServiceImpl implements TagService {
    //RedisAutoConfiguration 类中做的RedisTemplate的配置
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private TagMapper tagMapper;
    @Override
    public List<Tag> selectTags() {
        //1.从redis查询Tag信息,redis有则直接返回
        ValueOperations<String,List<Tag>> valueOperations =
        redisTemplate.opsForValue();
        List<Tag> tags=valueOperations.get("tags");
        if(tags!=null&&!tags.isEmpty())return tags;
        //2.从redis没有获取tag信息,查询mysql
        tags = tagMapper.selectList(null);
        //3.将从mysql查询到tag信息存储到redis
        valueOperations.set("tags", tags);
        //4.返回查询结果
        return tags;
    }
}

说明,假如将List存储到redis,此时Tag必须实现Serializable接口

第三步:定义TagServiceTests单元测试类并进行单元测试,代码如下:

package com.jt.blog.service;

import com.jt.blog.domain.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
public class TagServiceTests {
    @Autowired
    private TagService tagService;
    
    @Test
    void testSelectTags(){
        List<Tag> tags=
        tagService.selectTags();
        System.out.println(tags);
    }
}

第一次查询,redis中没有数据,从数据库中取数据并存入redis中
在这里插入图片描述
在这里插入图片描述

第二次直接从redis中取数据

在这里插入图片描述

Controller逻辑对象设计

创建Tag控制逻辑对象,用于处理请求和响应逻辑,代码如下:

package com.jt.blog.controller;

import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/tag")
public class TagController {
    @Autowired
    private TagService tagService;
    
    @GetMapping
    public  List<Tag> doSelectTags(){
      return  tagService.selectTags());//1.redis,2.mysql
    }
}

启动服务,打开浏览器进行访问测试。同时思考,我们是否可以在这个层加一个本地cache。

在这里插入图片描述

业务逻辑代码优化

定制RedisTemplate对象

RedisTemplate默认采用的是JDK的序列化方式,假如对系统对序列化做一些调整,可以自己定义RedisTemplate对象,例如:

package com.jt;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.net.UnknownHostException;

@Configuration
public class RedisCacheConfig {
    //代码定制参考RedisAutoConfiguration类
    @Bean
    public RedisTemplate<Object,Object> redisTemplate(
         RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        System.out.println("===redisTemplate===");
        RedisTemplate<Object,Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        //定义redisTemplate对象的序列化方式
        //1.定义key的序列化方式
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);
        //2.定义Value的序列化方式
        Jackson2JsonRedisSerializer jsonRedisSerializer=
                new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        objectMapper.setVisibility(
                PropertyAccessor.GETTER,
                JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        jsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        //3.redisTemplate默认特性设置(除了序列化,其它原有特性不丢)
        template.afterPropertiesSet();
        return template;
    }
}

Service中缓存应用优化

目标:简化缓存代码的编写

解决方案:基于AOP(面向切面编程)方式实现缓存应用

实践步骤:

第一步:在启动上类添加@EnableCaching注解(开启AOP方式的缓存配置),例如:

@EnableCaching //启动AOP方式的缓存配置
@SpringBootApplication
public class RedisApplication {
 ....
}

第二步:重构TagServiceImpl中的selectTags()方法,方法上使用@Cacheable注解,例如:

@Cacheable(value = "tagCache")
@Override
public List<Tag> selectTags() {
    return tagMapper.selectList(null);
}

其中,@Cacheable描述的方法为AOP中的一个切入点方法,访问这个方法时,系统底层会通过一个拦截器,检查缓存中是否有你要的数据,假如有则直接返回,没有则执行方法从数据库查询数据

我们还可以定义Redis中key和value的序列化方式,修改key的生成策略,例如:

package com.jt.blog;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
    /**
     * 定义缓存key生成器,不定义也可以使用默认的。
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getName()); //获取包名
            sb.append("::");
            sb.append(method.getName()); //获取方法名
            for (Object param : params) {
                sb.append(param.toString());//获取参数列表
            }
            return sb.toString();
        };
    }
    /**
     * 自定义Cache管理器对象
     * 配置缓存管理器
     * 初始化一个Cache管理器对象
     */
    /**
     * 自定义Cache管理器对象,不定义也可以,有默认的,但假如希望基于AOP
     * 方式实现Redis的操作时,按照指定的序列化方式进行序列化,
     * 可以对CacheManager进行自定义。
     *
     * @param connectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config =
                RedisCacheConfiguration.defaultCacheConfig()
                        //设置key的有效期
                        .entryTtl(Duration.ofSeconds(60 * 60))

                        //配置key的序列化
                        .serializeKeysWith(RedisSerializationContext
                                .SerializationPair.fromSerializer(new StringRedisSerializer()))

                        //配置值的序列化
                        .serializeValuesWith(RedisSerializationContext
                                .SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class)))
                        //不存储null值
                        .disableCachingNullValues();

        return RedisCacheManager
                .builder(connectionFactory)
                .cacheDefaults(config)//修改默认配置
                .transactionAware()//启动默认事务
                .build();
    }
    
    .....
}

重写keyGenerator方法实现自定义redis缓存中的key值

缓存对象集合中,缓存是以key-value形式保存的。当不指定缓存的key时,SpringBoot会使用SimpleKeyGenerator生成key

在这里插入图片描述
在springboot中 org.springframework.cache.interceptor 包下的 KeyGenerator 接口

@FunctionalInterface
public interface KeyGenerator {
	Object generate(Object target, Method method, Object... params);
}

在springboot中 默认的SimpleKeyGenerator类

public class SimpleKeyGenerator implements KeyGenerator {

	@Override
	public Object generate(Object target, Method method, Object... params) {
		return generateKey(params);
	}

	/**
	 * Generate a key based on the specified parameters.
	 */
	public static Object generateKey(Object... params) {
		if (params.length == 0) {
			return SimpleKey.EMPTY;
		}
		if (params.length == 1) {
			Object param = params[0];
			if (param != null && !param.getClass().isArray()) {
				return param;
			}
		}
		return new SimpleKey(params);
	}

}
public SimpleKey(Object... elements) {
    Assert.notNull(elements, "Elements must not be null");
    this.params = new Object[elements.length];
    System.arraycopy(elements, 0, this.params, 0, elements.length);
    this.hashCode = Arrays.deepHashCode(this.params);
}

查看源码可以发现,它是使用方法参数组合生成的一个key

此时有一个问题:

使用默认的SpringBoot的SimpleKeyGenerator生成key,如果2个方法,参数是一样的,但执行逻辑不同,那么将会导致执行第二个方法时命中第一个方法的缓存。

解决办法是在@Cacheable注解参数中指定key,或者自己实现一个KeyGenerator,在注解中指定KeyGenerator。

但是如果这样的情况很多,每一个都要指定key、KeyGenerator很麻烦。

Spring同样提供了方案:继承CachingConfigurerSupport并重写keyGenerator()

以下是在上面代码中我们重写的keyGenerator()

@Bean
public KeyGenerator keyGenerator() {
    return (target, method, params) -> {
        StringBuilder sb = new StringBuilder();
        sb.append(target.getClass().getName()); //获取包名
        sb.append("::");
        sb.append(method.getName()); //获取方法名
        for (Object param : params) {
            sb.append(param.toString());//获取参数列表
        }
        return sb.toString();
    };
}

此时,缓存的key是包名+方法名+参数列表,这样就很难会冲突了。

测试:
在这里插入图片描述

自定义Cache管理器对象

自定义Cache管理器对象,不定义也可以,有默认的,但假如希望基于AOP方式实现Redis的操作时,按照指定的序列化方式进行序列化,可以对CacheManager进行自定义

Service中缓存一致性分析

当我们从数据库查询数据以后,假如将数据存入到了缓存,后续更新了数据库的数据,但假如没有更新缓存就会出现缓存数据与数据库数据不一致的这样的现象,对于这样问题,有时允许在一定时间范围之内存在。

假如我们希望在更新了数据库数据以后要更新缓存,如何实现呢?接下来通过一个案例,来演示和解决一下这个问题。

数据库与redis缓存中的数据同步更新

思考:假如系统中做了insert操作后,数据库中的数据更新了,也要更新redis缓存,你有什么方案?

方案1:不更新redis,等redis的key失效后,自动更新。

RedisCacheConfiguration config =
    RedisCacheConfiguration.defaultCacheConfig()//redis缓存配置.默认配置
    //设置key的有效期
    .entryTtl(Duration.ofSeconds(60 * 60))//持续时间60*60s

方案2:执行完insert,删除redis指定的key对应的数据。

在这里插入图片描述
@CacheEvict 注解的作用是定义切入点方法,执行此注解描述的方法时,底层通过AOP方式执行redis数据的清除操作。

在 TagServiceImpl 类中添加以下代码

@CacheEvict(value = "tagCache",
        allEntries = true,//删除key对应的所有数据
        beforeInvocation = false)//执行insert之后,删除缓存数据
@Override
public void insertTagOfDelRedis(Tag tag) {
    tagMapper.insert(tag);
}

测试类中执行 insertTagOfDelRedis(Tag tag) 方法

  @Test
    void testInsertTagOfDelRedis(){
        Tag tag = new Tag();
        tag.setName("SQLite");
        tagService.insertTagOfDelRedis(tag);
    }

查看数据库,数据已添加

在这里插入图片描述
再次执行 testSelectTags() 方法取缓存数据

第一次查询(redis对应的数据已清空,从数据库获取数据):
在这里插入图片描述

第二次查询(从redis缓存中获取):

在这里插入图片描述

方案3:执行完insert,更新redis指定key对应的内容

使用 @CachePut 注解实现,更新redis指定key对应的内容。

@Cacheable 注解描述的方法为缓存切入点方法访问此方法时系统底层会先从缓存查找数据,假如缓存缓存没有,会查询mysql数据库,这个注解假如想生效需要在启动类或者配置类上添加@EnableCaching注解。

其中,这里的value用于指定一个key前缀,没有指定key属性,则默认会使用 KeyGenerator对象创建key。

在 TagServiceImpl 类中添加以下代码

@Cacheable(value = "tagCache")
@Override
public List<Tag> selectTags() {
     return tagMapper.selectList(null);
}

测试类添加方法测试

   @Test
    void testInsertTagOfRefRedis(){
        Tag tag = new Tag();
        tag.setName("Hive");
        tagService.insertTagOfRefRedis(tag);
    }

由执行的结果我们发现 @CachePut 并不是在原缓存中覆盖去加新的数据,而是向redis中使当前方法名做为Key去新增一条数据

在这里插入图片描述

@CachePut 更适用于以下这种情况使用返回单个对象的数据放入缓存中而不是List集合

 /**
  * @CachePut 更适用于以下这种情况使用
  * 返回单个对象放入缓存中而不是List集合
  * @param tag
  */
 //删除key对应的所有数据
 @CacheEvict(value = "tagCache", allEntries = true, beforeInvocation = false)
 @CachePut(value = "tagCache",key="#tag.id")
 @Override
 public Tag updateTag(Tag tag){
     tagMapper.updateById(tag);
     return tag;
 }

 @Cacheable(value="tagCache",key="#id")
 @Override
 public Tag selectById(Long id){
     return tagMapper.selectById(id);
 }

Controller中添加本地缓存

在Controller中添加一个本地缓存,减少对远程redis缓存的访问,添加了synchronized锁以及双重if判断,用于解决线程安全的问题,例如:

package com.jt.blog.controller;

import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@RestController
@RequestMapping("/tag")
public class TagController {
    @Autowired
    private TagService tagService;
    //此对象存哪了?(JVM)
    private List<Tag> tags=new CopyOnWriteArrayList<>();//本地 cache
    @GetMapping
    public  List<Tag> doSelectTags(){
       if(tags.isEmpty()) {
           synchronized (tags) {
               if(tags.isEmpty()) {
                   tags.addAll(tagService.selectTags());//1.redis,2.mysql
               }
           }
       }
       return tags;
    }
}

Controller中本地缓存一致性分析

此次项目案例中,我们在Controller层添加了本地缓存,这个缓存我们也需要考虑其缓存一致性,其相关代码实现如下:

package com.jt.blog.controller;

import com.jt.blog.domain.Tag;
import com.jt.blog.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;

@RestController
@RequestMapping("/tag")
public class TagController {
    @Autowired
    private TagService tagService;
    //private List<Tag> tags=new ArrayList<>();
    private List<Tag> tags=new CopyOnWriteArrayList<>();//本地 cache


    @GetMapping("/{id}")
    public Tag doSelectById(@PathVariable("id") Long id){
        //查询本地缓存
        for(Tag t:tags){
            if(t.getId().equals(id)) return t;
        }
        //查询redis,mysql
        return tagService.selectById(id);
    }

    @PostMapping
    public String doInsertTag(Tag tag){
        //向数据库写入数据
        tagService.insertTag(tag);//A
        //更新本地缓存
        tags.add(tag);
        return "insert ok";
    }

    @PutMapping
    public String doUpdateTag(Tag tag){//id=1,name=mysql 8.9
        //向数据库写入数据
        tagService.updateTag(tag);
        //更新本地缓存
        for(Tag t:tags){
            if(t.getId().equals(tag.getId())){
                t.setName(tag.getName());
            }
        }
        return "update ok";
    }


    @GetMapping
    public  List<Tag> doSelectTags(){//B
       if(tags.isEmpty()) {
           synchronized (tags) {
               if(tags.isEmpty()) {
                   tags.addAll(tagService.selectTags());//1.redis,2.mysql
               }
           }
       }
       return tags;
    }
    /**Spring中Bean对象的生命周期方法,对象初始化时执行此方法*/
    @PostConstruct
    public void doInit(){
        doTimerRefreshTask();
    }
    /**Spring中Bean对象的生命周期方法,Bean对象初始化时执行此方法*/
    @PreDestroy
    public void doDestory(){
        //退出定时任务
        timer.cancel();
    }
    private Timer timer;
    //定义刷新任务
    private void doTimerRefreshTask(){
        //构建一个定时任务调度对象
        timer=new Timer();
        //构建一个任务对象
        TimerTask task=new TimerTask() {
            @Override
            public void run() {
                System.out.println("refresh cache");
                tags.clear();
            }
        };
        //执行任务对象(每隔5秒执行一次)
        timer.schedule(task, 5000, 5000);
    }

}

其中

Redis集群链接配置实践

修改项目中的application.yml配置文件,修改redis配置,采用集群方式进行实现,例如:

spring:
  datasource: #默认配置的是HikariDataSource,应用的是HikariCP链接池(HikariPool)
    url: jdbc:mysql:///blog?serverTimezone=Asia/Shanghai&characterEncoding=utf8
    username: root
    password: root
#redis 集群配置
  redis:
    cluster: #redis 集群配置
      nodes: 192.168.126.129:8010,192.168.126.129:8011,192.168.126.129:8012,192.168.126.129:8013,192.168.126.129:8014,192.168.126.129:8015
      max-redirects: 3 #最大跳转次数
    timeout: 5000 #超时时间
    database: 0
    jedis: #连接池
      pool:
        max-idle: 8
        max-wait: 0
#日志配置
logging:
  level:
    com.jt: debug

总结(Summary)

本章节重点是学习项目中缓存(Cache)的一种应用思想。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值