一、缓存
Redis作为"缓存"。缓存:数据的高速临时存取的技术
如何使用缓存?
如果数据库里数据变了,缓存怎么办?
如果数据库里数据变了,缓存没有同步进行变化,造成:MySQL和Redis数据不一致
解决方案:
方案1:对数据库里的数据 做什么样的修改,也对Redis里的缓存做同样的修改
方案2:数据库里的数据 无论有任何变化,直接清除Redis缓存。客户端查询时没有缓存会重新从数据库里查询,再放到缓存里。
建议采用方案2,方案1太繁琐。
从上述分析使用缓存:
-
当查询数据时
优先从Redis里查询缓存,如果查询到缓存的数据,就直接返回给客户端
如果查不到缓存数据:从数据库里查询得到数据,把数据缓存到Redis里,然后再给客户端返回结果
-
当数据有变化时:增、删、改时
要防止出现数据不一致问题:MySQL里的数据被修改了,缓存Redis里还是旧数据
解决的方案:可以在增、删、改成功之后,清除掉缓存
每次操作查询都需要在原有业务代码前后各加一部分代码,操作增、删、改 都需要清除缓存,很多代码重复率高,所以可以想到,在不改变原有业务代码的情况下,对原有业务增强,我们可以使用AOP对其方法经行增强。然后发现使用AOP虽然可以完成功能增强,但是代码写起来会很繁琐,比如在考虑切入点时,我们不可能做到每个方法命名都提前固定好,所以要是使用注解方式,定义注解还需要设定value值来区分要增强的方法等等。
所以Sping就提供了SpringCache 下面就来介绍一下SpringCache以及使用方式和注意细节。
二、SpringCache
1. 介绍
在企业开发中,缓存对于提升程序性能有非常大的作用,所以已经广泛应用于企业项目开发中。但是缓存技术是多种多样的,例如Redis、Caffeine、MemCache、EhCache等等。而不同的缓存技术,其操作方法并不统一,并且还需要开发人员编写代码实现缓存操作。
从Spring3.1版本开始,Spring就利用AOP思想,对不同的缓存技术做了再封装,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。让开发人员只专注于业务,不需要再关心具体的缓存技术。
2. 使用步骤
-
添加起步依赖:spring cache的依赖坐标,缓存技术比如redis的坐标
-
在引导类上添加注解 @EnableCaching,开启缓存功能
-
在方法上添加注解使用缓存:
-
方法查询数据使用缓存:@Cacheable(cacheName="",key="")
缓存的键是:cacheName::key
缓存的值是:方法的返回值
-
方法执行后想要清除缓存:@CacheEvict(cacheName="",key="")
把键为cacheName::key对应的数据清除掉
-
方法执行后想要更新缓存:@CachePut(cacheName="",key="")
更新哪个键的:cacheName::key
把值更新成:方法的返回值,会把Redis里原来的值给覆盖掉
-
3. 使用要求
- SpringCache结合Redis时,默认会使用JDK序列化方式,将数据序列化成字节数组,再缓存起来。
- 所以实现类要实现
Serializable
接口
4. 常用注解
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值更新到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除。哪个方法执行后想要清除缓存,就在方法上加这个注解 |
5. SpEL表达式
Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:
名称 | 位置 | 描述 | 示例 |
---|---|---|---|
ArgumentName |
执行上下文 | 当前被调用的方法的参数 | 如findArtisan(Artisan artisan) 方法 可以通过#artsian.id 获得参数 |
result |
执行上下文 | 方法执行后的返回值 仅当方法执行后的判断有效 如 unless cacheEvict的beforeInvocation=false | #result |
methodName |
root对象 | 当前被调用的方法名 | #root.methodname |
method |
root对象 | 当前被调用的方法 | #root.method.name |
target |
root对象 | 当前被调用的目标对象实例 | #root.target |
targetClass |
root对象 | 当前被调用的目标对象的类 | #root.targetClass |
args |
root对象 | 当前被调用的方法的参数列表 | #root.args[0] |
caches |
root对象 | 当前被调用的方法使用的缓存列表 | #root.caches[0].name |
注意:
- 使用方法参数时,可以直接写成
#参数名
,也可以写成:#p参数索引
,例如#p0
表示索引0的参数
6. 使用示例
6.1 起步依赖:
xml
<!-- SpringCache起步依赖坐标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Redis起步依赖坐标 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
如果只添加一个SpringCache依赖的话,springCache默认将会使用ConcurrentHashMap作为缓存容器。但是Spring Cache 其实提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache,如果添加了EHCache的依赖坐标,SpringCache将会使用EhCache作为缓存容器
- Caffeine,如果添加了caffeine的依赖坐标,SpringCache将会使用Caffeine作为缓存容器
- Redis(常用),如果添加了Redis依赖坐标,SpringCache将会使用Redis作为缓存容器
- ......
6.2 引导类上加注解@EnableCaching:
java
@EnableCaching //开启声明式缓存功能
@SpringBootApplication
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class, args);
}
}
6.3 使用缓存加@Cacheable
@Cacheable 说明:
-
作用:在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;
若没有数据,调用方法并将方法返回值放到缓存中
-
用法:
@Cacheable(cacheNames="", key="")
缓存的key:以
cacheNames::key
的值作为key,查找对应的值 -
注意:注解里的
key
支持SpringEL表达式
java
/**
* 根据id查询用户
* Spring会优先从缓存中查找:以cacheNames::key作为key,查找对应的值
* 如果找到了,就会直接返回结果;这个方法是不会执行的
* 如果找不到,才会执行这个方法,并把方法的返回值缓存起来
*/
@Override
@Cacheable(cacheNames = "user", key = "#id")
public User queryById(Long id) {
System.out.println(">>>>UserServiceImpl.queryById方法执行了");
return userMapper.selectById(id);
}
测试效果
然后在测试类里增加测试方法,并执行:
java
@Test
public void testCacheable(){
System.out.println("--------第一次查询用户1,没有缓存,会执行SQL语句从数据库里查询,然后把记过存到缓存里");
System.out.println(userService.queryById(1L));
System.out.println("--------第二次查询用户1,缓存里有数据,直接取缓存,不会执行SQL语句了 ");
System.out.println(userService.queryById(1L));
System.out.println("--------第三次查询用户1,缓存里有数据,直接取缓存,不会执行SQL语句了");
System.out.println(userService.queryById(1L));
}
测试结果
6.4 清理缓存加@CacheEvict
@CacheEvict 说明
-
作用:清理指定缓存
-
用法:
- 用法1:
CacheEvict(cacheNames="", key="")
,清除cacheNames::key
对应的缓存 - 用法2:
CacheEvict(cacheNames="", allEntries=true)
,清理所有以cacheNames::
开头的key对应的缓存
- 用法1:
-
注意:注解里的
key
支持SpringEL表达式
使用示例
java
@Override
@CacheEvict(cacheNames = "user", key = "#id")
public void deleteUser(Long id) {
System.out.println(">>>>UserServiceImpl.deleteUser方法执行了");
userMapper.deleteById(id);
}
测试效果
java
@Test
public void testCacheEvict(){
//从数据库里删除掉用户1,并且也会从缓存中清除掉 user::1 对应的缓存
userService.deleteUser(1L);
}
测试结果
6.5 更新缓存加@CachePut
@CachePut 说明
-
作用:将方法返回值,放入缓存(更新缓存)
-
用法:
@CachePut(cacheNames="", key="")
-
缓存的key:Spring将使用
cacheNames的值::key的值
作为缓存的key -
缓存的值:Spring将方法的返回值作为缓存的value
-
-
注意:注解里的
key
支持SpringEL表达式
使用示例
如果在做新增操作,或者修改操作时,可以更新缓存:当新增或修改操作后,希望把最新的数据缓存起来,方便后续使用。可以在新增或修改方法上加注解@CachePut
java
/**
* 新增用户方法
* 注解@CachePut将会把方法返回值缓存起来:以cacheNames+key作为缓存的key,以方法返回值作为缓存的value
*/
@Override
@CachePut(cacheNames = "user", key = "#user.id")
public User addUser(User user) {
userMapper.insert(user);
return user;
}
测试效果
java
@SpringBootTest
public class CacheTest {
@Autowired
private UserService userService;
@Test
public void testCachePut(){
User user = new User();
user.setName("pony");
user.setAge(60);
//新增完成后,数据库里会多一条数据,使用AnotherRedisDesktopManager连接Redis,会发现也有此用户的缓存
userService.addUser(user);
}
}