优雅的缓存解决方案--SpringCache和Redis集成(SpringBoot)

x33g5p2x  于2022-03-21 转载在 Java  
字(7.5k)|赞(0)|评价(0)|浏览(334)

1. 前言

一个系统在于数据库交互的过程中,内存的速度远远快于硬盘速度,当我们重复地获取相同数据时,我们一次又一次地请求数据库或远程服务,者无疑时性能上地浪费(这会导致大量时间被浪费在数据库查询或者远程方法调用上致使程序性能恶化),于是有了“缓存”。
本文将介绍在spring boot项目开发中怎样使用spring提供的Spring Cache 与最近很火的 Redis 数据库来实现数据的缓存。

2. SpringCache简介

Spring CacheSpring框架提供的对缓存使用的抽象类,支持多种缓存,比如RedisEHCache等,集成很方便。同时提供了多种注解来简化缓存的使用,可对方法进行缓存。

2.1 关于SpringCache 注解的简单介绍

  • @Cacheable:标记在一个方法上,也可以标记在一个类上。主要是缓存标注对象的返回结果,标注在方法上缓存该方法的返回值,标注在类上,缓存该类所有的方法返回值。
    参数: value缓存名、 key缓存键值、 condition满足缓存条件、unless否决缓存条件

  • @CacheEvict:从缓存中移除相应数据。

  • @CachePut:方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

  • @Caching:多个Cache注解使用,比如新增用户时,删除用户属性等需要删除或者更新多个缓存时,集合以上三个注解。

2.2 SpEL上下文数据

Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:

名字位置描述示例
methodNameroot对象当前被调用的方法名#root.methodName
methodroot对象当前被调用的方法#root.method.name
targetroot对象当前被调用的目标对象#root.target
targetClassroot对象当前被调用的目标对象类#root.targetClass
argsroot对象当前被调用的方法的参数列表#root.args[0]
cachesroot对象当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache#root.caches[0].name
argument name执行上下文当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数#user.id
result执行上下文方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict’的beforeInvocation=false)#result

其他关于 Cache 详细配置或注解,请参考文章基于Redis的Spring cache 缓存介绍或spring官方文档

3. Redis简介

Redis 是完全开源免费的,遵守BSD协议,是一个高性能的key-value数据库。

Redis 与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

Redis 的安装和使用请自行Google 。

4. 实践–SpringCache和Redis集成

4.1 步骤

我们要把一个查询函数加入缓存功能,大致需要三步。

  • 一、在函数执行前,我们需要先检查缓存中是否存在数据,如果存在则返回缓存数据。
  • 二、如果不存在,就需要在数据库的数据查询出来。
  • 三、最后把数据存放在缓存中,当下次调用此函数时,就可以直接使用缓存数据,减轻了数据库压力。

本实例没有存入MySQL数据库,主要是为了方便实践,实际使用中大家可以把service层中的方法改为数据库操作代码即可。

4.2 具体操作

添加依赖

<!-- springboot redis依赖-->
        <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

注: 其实我们从官方文档可以看到spring-boot-starter-data-redis 已经包含了jedis客户端,我们在使用jedis连接池的时候不必再添加jedis依赖。

  • 官方文档Spring Data Redis

配置SpringCache,Redis连接等信息

  • SpringCache配置–RedisConfig.java
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:04
 * @Version 1.0
 */

@Configuration
@EnableCaching
public class RedisConfig {

    /**
     * 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)
     * 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值

     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return RedisCacheManager.create(redisConnectionFactory);
    }

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        // 创建一个模板类
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        // 将刚才的redis连接工厂设置到模板类中
        template.setConnectionFactory(factory);
        // 设置key的序列化器
        template.setKeySerializer(new StringRedisSerializer());
        // 设置value的序列化器
        //使用Jackson 2,将对象序列化为JSON
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //json转对象类,不设置默认的会将json转成hashmap
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);

        return template;
    } 
}
  • redis配置–application.yml
server:
  port: 8080
spring:
  # redis相关配置
  redis:
    database: 0
    host: localhost
    port: 6379
    password: 123456
    jedis:
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
    # 连接超时时间(毫秒)默认是2000ms
    timeout: 2000ms
  cache:
    redis:
      ## Entry expiration in milliseconds. By default the entries never expire.
      time-to-live: 1d
      #写入redis时是否使用键前缀。
      use-key-prefix: true

编写实体类

import lombok.Data;
import java.io.Serializable;

/**
 * @Author: MaoLin
 * @Date: 2019/3/24 14:36
 * @Version 1.0
 */

@Data
//lombok依赖,可省略get set方法
public class User implements Serializable {

    private int userId;

    private String userName;

    private String userPassword;

    public User(int userId, String userName, String userPassword) {
        this.userId = userId;
        this.userName = userName;
        this.userPassword = userPassword;
    }
}

service简单操作

import com.ml.demo.entity.User;
import org.springframework.stereotype.Service;

/**
 * @Author: MaoLin
 * @Date: 2019/3/24 14:38
 * @Version 1.0
 */
@Service
public class UserDao {

    public User getUser(int userId) {
        System.out.println("执行此方法,说明没有缓存,如果没有走到这里,就说明缓存成功了");
        User user = new User(userId, "没有缓存_"+userId, "password_"+userId);
        return user;
    }

    public User getUser2(int userId) {
        System.out.println("执行此方法,说明没有缓存,如果没有走到这里,就说明缓存成功了");
        User user = new User(userId, "name_nocache"+userId, "nocache");
        return user;
    }
}

控制层

  • 在方法上添加相应的方法即可操作缓存了,SpringCache 对象可以对redis自行操作,减少了很多工作啊,还是那个开箱即用的Spring
import com.ml.demo.dao.UserDao;
import com.ml.demo.entity.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

/**
 * @Author: MaoLin
 * @Date: 2019/3/26 17:03
 * @Version 1.0
 */


@RestController
public class testController {
    @Resource
    private UserDao userDao;

    /**
     * 查询出一条数据并且添加到缓存
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser")
    @Cacheable("userCache")
    public User getPrud(@RequestParam(required = true) String userId) {
        System.out.println("如果没有缓存,就会调用下面方法,如果有缓存,则直接输出,不会输出此段话");
        return userDao.getUser(Integer.parseInt(userId));
    }

    /**
     * 删除一个缓存
     *
     * @param userId
     * @return
     */
    @RequestMapping(value = "/deleteUser")
    @CacheEvict("userCache")
    public String deleteUser(@RequestParam(required = true) String userId) {
        return "删除成功";
    }

    /**
     * 添加一条保存的数据到缓存,缓存的key是当前user的id
     *
     * @param user
     * @return
     */
    @RequestMapping("/saveUser")
    @CachePut(value = "userCache", key = "#result.userId +''")
    public User saveUser(User user) {
        return user;
    }


    /**
     * 返回结果userPassword中含有nocache字符串就不缓存
     *
     * @param userId
     * @return
     */
    @RequestMapping("/getUser2")
    @CachePut(value = "userCache", unless = "#result.userPassword.contains('nocache')")
    public User getUser2(@RequestParam(required = true) String userId) {
        System.out.println("如果走到这里说明,说明缓存没有生效!");
        User user = new User(Integer.parseInt(userId), "name_nocache" + userId, "nocache");
        return user;
    }


    @RequestMapping("/getUser3")
    @Cacheable(value = "userCache", key = "#root.targetClass.getName() + #root.methodName + #userId")
    public User getUser3(@RequestParam(required = true) String userId) {
        System.out.println("如果第二次没有走到这里说明缓存被添加了");
        return userDao.getUser(Integer.parseInt(userId));
    }
}

接下来最重要的工作:跑起来

运行结果

  • 存入数据:

  • 从缓存读取数据:

  • 删除缓存:

  • 再读取:

  • 此时没有缓存,调用方法,并存入缓存

  • 此为cache中的条件:含有nocache字符时不存入缓存。自己去探索就好。

5. 小结&参考资料

小结

为了实现缓存,在网上参考了很多博客、资料,但是都不尽人意,后来经过几天的学习,发现Spring提供了缓存对象,我结合redis,优雅地实现了缓存。学习代码是个艰辛的过程,我在学习这部分时看了好多书,找了好多博客资料,终于找到了合适的缓存方案,很开心,不过这还只是一小步啊,加油!!!

参考资料

相关文章