一、使用缓存
- 对缓存的常用操作描述
- 查询时,先读取缓存,如果缓存中没有数据,则触发真正的数据获取,如果缓存中有数据,直接返回缓存中的数据;
- 新增数据时,将数据写入缓存;
- 删除数据时,删除对应的缓存数据。并且可以自定义每个KEY的缓存有效期。
二、Spring Cache 提供缓存注解
-
@Cacheable
主要针对方法配置,能够根据方法的请求参数对其进行缓存 -
@CacheEvict
清空缓存 -
@CachePut
保证方法被调用,又希望结果被缓存与@Cacheable区别在于是否每次都调用方法,常用于更新
二、一个简单的缓存架构的实现(例子)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.lazyfennec</groupId>
<artifactId>custom-cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>custom-cache</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
1. Cacheable 注解
package cn.lazyfennec.customcache.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName Cacheable
* @Description 用于缓存读取
* @Author Neco
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {
String cacheName() default ""; // 缓存名称
String cacheKey(); // 缓存key
int expire() default 3600; // 有效时间(单位,秒),默认1个小时
int reflash() default -1; // 缓存主动刷新时间(单位,秒)
}
2.
package cn.lazyfennec.customcache.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName CacheEvict
* @Description 缓存清除
* @Author Neco
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheEvict {
String cacheName() default ""; //缓存名称
String cacheKey(); //缓存key
boolean allEntries() default false; //是否清空cacheName的全部数据
}
3. CachePut 注解
package cn.lazyfennec.customcache.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName CachePut
* @Description 缓存写入
* @Author Neco
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CachePut {
String cacheName() default ""; //缓存名称
String cacheKey(); //缓存key
int expire() default 3600; //有效期时间(单位:秒),默认1个小时
}
4. 缓存配置文件
package cn.lazyfennec.customcache.aop.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @ClassName CachePut
* @Description 缓存写入
* @Author Neco
* @Version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CachePut {
String cacheName() default ""; //缓存名称
String cacheKey(); //缓存key
int expire() default 3600; //有效期时间(单位:秒),默认1个小时
}
5. 默认缓存键生成器
package cn.lazyfennec.customcache.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
/**
* @Author: Neco
* @Description: key生成策略,缓存名+缓存KEY(支持Spring EL表达式)
* @Date: create in 2022/6/13 16:04
*/
@Component
public class DefaultKeyGenerator {
private static Logger logger = LoggerFactory.getLogger(DefaultKeyGenerator.class);
// 用于SpEL表达式解析
private SpelExpressionParser parser = new SpelExpressionParser();
// 用于获取方法参数定义名字
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* @Description cache key生成
* @Param cacheKey:key值必传,cacheNames:缓存名称,不传取方法路径
**/
public String generateKey(ProceedingJoinPoint pjp, String cacheName, String cacheKey) throws NoSuchMethodException {
if (StringUtils.isEmpty(cacheKey)) {
throw new NullPointerException("CacheKey can not be null...");
}
Signature signature = pjp.getSignature();
if (cacheName == null) {
cacheName = new String(signature.getDeclaringTypeName() + "." + signature.getName());
}
EvaluationContext evaluationContext = new StandardEvaluationContext();
if (!(signature instanceof MethodSignature)) {
throw new IllegalArgumentException("This annotation can only be used for methods...");
}
MethodSignature methodSignature = (MethodSignature) signature; //method参数列表
Method method = pjp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getMethod().getParameterTypes());
String[] parameterNames = nameDiscoverer.getParameterNames(method);
Object[] args = pjp.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
//解析cacheKey
String result = "CacheName_" + cacheName + "_CacheKey_" + parser.parseExpression(cacheKey).getValue(evaluationContext, String.class); //暂时只使用String类型
logger.info("=============>>> generateKeys : {}", result);
return result;
}
}
6. AOP
package cn.lazyfennec.customcache.aop;
import cn.lazyfennec.customcache.aop.annotation.CacheEvict;
import cn.lazyfennec.customcache.aop.annotation.CachePut;
import cn.lazyfennec.customcache.aop.annotation.Cacheable;
import com.github.benmanes.caffeine.cache.Cache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.SerializationUtils;
/**
* @Author: Neco
* @Description: 自定义缓存注解AOP实现
* @Date: create in 2022/6/13 15:47
*/
@Aspect
@Component
public class CacheableAspect {
private static Logger logger = LoggerFactory.getLogger(CacheableAspect.class);
@Autowired
private Cache caffeineCache;
@Autowired
private DefaultKeyGenerator defaultKeyGenerator;
/**
* 读取缓存数据
* 定义增强,pointcut连接点使用@annotaion(xxx)进行定义
* @param pjp
* @param cache
* @return
* @throws Throwable
*/
@Around(value = "@annotation(cache)") // cache 与 下面参数名around对应
public Object cache(final ProceedingJoinPoint pjp, Cacheable cache) throws Throwable {
try {
String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
Object valueData = null;
// 获取缓存中的值
Object value = caffeineCache.getIfPresent(key);
if (value != null) {
//如果缓存有值,需要判断刷新缓存设置和当前缓存的失效时间
if (cache.reflash() > 0) {
//查询当前缓存失效时间是否在主动刷新规则范围内
// caffeine 中刷新的话可以使用同步加载的方式调用reflash()
}
return value;
}
//缓存中没有值,执行实际数据查询方法
if (valueData == null) {
valueData = pjp.proceed(); //写入缓存
}
if (cache.expire() > 0) {
caffeineCache.put(key, valueData);
} else { //否则设置缓存时间 ,序列化存储
caffeineCache.put(key, valueData);
}
return valueData;
} catch (Exception e) {
logger.error("读取caffeine缓存失败,异常信息:" + e.getMessage());
return pjp.proceed();
}
}
/**
* 新增缓存
*/
@Around(value = "@annotation(cache)")
public Object cachePut(final ProceedingJoinPoint pjp, CachePut cache) throws Throwable {
try {
String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
Object valueData = pjp.proceed();
// 写入缓存
// 由于 caffeine 是内存缓存,对每个可以设置超时支持并不够好。除非每个 key 都构造一个 cache 对象;可以使用 redis 代替
if (cache.expire() > 0) {
caffeineCache.put(key, SerializationUtils.serialize(pjp.getArgs()[0]));
} else {
caffeineCache.put(key, SerializationUtils.serialize(pjp.getArgs()[0]));
}
return valueData;
} catch (Exception e) {
logger.error("写入caffeine缓存失败,异常信息:" + e.getMessage());
return pjp.proceed();
}
}
/**
* 删除缓存
*/
@Around(value = "@annotation(cache)")
public Object cacheEvict(final ProceedingJoinPoint pjp, CacheEvict cache) throws Throwable {
try {
String cacheName = cache.cacheName();
boolean allEntries = cache.allEntries();
if (allEntries) {
if (cacheName == null) {
Signature signature = pjp.getSignature();
cacheName = new String(signature.getDeclaringTypeName() + "." + signature.getName());
}
caffeineCache.invalidate("CacheName_" + cacheName);
} else {
String key = defaultKeyGenerator.generateKey(pjp, cache.cacheName(), cache.cacheKey());
caffeineCache.invalidate(key);
}
} catch (Exception e) {
logger.error("删除caffeine缓存失败,异常信息:" + e.getMessage());
}
return pjp.proceed();
}
}
7.模拟实现类
package cn.lazyfennec.customcache.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class Country implements Serializable {
/**
* 主键
*/
private Integer id;
/**
* 名称
*/
private String countryname;
/**
* 代码
*/
private String countrycode;
}
8. service相关
package cn.lazyfennec.customcache.service;
import cn.lazyfennec.customcache.entity.Country;
import java.util.List;
/**
* @Author: Neco
* @Description:
* @Date: create in 2022/6/14 13:04
*/
public interface ICountryService {
List<Country> listAll();
int save(Country country);
int update(Country country);
int deleteById(int id);
Country getById(int id);
}
package cn.lazyfennec.customcache.service.impl;
import cn.lazyfennec.customcache.aop.annotation.CacheEvict;
import cn.lazyfennec.customcache.aop.annotation.CachePut;
import cn.lazyfennec.customcache.aop.annotation.Cacheable;
import cn.lazyfennec.customcache.entity.Country;
import cn.lazyfennec.customcache.mapper.CountryMapper;
import cn.lazyfennec.customcache.service.ICountryService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author: Neco
* @Description:
* @Date: create in 2022/6/14 13:07
*/
@Service
public class CountryServiceImpl implements ICountryService {
@Resource
private CountryMapper countryMapper;
@Override
public List<Country> listAll() {
return countryMapper.listAll();
}
@Override
@CachePut(cacheName = "country_", cacheKey = "#country.id", expire = 3600)
public int save(Country country) {
return countryMapper.save(country);
}
@Override
@CacheEvict(cacheName = "country_", cacheKey = "#id")
public int update(Country country) {
return countryMapper.update(country);
}
@Override
@CacheEvict(cacheName = "country_", cacheKey = "#id")
public int deleteById(int id) {
return countryMapper.deleteById(id);
}
@Override
@Cacheable(cacheName = "country_", cacheKey = "#id", expire = 3600)
public Country getById(int id) {
return countryMapper.getById(id);
}
}
9. Mapper
package cn.lazyfennec.customcache.mapper;
import cn.lazyfennec.customcache.entity.Country;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* @Author: Neco
* @Description:
* @Date: create in 2022/6/14 13:07
*/
@Mapper
public interface CountryMapper {
@Select("select * from country")
List<Country> listAll();
@Insert("insert into country (id, countryname, countrycode) values(#{country.id}, #{country.countryname}, #{country.countrycode})")
int save(@Param("country") Country country);
@Update("update country set countryname = #{country.countryname} and countrycode = #{country.countrycode} where id=#{country.id}")
int update(@Param("country") Country country);
@Delete("delete from country where id=#{country.id}")
int deleteById(int id);
@Select("select * from country where id = #{id}")
Country getById(int id);
}
10. Controller
package cn.lazyfennec.customcache.controller;
import cn.lazyfennec.customcache.entity.Country;
import cn.lazyfennec.customcache.entity.vo.Result;
import cn.lazyfennec.customcache.service.ICountryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @Author: Neco
* @Description:
* @Date: create in 2022/6/14 11:07
*/
@RestController
@RequestMapping("/cache")
public class CacheController {
@Autowired
private ICountryService countryService;
@GetMapping("/country/list")
public Result list() {
List<Country> list = countryService.listAll();
return Result.OK(list);
}
@GetMapping("/country/{id}")
public Result getById(@PathVariable Integer id) {
Country country = countryService.getById(id);
return Result.OK(country);
}
@DeleteMapping("/country/{id}")
public Result deleteById(@PathVariable Integer id) {
countryService.deleteById(id);
return Result.OK("删除成功!");
}
@PostMapping("/country/save")
public Result save(@RequestBody Country country) {
countryService.save(country);
return Result.OK("保存成功!");
}
@PutMapping("/country/update")
public Result update(@RequestBody Country country) {
countryService.update(country);
return Result.OK("更新成功!");
}
}
测试
可以得知,一定时间内,多次访问 http://localhost:8080/cache/country/1 这样的地址,后边的内容会从缓存中取相关的数据。
如果觉得有收获就点个赞吧,更多知识,请点击关注查看我的主页信息哦~
网友评论