赞
踩
Java Caching
定义了5个核心接口,分别是CachingProvider
, CacheManager
, Cache
, Entry
和 Expiry
。
Spring从3.1开始定义了org.springframework.cache.Cache 和org.springframework.cache.CacheManager接口来统一不同的缓存技术; 并支持使用JCache(JSR-107)注解简化我们开发;
开启缓存功能,一般放在启动类上。
当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"cacheName"})
注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。
根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
sync | 是否使用异步模式,默认为false |
使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
unless | 条件符合则不缓存 |
使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上
查看源码,属性值如下:
属性/方法名 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
keyGenerator | key的生成器。key/keyGenerator二选一使用 |
cacheManager | 指定缓存管理器 |
cacheResolver | 指定获取解析器 |
condition | 条件符合则缓存 |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
该注解可以实现同一个方法上同时使用多种注解。可从其源码看出
@Caching(
cacheable = {
@Cacheable(value="emp",key = "#lastName")
},
put = {
@CachePut(value="emp",key = "#result.id"),
@CachePut(value="emp",key = "#result.email")
}
)
public Employee getEmpByLastName(String lastName) {
return employeeMapper.getEmpByLastName(lastName);
}
Spring 支持的常用 CacheManager 如下:
CacheManager | 描述 |
---|---|
SimpleCacheManager | 使用简单的 Collection 来存储缓存 |
ConcurrentMapCacheManager | 使用 java.util.ConcurrentHashMap 来实现缓存 |
NoOpCacheManager | 仅测试用,不会实际存储缓存 |
EhCacheCacheManger | 使用EhCache作为缓存技术。EhCache 是一个纯 Java 的进程内缓存框架,特点快速、精干,是 Hibernate 中默认的 CacheProvider,也是 Java 领域应用最为广泛的缓存 |
JCacheCacheManager | 支持JCache(JSR-107)标准的实现作为缓存技术 |
CaffeineCacheManager | 使用 Caffeine 作为缓存技术。用于取代 Guava 缓存技术。 |
RedisCacheManager | 使用Redis作为缓存技术 |
HazelcastCacheManager | 使用Hazelcast作为缓存技术 |
CompositeCacheManager | 用于组合 CacheManager,可以从多个 CacheManager 中轮询得到相应的缓存 |
为什么要用本地缓存
1、 使用ConcurrentHashMap实现本地缓存
缓存的本质就是存储在内存中的KV数据结构,对应的就是jdk中线程安全的ConcurrentHashMap,但是要实现缓存,还需要考虑淘汰、最大限制、缓存过期时间淘汰等等功能;
优点是实现简单,不需要引入第三方包,比较适合一些简单的业务场景。缺点是如果需要更多的特性,需要定制化开发,成本会比较高,并且稳定性和可靠性也难以保障。对于比较复杂的场景,建议使用比较稳定的开源工具。
2、基于Guava Cache实现本地缓存
Guava是Google团队开源的一款 Java 核心增强库,包含集合、并发原语、缓存、IO、反射等工具箱,性能和稳定性上都有保障,应用十分广泛。Guava Cache支持很多特性:
3、Caffeine
Caffeine是基于java8实现的新一代缓存工具,缓存性能接近理论最优。可以看作是Guava Cache的增强版,功能上两者类似,不同的是Caffeine采用了一种结合LRU、LFU优点的算法:W-TinyLFU,在性能上有明显的优越性
4、 Ehcache
Ehcache是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。同Caffeine和Guava Cache相比,Ehcache的功能更加丰富,扩展性更强:
Google Guava Cache是一种非常优秀本地缓存解决方案,提供了基于容量,时间和引用的缓存回收方式。基于容量的方式内部实现采用LRU算法,基于引用回收很好的利用了Java虚拟机的垃圾回收机制。其中的缓存构造器CacheBuilder采用构建者模式提供了设置好各种参数的缓存对象,缓存核心类LocalCache里面的内部类Segment与jdk1.7及以前的ConcurrentHashMap非常相似,都继承于ReetrantLock,还有六个队列,以实现丰富的本地缓存方案。 通俗的讲,Guva是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库。cache只是其中的一个模块。使用Guva cache能够方便快速的构建本地缓存。
Caffeine是使用Java8对Guava缓存的重写版本,在Spring Boot 2.0中将取代Guava。如果出现Caffeine,CaffeineCacheManager将会自动配置。
代码实现: 基于SpringBoot(2.3.2.RELEASE)+mybatis-plus(3.4.3)实现
CREATE TABLE `tb_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(100) DEFAULT NULL COMMENT '姓名',
`phone` varchar(15) DEFAULT NULL COMMENT '手机号',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`salary` int DEFAULT NULL,
`hire_date` datetime DEFAULT NULL COMMENT '入职日期',
`dept_id` bigint DEFAULT NULL COMMENT '部门编号',
`birthday` datetime DEFAULT NULL COMMENT '出生日期',
`photo` varchar(200) DEFAULT NULL COMMENT '照片路径',
`address` varchar(300) DEFAULT NULL COMMENT '现在住址',
PRIMARY KEY (`id`),
KEY `fk_dept` (`dept_id`)
) ENGINE=InnoDB AUTO_INCREMENT=61 DEFAULT CHARSET=utf8mb3;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
# DataSource Config
spring:
datasource:
# 数据源基本配置
url: jdbc:mysql://localhost:3306/study_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
initialization-mode: always #表示始终都要执行初始化,2.x以上版本需要加上这行配置
type: com.alibaba.druid.pool.DruidDataSource
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# Logger Config
logging:
level:
cn.zysheep.mapper: debug
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager(){
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
//Caffeine配置
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
//最后一次写入后经过固定时间过期
.expireAfterWrite(10, TimeUnit.SECONDS)
//maximumSize=[long]: 缓存的最大条数
.maximumSize(1000);
cacheManager.setCaffeine(caffeine);
return cacheManager;
}
}
Caffeine配置说明:
1、User 实体
@TableName(value ="tb_user")
@Data
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String userName;
private String phone;
private String province;
private String city;
private Integer salary;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date hireDate;
private Long deptId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date birthday;
private String photo;
private String address;
}
2、UserMapper
public interface UserMapper extends BaseMapper<User> {
}
3、UserService
public interface UserService extends IService<User> {
List<User> getUserByName(String name);
}
4、UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
}
5、UserController
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
}
6、启动类
@MapperScan("cn.zysheep.mapper")
@SpringBootApplication
public class CaffeineApplication {
public static void main(String[] args) {
SpringApplication.run(CaffeineApplication.class, args);
}
}
@GetMapping("getByNameNoCache")
public ResponseEntity getByNameNoCache(@RequestParam String username) {
List<User> users = userService.getUserByNameNoCache(username);
return ResponseEntity.ok(users);
}
public List<User> getUserByNameNoCache(String userName) {
LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(userName), User::getUserName, userName);
List<User> users = list(queryWrapper);
log.info("从数据库中读取,而非从缓存读取!");
log.info("users: {}", users);
return users;
}
不使用缓存每次都会查询数据库
idea会识别缓存注解,显示缓存标识
@Cacheble
注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上。@CacheEvict
注解的方法,会清空指定缓存。一般用在更新或者删除的方法上。@CachePut
注解的方法,保证方法被调用,又希望结果被缓存。会把方法的返回值put到缓存里面缓存起来。它通常用在新增方法上。@Cacheable标注的方法执行之前先来检查缓存中有没有这个数据,默认按照参数的值作为key去查询缓存, 如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。
当我们在声明 @Cacheable 时不指定 key 参数,则该缓存名下的所有 key 会使用 KeyGenerator 根据参数 自动生成。spring 有一个默认的 SimpleKeyGenerator ,在 spring boot 自动化配置中,这个会被默认注入。生成规则如下:
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
#指定缓存组件的名字;将方法的返回结果放在哪个缓存中,是数组的方式,可以指定多个缓存;
String key() default "";
# 缓存数据使用的key;可以用它来指定。默认是使用方法参数的值.如(1-方法返回值)
编写SpEL; #id;参数id的值 #a0 #p0 #root.args[0]
String keyGenerator() default "";
# key的生成器;可以自己指定key的生成器的组件id,key/keyGenerator:二选一使用;
String cacheManager() default "";
# 指定缓存管理器;或者cacheResolver指定获取解析器
String cacheResolver() default "";
# 缓存解析器
String condition() default "";
# 指定符合条件的情况下才缓存;condition = "#id>0" condition = "#a0>1":第一个参数的值>1的时候才进行缓存
String unless() default ""; # 否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断
boolean sync() default false; # 是否使用异步模式
}
Spring还为我们提供了一个root对象可以用来生成key。通过该root对象我们可以获取到以下信息。属性key用SpEL的写法
名字 | 位置 | 描述 | 示例 |
---|---|---|---|
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”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; | #username 、 #a0 、 #p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) | #result |
当我们要使用root对象的属性作为key时我们也可以将“#root
”省略,因为Spring默认使用的就是root对象的属性。
1、基本形式
@Cacheable(cacheNames = "user", key = "#id")
public List<User> getUserById(Long id) {
LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(Objects.nonNull(id), User::getId, id);
List<User> users = list(queryWrapper);
log.info("从数据库中读取,而非从缓存读取!");
log.info("users: {}", users);
return users;
}
第一次查库,后面相同参数的查询直接返回缓存数据,10s缓存过期,再次查询,会查库,然后缓存返回值数据
2、组合形式
// spEL使用"T(Type)"来表示 java.lang.Class 实例,"Type"必须是类全限定名,"java.lang"包除外。
@Cacheable(cacheNames = "user", key = "T(String).valueOf(#id).concat('::').concat(#userName)")
public List<User> getUserByIdAndName(Long id, String userName) {
LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery()
.like(StringUtils.isNotBlank(userName), User::getUserName, userName)
.eq(Objects.isNull(id), User::getId, id);
List<User> users = list(queryWrapper);
System.out.println("从数据库中读取,而非从缓存读取!");
return users;
3、对象形式
@Cacheable(cacheNames = "user", key = "#user.userName")
public List<User> getUser(User user) {
LambdaQueryWrapper<User> queryWrapper = Wrappers.<User>lambdaQuery().like(StringUtils.isNotBlank(user.getUserName()), User::getUserName, user.getUserName());
List<User> users = list(queryWrapper);
System.out.println("从数据库中读取,而非从缓存读取!");
return users;
}
4、自定义key生成策略
@Component
public class CacheKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
ObjectMapper JSON = new ObjectMapper();
String key = null;
try {
key = target.getClass().getSimpleName() + "#" + method.getName() + "(" + JSON.writeValueAsString(params) + ")";
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return key;
}
}
注意:官方说 key 和 keyGenerator 参数是互斥的,同时指定两个会导致异常。
1、自动配置类;CacheAutoConfiguration
2、缓存的配置类
3、哪个配置类默认生效:SimpleCacheConfiguration
;
4、给容器中注册了一个CacheManager
:ConcurrentMapCacheManager
5、可以获取和创建ConcurrentMapCache
类型的缓存组件;他的作用将数据保存在ConcurrentMap
中;
@CachePut也可以声明一个方法支持缓存功能。与@Cacheable不同的是使用@CachePut标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
@CachePut:既调用方法,又更新缓存数据;同步更新缓存 修改了数据库的某个数据,同时更新缓存;前提是必须是同一个缓存,同一个key
运行时机:
测试步骤
1、查询id=40用户;查到的结果会放在缓存中;缓存key的过期时间设置为(60*5)5分钟
2、更新id=40的用户信息;
3、再次查询id=40的用户信息;
添加缓存注解@CachePut
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService{
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
/**
* 查询用户信息,并缓存结果
* @param id
* @return
*/
@Cacheable(cacheNames = "user", key = "#id")
public User getUserById(Long id) {
User user = getById(id);
log.info("从数据库中读取,而非从缓存读取!");
log.info("users: {}", user);
return user;
}
/**
* 更新用户信息
* @param user
* @return
*/
@CachePut(cacheNames = "user", key = "#result.id")
public User updateUser(User user) {
log.info("user: {}", user);
updateById(user);
User user1 = getById(user.getId());
return user1;
}
}
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("getById")
public ResponseEntity getById(@RequestParam Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
@PostMapping("updateUser")
public ResponseEntity updateUser(User user) {
return ResponseEntity.ok(userService.updateUser(user));
}
}
1、PostMan 查询id=40用户;查到的结果会放在缓存中;缓存key的过期时间设置为(60*5)5分钟
2、更新id=40的用户信息;数据库数据已更新成功
3、再次查询id=40的用户信息;查询返回的还是上一次缓存的数据,缓存未更新
为什么?
1、因为缓存默认的key
为参数的值,即第一次查询到的user
缓存的key 为id
的值40;
2、更新用户将方法的返回值放进缓存了;key是传入的user对象的值, 值是返回的User对象;是两个不同的缓存,所以总是显示为上一次缓存的数据
如何解决
只需要缓存相同的key就可以实现修改缓存数据;即取缓存的key和存缓存的key相同
key = "#result.id"
,key = "#user.id"
key = "#user.id":使用传入的参数的员工id;
key = "#result.id":使用返回后的id
注意:@Cacheable的key是不能用#result
@CachePut(cacheNames = "user", key = "#result.id")
public User updateUser(User user) {
log.info("user: {}", user);
updateById(user);
User user1 = getById(user.getId());
return user1;
}
@CacheEvict是用来标注在需要清除缓存元素的方法或类上的。当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作
@CacheEvict(cacheNames = "user", beforeInvocation = true, key = "#id")
public String deleteUserById(Long id) {
boolean b = removeById(id);
int i = 1/0;
return b?"删除成功":"删除失败";
}
@Caching
定义复杂的缓存规则
@Caching(
cacheable = {
@Cacheable(value="emp",key = "#lastName")
},
put = {
@CachePut(value="emp",key = "#result.id"),
@CachePut(value="emp",key = "#result.email")
}
)
public Employee getEmpByLastName(String lastName) {
return employeeMapper.getEmpByLastName(lastName);
}
每次都要定义cacheNames ,和key,可以在类上定义公共配置,方便管理
@CacheConfig(cacheNames = "emp"/*,cacheManager = "employeeCacheManager"*/) //抽取缓存的公共配置
Spring cache默认使用的是ConcurrentMapCacheManager==ConcurrentMapCache;将数据保存在 ConcurrentMap<Object, Object>中
开发中使用缓存中间件;redis、memcached、ehcache;或者替换本地缓存为Guava或Caffeine
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。