1、简介
- Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发;
- Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;
- 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。
- 使用 Spring 缓存抽象时我们需要关注以下两点;
1、确定方法需要被缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
2、基础概念
2.1 缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落 盘工作。 哪些数据适合放入缓存?
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率 来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
2.2 伪代码
data = cache.load(id);//从缓存加载数据
If(data == null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到 cache 中
}
return data;
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没 有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题。
2.3 切换使用 jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
3、 缓存失效问题
先来解决大并发读情况下的缓存失效问题;
3.1 缓存穿透
- 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次 请求都要到存储层去查询,失去了缓存的意义。
- 在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是 漏洞。
- 解决: 缓存空结果、并且设置短的过期时间。
3.2 缓存雪崩
- 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失 效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
- 解决: 原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。
3.3 缓存击穿
- 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问, 是一种非常“热点”的数据。
- 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
- 解决: 加锁
4、缓存数据一致性
4.1 保证一致性模式-双写模式
4.2 保证一致性模式-失效模式
4.3 改进方法 1-分布式读写锁
分布式读写锁。读数据等待写数据整个操作完成(具体见下面的分布式锁)
4.4 改进方法 2-使用 cananl
5、注解
Cache | 缓存接口,定义缓存操作,实现有:RedisCache, EhCache,ConcurrentMapCache等 |
---|---|
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@Cacheable | 主要针对方法配置,能够根据方法请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@EnableCaching | 开启基于注解的缓存 |
keyGenerator | 缓存数据时key生生策略 |
Serialize | 缓存数据时value的序列化策略 |
@Cacheable/@CachePut/@CacheEvict 主要的参数
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如∶@Cacheable(value=“mycache”)或者@Cacheable(value={"cache1"“cache2”} |
---|---|---|
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 例如∶@Cacheable(value=“testcache”,key=“#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断 | 例如@Cacheable(value=“testcache”,condition=“#userNam e.length()>2”) |
allEntries(@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为true,则方法调用后将立即清空所有缓存 | 例如∶@CachEvict(value=“testcache”,allEntries=true) |
beforelnvocation (@cacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | 例如∶@CachEvict(value=“testcache”, beforelnvocation=true) |
unless (@CachePut)(@Cacheable) | 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 | 例如∶ @Cacheable(value=“testcache"unless=”#result =nul") |
6、表达式语法
Cache SpEL available metadata
名字 | 位置 | 描述 | 示列 |
---|---|---|---|
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.methodName |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={cache1",#root.caches【0】.name “cache2”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字.可以直接 掺参数名,也可以使用 #p0或#a0的形式,0代表参数的索引; | #iban、#a0、#p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如’unless’,'cache put的表达式’cache evict’的表达式beforelnvocation=false) | #result |
7、整合springCache
目录结构只需关注redis目录下的结果即可
7.1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
7.2、编写配置
7.2.1 已经帮我们配置好了哪些
- CacheAuroConfiguration会导入 RedisCacheConfiguration,自动配好了缓存管理器RedisCacheManager
7.2.2 我们的操作步骤
1. 配置文件properties使用redis作为缓存 spring.cache.type=redis
spring.redis.host=47.103.114.78
spring.redis.port=6379
# 配置缓存的 类型为 redis
spring.cache.type=redis
#缓存的时间 单位是以毫秒为单位
spring.cache.redis.time-to-live=3600000
# spring.cache.cache-names=cache1,cache2 .... 配置缓存的名字,如果这里配置了那么所有的缓存的名字
# 都要在这里事先配置,会给你禁用掉动态生产缓存名字
# 缓存的前缀,这里指定了就回你用我们指定的前缀,如果没有指定默认就使用缓存的名字(分区)作为前缀,建议使用默认的分区
# spring.cache.redis.key-prefix=CACHE_
# 是否开启缓存的前缀
spring.cache.redis.use-key-prefix=false
# 是否缓存null值 防止缓存穿透
spring.cache.redis.cache-null-values=true
2. 主启动类开启缓存
@EnableCaching
@SpringBootApplication
public class ValidApplication {
public static void main(String[] args) {
SpringApplication.run(ValidApplication.class, args);
}
}
3. 编写配置类(自定义redis序列化的机制等)
package com.st.valid.redis.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @创建人: 放生
* @创建时间: 2022/4/3
* @描述:
*/
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
public class MyCacheConfig {
/**
* 配置文件中的东西没有用上;
*
* 1、原来和配置文件绑定的配置类是这样子的
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties
*
* 2、要让他生效
* @EnableConfigurationProperties(CacheProperties.class)
*
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//将配置文件中的所有配置都生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
4. sevice
package com.st.valid.redis.service;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/4/3
* @描述: 定义三个接口,分别是获取,删除,修改 缓存的数据
*/
public interface RedisCacheService {
Map<String,String> getData();
Map<String,String> invalid();
Map<String,String> doubleWrite();
}
5. serviceIpml
package com.st.valid.redis.service.ipml;
import com.st.valid.redis.service.RedisCacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/4/3
* @描述:
*/
@Service
@Slf4j
public class RedisCacheServiceIpml implements RedisCacheService {
/**
* 1、每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
* 2、 @Cacheable({"category"})
* 代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
* 如果缓存中没有,会调用方法,最后将方法的结果放入缓存
* 3、默认行为
* 1)、如果缓存中有,方法不用调用。
* 2)、key默认自动生成;缓存的名字::SimpleKey [](自主生成的key值)
* 3)、缓存的value的值。默认使用jdk序列化机制,将序列化后的数据存到redis
* 4)、默认ttl时间 -1;
*
* 自定义:
* 1)、指定生成的缓存使用的key: key属性指定,接受一个SpEL
* SpEL的详细https://docs.spring.io/spring/docs/5.1.12.RELEASE/spring-framework-reference/integration.html#cache-spel-context
* 2)、指定缓存的数据的存活时间: 配置文件中修改ttl
* 3)、将数据保存为json格式:
* 自定义RedisCacheConfiguration即可
* 4、Spring-Cache的不足;
* 1)、读模式:
* 缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;?默认是无加锁的;sync = true(加锁,解决击穿)
* 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新数据库
* 3)、读多写多,直接去数据库查询就行
* 总结:
* 常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)
* 特殊数据:特殊设计
*
* 原理:
* CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
*
* 1、value = {"category"} : 相当于是一个分区
* 2、key = "#root.method.name" 在category分区下的key为方法的名字如果指定常量为key key = "‘k1’"
* 3、默认是无加锁的;sync = true(加锁,解决击穿)
* @return
*/
// @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
@Cacheable(value = {"category"},key = "'like_fruit'",sync = true)
@Override
public Map<String, String> getData() {
log.info("未命中缓存。。。。。getData。。。");
// 模拟业务操作
Map<String, String> data = new HashMap<>();
data.put("key1","苹果");
data.put("key2","香蕉");
data.put("key3","桔子");
data.put("key4","西瓜");
return data;
}
/**
* 失效模式
* 场景分析:如果我们往db中更新了一条数据,假如是客户的信息,那么客户的分区下的缓存信息需要更新
* 可能在另外一个订单分区也需要更新客户信息就可以使用如下的方法
*
* @return
*/
// @Caching(evict = { // 会删除指定的分区下 和指定的key 的缓存
// @CacheEvict(value = "category",key = "'like_fruit'"),
// @CacheEvict(value = "category",key = "'like_fruit'")
// })
@CacheEvict(value = "category",allEntries = true) //失效模式,会删除category分区下所有的缓存
@Override
public Map<String, String> invalid() {
log.info("失效模式。。。。。invalid。。。");
Map<String, String> map = new HashMap<>();
map.put("delete", "all_fruit");
return map;
}
/**
* 双写模式
*
* @return
*/
@CachePut(value = "category",key ="'like_fruit'" )
@Override
public Map<String, String> doubleWrite() {
log.info("双写模式。。。。。doubleWrite。。。");
Map<String, String> data = new HashMap<>();
data.put("key1","荔枝");
data.put("key2","葡萄");
data.put("key3","柚子");
data.put("key4","榴莲");
return data;
}
}
6. controller
package com.st.valid.redis.controller;
import com.st.valid.redis.service.RedisCacheService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/4/3
* @描述:
*/
@RestController
public class RedisCacheController {
@Autowired
private RedisCacheService redisCacheService;
@GetMapping("/getData/fromCache")
public Map<String,String> getData(){
return redisCacheService.getData();
}
@GetMapping("/getData/invalid")
public Map<String,String> invalid(){
return redisCacheService.invalid();
}
@GetMapping("/getData/doubleWrite")
public Map<String,String> doubleWrite(){
return redisCacheService.doubleWrite();
}
}
7.2.3 测试使用缓存
@Cacheable: Triggers cache population.:触发将数据保存到缓存的操作
@CacheEvict: Triggers cache eviction.:触发将数据从缓存删除的操作 执行更新缓存 (失效模式)
@CachePut: Updates the cache without interfering with the method execution.:不影响方法执行更新缓存(双写模式)
@Caching: Regroups multiple cache operations to be applied on a method.:组合以上多个操作
@CacheConfig: Shares some common cache-related settings at class-level.:在类级别共享缓存的相同配置
1)、开启缓存功能 @EnableCaching
2)、只需要使用注解就能完成缓存操作
获取数据from cache:127.0.0.1:8080/getData/fromCache
删除缓存-失效模式:127.0.0.1:8080/getData/invalid
更新缓存-双写模式:127.0.0.1:8080/getData/doubleWrite
7.2.4 原理
CacheAutoConfiguration -> RedisCacheConfiguration ->自动配置了RedisCacheManager->初始化所有的缓存->每个缓存决定使用什么配置 ->如果redisCacheConfiguration有就用已有的,没有就用默认配置 ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可 ->就会应用到当前RedisCacheManager管理的所有缓存分区中
8、分布式锁
8.1 分布式锁与本地锁
8.2、分布式锁实现
使用 RedisTemplate 操作分布式锁
public Map<String, List<Data2Vo>> getDataJsonFromDbWithRedisLock() {
//1、占分布式锁。去 redis 占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功...");
//加锁成功... 执行业务
//2、设置过期时间,必须和加锁是同步的,原子的
//redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Data2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class) , Arrays.asList("lock"), uuid);
}
//获取值对比+对比成功删除=原子操作 lua 脚本解锁
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(uuid.equals(lockValue)){
// //删除我自己的锁
// redisTemplate.delete("lock");//删除锁
// }
return dataFromDb;
}else {
//加锁失败...重试。synchronized ()
//休眠 100ms 重试
System.out.println("获取分布式锁失败...等待重试");
try{Thread.sleep(200); }catch (Exception e){ }
return getDataJsonFromDbWithRedisLock();//自旋的方式
}
}
8.3 Redisson 完成分布式锁
8.3.1 简介
Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分 的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者 提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工 具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式 系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间 的协作。
官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
8.3.2 配置
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();
Config config = new Config();
//redis://127.0.0.1:7181 //可以用"rediss://"来启用 SSL 连接
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
RedissonClient redisson = Redisson.create(config);
8.3.3 使用分布式锁
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
// 加锁以后 10 秒钟自动解锁
// 无需调用 unlock 方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待 100 秒,上锁以后 10 秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
}
finally {
lock.unlock();
}
}