前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果:
耗时比较大的往往有两个地方:
1、查数据库;
2、调用其它服务的API(因为其它服务最终也要去做查数据库等耗时操作);
重复查询也有两种:
1、我们在应用程序中代码写得不好,写的for循环,可能每次循环都用重复的参数去查询了。
2、大量的相同或相似请求造成的。比如资讯网站首页的文章列表、电商网站首页的商品列表、微博等社交媒体热搜的文章等等,当大量的用户都去请求同样的接口,同样的数据,如果每次都去查数据库,那对数据库来说是一个不可承受的压力。所以我们通常会把高频的查询进行缓存,我们称它为“热点”。
使用Spring Cache的好处:
Spring Cache就是一个缓存框架。它利用了AOP(将缓存逻辑与服务逻辑解耦),实现了基于注解的缓存功能(声明式缓存),并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。而且Spring Cache也提供了很多默认的配置,用户可以快速将缓存集成到项目中;
我这里底层的缓存选择的是redis,所以redis的缓存也要导入进来
#端口
server:
port: 8888
#数据库配置
spring:
redis:
host: 192.168.230.100 # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
# password: ld123456 # Redis服务器连接密码(默认为空)
datasource:
url: jdbc:mysql://192.168.230.100:3306/tmp_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
username: root
password: 1234
driver-class-name: com.mysql.jdbc.Driver
#打印日志
logging:
level:
com.donleo.cache.mapper: debug
mybatis:
mapper-locations: classpath:mappers/*.xml
type-aliases-package: com.itheima.cache.model
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
第三步.缓存配置类定义
SpringCache抽象出公共的缓存接口,同时面向用户屏蔽了底层实现细节,用户可通过配置缓存管理器来实现缓存方案的替换:
当前以Redis作为SpringCache缓存底层实现为例
/**
* @author hhh
* code 自定义redis序列化配置类
*/
@Configuration
//开启Springcaching的支持,底层自动识别相关springCache的注解
@EnableCaching
public class RedisCacheConfig {
/**
* 配置 cacheManager 代替默认的cacheManager (缓存管理器)
* 当前使用的redis缓存做为底层实现,如果将来想替换缓存方案,那么只需调整CacheManager的实现细节即可
* 其他代码无需改动
* @param factory RedisConnectionFactory
* @return CacheManager
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
//定义redis数据序列化的对象
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
//jackson序列化方式对象
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//设置被序列化的对象的属性都可访问:暴力反射
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//仅仅序列化对象的属性,且属性不可为final修饰
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(objectMapper);
// 配置key value序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
//关闭控制存储--》禁止缓存value为null的数据
.disableCachingNullValues()
//修改前缀与key的间隔符号,默认是:: eg:name:findById
.computePrefixWith(cacheName->cacheName+":");
//设置特有的Redis配置
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
//定制化的Cache 设置过期时间 eg:以role:开头的缓存存活时间为10s
// cacheConfigurations.put("role",customRedisCacheConfiguration(config,Duration.ofSeconds(20)));
cacheConfigurations.put("stock",customRedisCacheConfiguration(config,Duration.ofSeconds(3000)));
cacheConfigurations.put("market",customRedisCacheConfiguration(config,Duration.ofSeconds(300)));
//设置一role开头的缓存存活周期为30s
cacheConfigurations.put("role",customRedisCacheConfiguration(config,Duration.ofSeconds(30)));
//构建redis缓存管理器
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
//Cache事务支持,保证reids下的缓存与数据库下的数据一致性
.transactionAware()
.withInitialCacheConfigurations(cacheConfigurations)
.cacheDefaults(config)
.build();
//设置过期时间
return cacheManager;
}
/**
* 设置RedisConfiguration配置
* @param config
* @param ttl
* @return
*/
public RedisCacheConfiguration customRedisCacheConfiguration(RedisCacheConfiguration config, Duration ttl) {
//设置缓存缺省超时时间
return config.entryTtl(ttl);
}
}
==如果缓存中没有:查询数据库,存储缓存,返回结果,==
==如果缓存中有:直接返回结果==
作用:可以用来进行缓存的写入,将结果存储在缓存中,以便于在后续调用的时候可以直接返回缓存中的值,而不必再执行实际的方法。 最简单的使用方式,注解名称=缓存名称,使用例子如下:
在一个方法上使用注解@Cacheable
属性:cacheNames(value)->存入redis缓存中的key值的前缀,
key->指定要存入redis缓存中的key,使用#id表示引用方法参数的id的值作为key值
value为该方法的返回值
@Cacheable(cacheNames = "role",key = "#id")
@Override
public Role findById(Integer id) {
return roleMapper.selectByPrimaryKey(id);
}
我们会发现如果在每个方法的方面都加上cacheNames来表示key值的前缀十分冗余,所以我们可以在类上使用@CacheConfig(cacheNames = "role")//提取缓存的前缀配置
这样一来这个类下的每个方法存入redis时都会有role前缀
@Service
@CacheConfig(cacheNames = "role")
public class RoleServiceImpl implements IRoleService {
@Autowired
private RoleMapper roleMapper;
@Cacheable(cacheNames = "role",key = "#id")
@Override
public Role findById(Integer id) {
return roleMapper.selectByPrimaryKey(id);
}
}
第一遍会去数据库中加载数据,并存入redis缓存中
@Test
public void test3(){
Role role = roleService.findById(8);
System.out.println(role);
}
删除数据库数据的同时,还对缓存的数据进行删除
@CacheEvict:删除缓存的注解,这对删除旧的数据和无用的数据是非常有用的。这里还多了一个参数(allEntries),设置allEntries=true时,可以对整个条目进行批量删除
@Override
@CacheEvict(key="#id")//根据id为key去redis缓存中删除数据
public Integer delete(Integer id) {
return roleMapper.deleteByPrimaryKey(id);
}
@CachePut:当需要更新缓存而不干扰方法的运行时 ,可以使用该注解。也就是说,始终执行该方法,并将结果放入缓存
本质上说,如果存在对应的缓存,则更新覆盖(先删除原来key相同的缓存,再添加),不存在则添加;
使用
@Override
@CachePut(key="#role.id") //使用对象的成员变量id作为key值
public Role update(Role role) {
roleMapper.updateByPrimaryKey(role);
return role;
}
会使用id=8为key值,然后然后类上使用的注解@CacheConfig(cacheNames="role")role前缀
@Test
public void testUpdate(){
Role role = Role.builder().id(8).rolecode("080").rolename("008Role").introduce("008Introduce").build();
Role update = roleService.update(role);
System.out.println(role);
}
在使用缓存的时候,有可能会同时进行更新和删除,会出现同时使用多个注解的情况.而@Caching可以实现,对于复杂的缓存策略,我们可借助SpEL实现;
使用
//执行这个添加方法的时候,向redis中添加三个key,value为返回值,并删除key为8的值
@Caching(
cacheable = @Cacheable(key="#role.rolename"),
put = {@CachePut(key="#role.id"),@CachePut(key="#role.rolecode")},
evict = @CacheEvict(key="8")
)
@Override
public R add(Role role) {
try {
roleMapper.insert(role);
} catch (Exception e) {
return R.error();
}
return R.ok(role.getId());
}
@Test
public void testCaching(){
Role role = Role.builder().id(15).rolecode("015").rolename("015Role").introduce("015Introduce").build();
roleService.add(role);
}
对于缓存声明,spring的缓存提供了一组java注解:
选择Face的理由:
<!--不要将缓存放在中间common层,因为如果引用common的第三方不适用缓存,会导致因为场景依赖自动装配的机制导致启动失败-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--引入redis的starter依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis创建连接池,默认不会创建连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
spring:
# 配置缓存
redis:
host: 192.168.230.100
port: 6379
database: 0 #Redis数据库索引(默认为0)
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 1 # 连接池中的最小空闲连接
timeout: PT10S # 连接超时时间
package com.hhh.stock.config;
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 com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/**
* @author hhh
* code 自定义redis序列化配置类
*/
@Configuration
//开启Springcaching的支持,底层自动识别相关springCache的注解
@EnableCaching
public class CacheConfig {
/**
* 配置 cacheManager 代替默认的cacheManager (缓存管理器)
* 当前使用的redis缓存做为底层实现,如果将来想替换缓存方案,那么只需调整CacheManager的实现细节即可
* 其他代码无需改动
* @param factory RedisConnectionFactory
* @return CacheManager
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
//定义redis数据序列化的对象
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
//jackson序列化方式对象
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//设置被序列化的对象的属性都可访问:暴力反射
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//仅仅序列化对象的属性,且属性不可为final修饰
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(objectMapper);
// 配置key value序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
//关闭控制存储--》禁止缓存value为null的数据
.disableCachingNullValues()
//修改前缀与key的间隔符号,默认是:: eg:name:findById
.computePrefixWith(cacheName->cacheName+":");
//设置特有的Redis配置
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
//定制化的Cache 设置过期时间 eg:以role:开头的缓存存活时间为10s
//cacheConfigurations.put("role",customRedisCacheConfiguration(config,Duration.ofSeconds(20)));
cacheConfigurations.put("stock",customRedisCacheConfiguration(config,Duration.ofSeconds(3000)));
cacheConfigurations.put("market",customRedisCacheConfiguration(config,Duration.ofSeconds(300)));
//设置一role开头的缓存存活周期为30s
//cacheConfigurations.put("role",customRedisCacheConfiguration(config,Duration.ofSeconds(300)));
//构建redis缓存管理器
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
//Cache事务支持,保证reids下的缓存与数据库下的数据一致性
.transactionAware()
.withInitialCacheConfigurations(cacheConfigurations)
.cacheDefaults(config)
.build();
//设置过期时间
return cacheManager;
}
/**
* 设置RedisConfiguration配置
* @param config
* @param ttl
* @return
*/
public RedisCacheConfiguration customRedisCacheConfiguration(RedisCacheConfiguration config, Duration ttl) {
//设置缓存缺省超时时间
return config.entryTtl(ttl);
}
}
向数据库查到数据的同时并存入缓存中,下一次查询时,如果缓存中有对应的key时,直接获取缓存中的数据
@Component("stockCacheFace")
public class StockCacheFaceImpl implements StockCacheFace {
@Autowired
private StockBusinessMapper stockBusinessMapper;
/**
* 获取所有股票编码,并添加上证或者深证的股票前缀编号:sh sz
*/
@Override
@Cacheable(cacheNames = "stock",key = "'stockCodes'")//常量要使用单引号,不然会报错
public List<String> getAllStockCodeWithPredix() {
//获取所有的A股编码信息
List<String>allCodes=stockBusinessMapper.getAllStockCode();
//http://hq.sinajs.cn/list=sh601003,sh601001,sz000019
//TODO:给取出的编码加上前缀,6开头加sh,0开头加sz
allCodes = allCodes.stream().map(code -> code.startsWith("6") ? "sh" + code : "sz" + code).collect(Collectors.toList());
return allCodes;
}
}
@Autowired
private StockCacheFace stockCacheFace;
@Override
public void getStockRtInfo() {
//获取所有的A股编码信息
/* List<String>allCodes=stockBusinessMapper.getAllStockCode();
//http://hq.sinajs.cn/list=sh601003,sh601001,sz000019
//TODO:给取出的编码加上前缀,6开头加sh,0开头加sz
allCodes = allCodes.stream().map(code -> code.startsWith("6") ? "sh" + code : "sz" + code).collect(Collectors.toList());*/
List<String>allCodes=stockCacheFace.getAllStockCodeWithPredix();
}
成功存入缓存