这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
一、Cache缓存的作用
随着时间的积累,应用的使用用户不断增加,数据规模也越来越大,往往数据库查询操作会成为用户使用体验的瓶颈,此时使用缓存往往是解决这一问题非常好的手段之一,Spring3开始提供了强大的基于注解的缓存支持,可以通过注解方式低侵入的给原有的Sping应用增加缓存功能,提高数据访问性能,Springboot为缓存提供了一系列的自动化的配置,使我们可以非常方便的使用缓存。
1,主要的核心类
Java Caching定义了5个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry.
CachingProvider定义了创建、配置、获取、管理和控制多个CacheManager.一个应用可以在运行期访问多个CachingProvider.
CacheManager定义了创建、配置、获取、管理和控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中。一个CacheManager仅被一个CacheProvider所拥有。
Cache是一个类似Map的数据结构并临时存储以Key为索引的值,一个Cache仅被一个CacheManager所拥有。
Entry是一个存储在Cache中的key-value对。
Expiry每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个时间,条目为过期的状态,一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。
2,Spring缓存对象
Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache(JSR-107)注解来简化我们的开发。
Cache接口为缓存的组件规范定义,包含缓存的各种的各种操作集合。
Cache接口下Spring提供了各种xxxCache的实现,如RedisCache,EhCacheCache,ConcurentMapCache.
每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过,如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户,下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
1,确定方法需要被缓存以及他们的缓存策略
2,从缓存中读取之前缓存存储的数据
二、几个重要概念&缓存注解
| 概念&注解 | 作用 |
|---|---|
| Cache | 缓存接口,定义缓存操作,实现有RedisCache,EhCacheCache,ConcurrentMapCache等 |
| CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
| @Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
| @CacheEvict | 清空缓存 |
| @CachePut | 保证方法被调用,又希望结果被缓存 |
| @EnableCaching | 开启基于注解的缓存 |
| keyGenerator | 缓存数据时key生成策略 |
| serialize | 缓存数据时value序列化策略 |
Cache SpEL available metadata
| 名字 | 位置 | 描述 | 实例 |
|---|---|---|---|
| methodName | root object | 当前被调用的方法名 | #root,methodName |
| method | root object | 当前被调用的方法 | root method name |
| target | root object | 当前被调用的目标对象 | root target |
| targetClass | root object | 当前被调用的目标对象类 | root targetClass |
| args | root object | 当前被调用的方法的参数列表 | root args[0] |
| caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={"cache1","cache2"})),则有两个cacho | root caches[0]name |
| argument name | evaluation context | 方法参数的名字,可以直接#参数名,也可以使用#p0或#a0的形式,0代表参数的索引 | #a0,#P0 |
| result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如'unless','cache put'的表达式'cache evict'的表达式beforeInvocation=false) | result |
三、Springboot中Cache缓存的使用,将缓存存到Redis中
首先项目目录:
首先加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
再主启动类上加入配置开启缓存功能
package com.sense;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@MapperScan(basePackages = "com.sense.dao")
@EnableCaching
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
创建实体类
package com.sense.entity;
import lombok.Data;
/**
@desc ...
@date 2021-07-20 14:37:35
@author
*/
@Data
public class User {
private int id;
private String username;
private int password;
}
创建dao
package com.sense.dao;
import com.sense.entity.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
@desc ...
@date 2021-08-02 10:42:33
@author
*/
@Mapper
public interface UserDao {
List<User> getUser();
User getUserById(Integer id);
void update(User user);
void deleteUser(Integer id);
User queryByName(String name);
}
创建service
package com.sense.service;
import com.sense.entity.User;
import java.util.List;
/**
@desc ...
@date 2021-08-02 10:37:24
@author
*/
public interface UserService {
List<User> getUser();
User getUserById(Integer id);
void update(User user);
void deleteUser(Integer id);
User queryByName(String name);
}
创建impl
package com.sense.service.impl;
import com.sense.dao.UserDao;
import com.sense.entity.User;
import com.sense.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import java.util.List;
/**
@desc ...
@date 2021-08-02 10:40:48
@author
*/
@Service
@Slf4j
@CacheConfig(cacheNames = "user") //将cacheNames抽取出来
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public List<User> getUser() {
return userDao.getUser();
}
/**
* @desc @Cacheable的几个属性详解:
* cacheNames/value:指定相关组件的名字
* key: 缓存数据使用的key,可以用它来指定。默认使用方法参数的值,一般不需要指定
* keyGenerator:作用和key一样,二选一
* cacheManager和cacheResolver作用相同,指定缓存管理器,二选一
* condition: 指定符合条件才缓存,比如: condition="#id>3"
* 也就是说传入的参数id>3才缓存数据
* unless: 否定缓存,当unless为true时不缓存,可以获取的方法结果进行判断
* sync: 是否使用异步模式
* @param
* @return java.util.List<com.sense.dingtalkdemo.entity.User>
*
* @date 2021/7/30 13:36
*/
@Override
@Cacheable(cacheNames = "user",key = "#id")
public User getUserById(Integer id) {
log.info("得到"+id+"的信息");
return userDao.getUserById(id);
}
/**
* @desc @CachePut必须结合@Cacheable一起使用,否则没什么意义
* @CachePut: 即调用方法,又更新缓存数据
* 修改了数据库中的数据,同时又更新了缓存
* 运行时机:
* 1,先调用目标方法,
* 2,将目标方法返回一的结果缓存起来
* 测试步骤:
* 1.查询1号的个人信息
* 2,以后查询还是之前的结果
* 3,更新1号的个人信息
* 4,查询1号员工返回的结果是什么
* 应该是更新后的员工
* 但只更新了数据库,但没有更新缓存是什么原因?
* 5,如何解决缓存和数据库同步更新
* @CachePut(cacheNames="user",key="#user.id")
* @CachePut(cacheNames="user",key="#result.id")
* @param user 1
* @return com.sense.dingtalkdemo.entity.User
*
* @date 2021/7/30 13:47
*/
@Override
@CachePut(cacheNames = "user",key = "#result.id")
public void update(User user) {
System.out.println("修改"+user.getId()+"的信息");
userDao.update(user);
}
/**
* @desc @CacheEvict:清除缓存
* 1.key:指定要清除缓存中的某条数据
* 2.allEntries=true:删除缓存中的所有数据
* beforeInvocation=true;在方法执行之前执行清除缓存
* 作用是:只清除缓存,不删除数据库数据
* @param id 1
* @return void
*
* @date 2021/7/30 14:10
*/
@Override
// @CacheEvict(cacheNames = "user",key = "#id")
@CacheEvict(cacheNames = "user",allEntries = true)
public void deleteUser(Integer id){
System.out.println("删除"+id+"号信息");
//删除数据库数据的同时删除缓存信息
userDao.deleteUser(id);
/**
* @desc beforeInvocation=true
* 使用在方法之前的好处:
* 如果方法出现异常,缓存依旧会被删除
*/
//int a=1/0;
}
/**
* @desc 注解的含义
* 1.当使用指定的名字查询数据库后,数据保存到缓存
* 2.现在使用id,age就会直接查询缓存,而不是查询数据库
* @param name 1
* @return com.sense.dingtalkdemo.entity.User
*
* @date 2021/7/30 14:24
*/
@Override
@Caching(cacheable = {@Cacheable(value = "user",key = "#name")},
put = { @CachePut(value = "user",key = "#result.id"),
@CachePut(value = "user",key = "#result.password")})
public User queryByName(String name) {
System.out.println("查询的姓名"+name);
return userDao.queryByName(name);
}
}
创建config
package com.sense.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
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.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
@desc ...
@date 2021-08-02 09:39:44
@author
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
/**
* @desc 配置自定义的redisTemplate
* @param connectionFactory 1
* @return org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object>
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setValueSerializer(jackson2JsonRedisSerializer());
//使用StringRedisSerializer来序列化和反序列化redis中key值
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
/**
* @desc json序列化
* @param
* @return org.springframework.data.redis.serializer.RedisSerializer<?>
*
* @date 2021/8/2 9:51
*/
@Bean
public RedisSerializer<?> jackson2JsonRedisSerializer() {
//使用jackson2JsonRedisSerializer来序列化和反序列化Redis的value值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
return serializer;
}
/**
* @desc 配置缓存管理器
* @param redisConnectionFactory 1
* @return org.springframework.cache.CacheManager
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
//生成一个默认配置,通过config对象即可对缓存进行自定义配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置缓存的默认过期时间,也是对Duration设置
config = config.entryTtl(Duration.ofMinutes(1))
//设置key为string序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
//设置value为json序列化
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer()))
//不缓存空值
.disableCachingNullValues();
//设置一个初始化的缓存空间set集合
Set<String> cacheNames = new HashSet<>();
cacheNames.add("timeGroup");
cacheNames.add("user");
//对每个缓存空间应用不同的配置
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
configMap.put("timeGroup", config);
configMap.put("user", config.entryTtl(Duration.ofSeconds(120)));
//使用自定义的缓存配置初始化一个cacheManager
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
//一定要先调用该方法设置初始化的缓存名,再初始化相关的配置
.initialCacheNames(cacheNames)
.withInitialCacheConfigurations(configMap)
.build();
return cacheManager;
}
/**
* @desc 缓存的key是包名+方法名+参数列表
* @return org.springframework.cache.interceptor.KeyGenerator
* @date 2021/8/2 10:13
*/
@Override
@Bean
public KeyGenerator keyGenerator() {
return (target, method, objects) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append("::" + method.getName() + ":");
for (Object obj : objects) {
sb.append(obj.toString());
}
return sb.toString();
};
}
}
创建xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sense.dao.UserDao">
<select id="getUser" resultType="com.sense.entity.User">
select *
from user
</select>
<select id="getUserById" resultType="com.sense.entity.User">
select *
from user
where id = #{id}
</select>
<update id="update" parameterType="com.sense.entity.User">
update user
set username=#{username},
password=#{password}
where id = #{id}
</update>
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>
<select id="queryByName" resultType="com.sense.entity.User">
select *
from user
where username = #{username}
</select>
</mapper>
引入Ehcache
关于Ehcahe的一些说明:
- name:缓存名称。
- maxElementsInMemory:缓存最大数目
- maxElementsOnDisk:硬盘最大缓存个数。
- eternal:对象是否永久有效,一但设置了,timeout将不起作用。
- overflowToDisk:是否保存到磁盘。
- timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当
eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 - timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当
eternal=false对象不是永久有效时使用,默认是0,也就是对象存活时间无穷大。 - diskPersistent:是否缓存虚拟机重启期数据,默认值为false。
- diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
- diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
- memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
- clearOnFlush:内存数量最大时是否清除。
- memoryStoreEvictionPolicy:Ehcache的三种清空策略:FIFO,first in first out,这个是大家最熟的,先进先出。LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
导入依赖
<!-- ehcache -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
创建xml文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120" />
<cache
name="user"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true"/>
</ehcache>
yml配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
redis:
host: 127.0.0.1
port: 6379
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
cache:
ehcache:
config: ehcache.xml
对于Ehcache来说,更新方法加不加@CachePut注解,结果都一样。
redis中存的数据格式如下