SpringBoot 应用 Redis 声明式缓存

什么是声明式缓存 ?

Spring 框架提供一种抽象的缓存机制,且 Spring 只提供接口,不提供缓存的具体实现。所以在程序中使用时,需要有具体缓存的实现。目前支持的常见的缓存比如 JDK ConcurrentMap-based Cache、Ehcache、Redis、Caffeine Cache、Guava Cache 等。

所谓声明式缓存,即使用 Spring 框架提供的注解来使用缓存功能。这就需要知道一些常用的注解:

  • @EnableCaching

Spring 默认没有开启缓存注解支持,可以在配置类上使用该注解进行开启。

  • @Cacheable

程序在执行方法时首先会去缓存中检查 key 对应的 value 是否存在。如果存在,则方法体不再执行,直接取缓存中的 value 返回;否则执行方法体,并将方法的返回值进行缓存。@Cacheable 注解的使用倾向于减少方法的执行。

  • @CachePut

方法执行完毕之后,将返回值 update 或者 insert 到缓存中。

  • @CacheEvict

方法执行完毕之后,将缓存删除。

更多关于 Spring 框架缓存注解的说明可以参考官方文档:Spring Boot Caching

测试环境说明

  • SpringBoot版本:2.0.2.RELEASE

  org.springframework.boot
  spring-boot-starter-parent
  2.0.2.RELEASE
  

  • Redis 版本:3.2.9,pom 依赖:

  org.springframework.boot
  spring-boot-starter-data-redis

  • MySQL 版本:8.0.12,pom 依赖:


  org.springframework.boot
  spring-boot-starter-data-jpa



  org.springframework.boot
  spring-boot-starter-jdbc



  mysql
  mysql-connector-java
  8.0.12
  runtime

  • mock 数据 sql:
-- 测试数据库
CREATE DATABASE IF NOT EXISTS test;

-- 城市信息表
CREATE TABLE IF NOT EXISTS `test`.`city_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增 id',
  `city` varchar(128) NOT NULL DEFAULT '' COMMENT '城市名称',
  `longitude` bigint(20) NOT NULL DEFAULT '0' COMMENT '经度(100倍存储)',
  `latitude` bigint(20) NOT NULL DEFAULT '0' COMMENT '纬度(100倍存储)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='城市信息表';

-- fake 测试数据
INSERT INTO `city_info` (`id`, `city`, `longitude`, `latitude`)
VALUES
    (1, '合肥', 11717, 3152),
    (2, '安庆', 11702, 3031),
    (3, '宿州', 11658, 3338);

测试代码

代码中已经给出了详细的注释说明,且使用方法也比较简单,不做过多解释。

Redis 配置

/**
 * 

Redis 配置

* 继承 CachingConfigurerSupport 重写 CacheManager 和 KeyGenerator * Created by Qinyi. */ @EnableCaching // 启用缓存功能, 默认不启用 @Configuration public class RedisConfig extends CachingConfigurerSupport { private final RedisConnectionFactory redisConnectionFactory; @Autowired public RedisConfig(RedisConnectionFactory redisConnectionFactory) { this.redisConnectionFactory = redisConnectionFactory; } /** *

配置 CacheManager: 序列化方式、过期时间

* */ @Override public CacheManager cacheManager() { // 初始化一个 RedisCacheWriter // RedisCacheWriter 提供了对 Redis 的 set、setnx、get 等命令的访问权限 // 可以由多个缓存实现共享,并负责写/读来自 Redis 的二进制数据 RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); // 设置 CacheManager 的值序列化方式 RedisSerializer jsonSerializer = new JdkSerializationRedisSerializer(); RedisSerializationContext.SerializationPair pair = RedisSerializationContext.SerializationPair .fromSerializer(jsonSerializer); // 提供 Redis 的配置 RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(pair); // 设置默认超过期时间是 30 秒 defaultCacheConfig.entryTtl(Duration.ofSeconds(30)); // 初始化 RedisCacheManager 返回 return new RedisCacheManager(redisCacheWriter, defaultCacheConfig); } /** *

定义 key 生成器: 类名、方法名、参数列表

* 自定义 KeyGenerator 的核心思想是保证 key 不会冲突 * 默认的是 SimpleKeyGenerator, 它使用方法参数组合生成的一个 key, 这里存在一个问题: * 如果2个方法, 参数是一样的. 但执行逻辑不同, 那么将会导致执行第二个方法时命中第一个方法的缓存. 所以, 通常需要自定义. * */ @Override public KeyGenerator keyGenerator() { return (clazz, method, args) -> { StringBuilder sb = new StringBuilder(); sb.append(clazz.getClass().getName()).append("#"); sb.append(method.getName()).append("("); for (Object obj : args) { sb.append(obj.toString()).append(","); } sb.deleteCharAt(sb.length() - 1); sb.append(")"); return sb.toString(); }; } }

城市信息实体

/**
 * 

城市信息实体

* Created by Qinyi. */ @Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name = "city_info") public class CityInfo implements Serializable { /** 自增主键 */ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id", nullable = false) private Long id; /** 城市名称 */ @Basic @Column(name = "city", nullable = false) private String city; /** 经度 */ @Basic @Column(name = "longitude", nullable = false) private Long longitude; /** 纬度 */ @Basic @Column(name = "latitude", nullable = false) private Long latitude; }

城市信息服务接口定义

/**
 * 

城市信息服务接口定义

* Created by Qinyi. */ public interface ICityInfoService { /** *

根据 id 获取城市信息

* @param id 记录 id * @return {@link CityInfo} * */ CityInfo getCityInfoById(Long id); /** *

更新城市信息

* @param newObj 新的城市信息 * @return {@link CityInfo} * */ CityInfo updateCityInfo(CityInfo newObj); /** *

根据 id 删除城市信息

* @param id 记录 id * */ void deleteCityInfoById(Long id); }

城市信息服务接口定义

/**
 * 

城市信息服务接口定义

* Created by Qinyi. */ @Slf4j @Service public class CityInfoServiceImpl implements ICityInfoService { /** CityInfo Dao */ private final CityInfoRepository repository; @Autowired public CityInfoServiceImpl(CityInfoRepository repository) { this.repository = repository; } /** *

根据 id 获取城市信息

* 如果不指定 key, 则会使用 KeyGenerator 来生成 key * 1\. 如果指定了 key = "#id", redis 中的 key 是 city_info::1 * 2\. 如果使用自定义的 KeyGenerator(不指定 key), redis 中的 key 是: * city_info::com.imooc.ad.service.impl.CityInfoServiceImpl#getCityInfoById(1) * */ @Override @SuppressWarnings("all") @Cacheable(cacheNames = "city_info", key = "#id") // @Cacheable(cacheNames = "city_info") public CityInfo getCityInfoById(Long id) { log.info("get CityInfo by id: {}", id.toString()); return repository.findById(id).get(); } @Override @CachePut(cacheNames="city_info", key="#newObj.id") public CityInfo updateCityInfo(CityInfo newObj) { log.info("update CityInfo: {}", JSON.toJSONString(newObj)); return repository.save(newObj); } @Override @CacheEvict(cacheNames = "city_info", key = "#id") public void deleteCityInfoById(Long id) { log.info("delete CityInfo by id: {}", id.toString()); repository.deleteById(id); } }

Spring 声明式缓存测试用例

/**
 * 

Spring 声明式缓存测试用例

* Created by Qinyi. */ @RunWith(SpringRunner.class) @SpringBootTest(classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE) public class CacheAnnotationTest { @Autowired private ICityInfoService cityInfoService; /** *

测试多次获取, 可以直接从缓存(Redis)中获取数据, 而不用查询数据库

* */ @Test public void testGetCityInfoById() { System.out.println(JSON.toJSONString(cityInfoService.getCityInfoById(1L))); } /** *

测试更新缓存

* */ @Test public void testUpdateCityInfo() { System.out.println(JSON.toJSONString(cityInfoService.updateCityInfo( new CityInfo(1L, "合肥", 11717L, 3153L) ))); } /** *

测试删除缓存

* */ @Test public void testDeleteCityInfoById() { cityInfoService.deleteCityInfoById(1L); } }

执行测试用例之后,可以在 Redis 中看到自动生成的缓存 KV:

127.0.0.1:6379> keys *
1) "city_info::1"
2) "city_info::com.imooc.ad.service.impl.CityInfoServiceImpl#getCityInfoById(1)"
127.0.0.1:6379> type city_info::1
string
127.0.0.1:6379> get city_info::1
"\xac\xed\x00\x05sr\x00\x1ccom.imooc.ad.entity.CityInfo\xe2/O>\xd3\xe6\xee\xc8\x02\x00\x04L\x00\x04cityt\x00\x12Ljava/lang/String;L\x00\x02idt\x00\x10Ljava/lang/Long;L\x00\blatitudeq\x00~\x00\x02L\x00\tlongitudeq\x00~\x00\x02xpt\x00\x06\xe5\x90\x88\xe8\x82\xa5sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x00\x00\x00\x00\x01sq\x00~\x00\x05\x00\x00\x00\x00\x00\x00\x0cPsq\x00~\x00\x05\x00\x00\x00\x00\x00\x00-\xc5"
12

最后
在此我向大家推荐一个架构学习交流圈。点击加入交流圈 里面资深架构师会分享一些整理好的录制视频录像和BATJ面试题:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多。

SpringBoot 应用 Redis 声明式缓存_第1张图片

注:
作者:张勤一
出处:http://www.imooc.com/article/283946

你可能感兴趣的:(SpringBoot 应用 Redis 声明式缓存)