一、缓存简介
1、缓存的作用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。
2、哪些数据适合放入缓存
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率未定),后台如果发布一个商品,买家需要5分钟才能看到新的商品还是可以接受的。
3、缓存的方式
1)map实现本地缓存
本地缓存:运行在同一个项目里面,同一个副本里面。
本地缓存适用于单体应用,什么问题都没有,还很快。但是如果是分布式系统下,每一个系统都可能会放在多个不同的服务器,那么他们都需要有一个自己的本地缓存。
分布式系统下不应该使用本地缓存!
2)redis
分布式缓存,只需要一个缓存中间件。常用分布式缓存:redis。
直接使用redis对数据进行存取,就可以实现缓存操作。
第一次搜索某条数据的线程,去数据库中查找,找到后放入redis中。之后的线程在该数据过期之前,都直接从redis中取出该数据。
需要阿里巴巴的fast-Json工具,存入缓存和取出缓存的数据都为json数据。
二、高并发下缓存失效的三大问题
1、缓存穿透
产生条件: 查询一个一定不存在的数据。(利用不存在的数据进行攻击)
状态: 用户发送的请求永远无法命中该数据,且每次请求都去查询该数据。
结果: 数据库被疯狂请求,导致数据库压力增大,最终导致崩溃。
解决方法: null结果/空结果缓存,并加入短暂的过期时间。
2、缓存雪崩
产生条件: 设置缓存时,过多的key采用了相同的过期时间。
状态: 缓存在某一时刻同时失效。
结果: 请求全部转发到DB,DB瞬时压力过重雪崩。
解决方法: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
产生条件: 设置了过期时间的热点key(比如淘宝京东上的新手机秒杀)热点数据。
状态: 无时无刻都有大量数据访问,但是在某一刻有大量的请求发送过来,正好撞上缓存失效。
结果: 所有的请求都落到DB上。
解决方法: 加锁,大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去DB。
由于大多数的项目是分布式项目,所以就需要分布式锁才能保证安全,
三、Redis的Set操作实现分布式锁
分布式锁的缺点就是性能比较慢一点,本地锁/进程锁快一点,但是本地锁没办法锁住分布式的情况,本地锁只能锁定当前服务,每个服务都会获取同时一次数据。
在分布式情况下,想要锁住所有,必须使用分布式锁。
1、分布式锁原理:
2、redis分布式锁基本实现
占坑操作:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX:过期时间秒;
- PX:过期时间毫秒;
- NX:not exist 仅在不存在的情况下设置密钥。(不存在密钥就放入数据并返回OK,存在就不放入数据并返回nil)。
原理: 大家都来用这个key来保存东西,保存成功的线程就是唯一一个占坑成功的线程,返回OK。其他异步线程保存不成功并返回nil。
代码实现:
//1、占分布式锁,去redis占坑
//setIfAbsent就是set nx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
//加锁成功,设置过期时间!!!
redisTemplate.expire("lock",30,TimeUnit.MINUTES);
//查询数据库并赋返回值
//删除锁
redisTemplate.delete("lock");
//返回数据
}else{
//加锁失败...
//方案1、等待100ms之后重试
//方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}
以上代码的问题:
1、业务代码异常报错或者突然断电,没有执行删锁操作,这就造成了死锁问题;解决方案:设置锁的自动过期时间比如30s。
加锁和设置过期时间必须是原子性的操作!!!预防断电!!!
加锁的同时设置过期时间
改良版加锁代码:
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","1111",300,TimeUnit.SECONDS)
如果业务代码时间超长30s,分布式锁过期时间为10s,那么,10s后会有2个线程在执行业务代码,30s时,第一个线程会删除第三个线程的分布式锁。
两个问题:
- 业务超时了怎么办;
- 删锁的时候,导致删除别人的锁。
改良代码:
String uuid = UUID.randomUUID().toString();
//1、占分布式锁,去redis占坑
//setIfAbsent就是set nx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS)
if(lock){
//加锁成功
//查询数据库并赋返回值
//查看是否是自己加的锁,如果是自己加的锁,就删除,只能删除自己的锁
String lockValue = redisTemplate.opsForValue().get("lock");
if(lockValue.equals(uuid)){
redisTemplate.delete("lock");
}
//返回数据
}else{
//加锁失败...
//方案1、等待100ms之后重试
//方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}
真正的删锁,需要使用脚本
终极形态:
String uuid = UUID.randomUUID().toString();
// 一、加锁保证原子性
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,TimeUnit.SECONDS)
if(lock){
//加锁成功
//二、查询数据库并赋返回值
/**
* 三、解锁保证原子性
* 脚本解析
* 通过get得到KEYS[1](下面的Arrays.asList("lock")参数)对应的值
* 如果上面得到的值等于ARGV[1](下面的uuid),就执行删除操作,删除KEYS[1],否则返回0;
*/
String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
redisTemplate.excute(new DefaultRedisScript<Integer>(script,Integer.class),Arrays.asList("lock"),uuid);
String uuid = UUID.randomUUID().toString();
//四、返回数据
}else{
//加锁失败...
//方案1、等待100ms之后重试
//方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}
加锁保证原子性,解锁保证原子性!
业务还没执行完成,锁就过期了,所以此时需要对锁进行自动续期。最简单的方法就是把锁的过期时间设置为较长,比如3000s。
如下:
String uuid = UUID.randomUUID().toString();
// 加锁保证原子性
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,3000,TimeUnit.SECONDS)
if(lock){
//创建一个返回值的引用
try{
//查询数据库并赋返回值
}finally{
//解锁
String script = "if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end";
redisTemplate.excute(new DefaultRedisScript<Integer>(script,Integer.class),Arrays.asList("lock"),uuid);
String uuid = UUID.randomUUID().toString();
}
//返回数据
}else{
//加锁失败...
//方案1、等待100ms之后重试
//方案2、休眠100ms + 自旋,从头到尾再次调用当前方法(推荐)
}
四、官方推荐分布式锁!Redisson
1、Redisson简介
Redis官方说明,Redis的Set操作设计模式不推荐用来实现redis分布式锁。
官方原文:
注意: 下面这种设计模式并不推荐用来实现redis分布式锁。应该参考the Redlock algorithm的实现,因为这个方法只是复杂一点,但是却能保证更好的使用效果。
官方推荐实现分布式锁,Redisson:
分布式锁更专业的框架-Redisson
用Redisson完成所有的分布式锁功能
2、整合Redisson作为分布式锁等功能框架
1)导入依赖
Redisson和之前的jedis与lettuce一样,都是操作redis的客户端。只是Redisson能提供更强大更深入的功能特性。
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
2)配置redisson
使用程序化配置方法:
创建一个新配置类MyRedissonConfig.java
package pers.tangxz.learn.configs;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* @Info:
* @Author: 唐小尊
* @Date: 2020/09/25 23:32
*/
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1、创建配置
Config config = new Config();
//2、使用单节点模式redis://或者rediss://(安全连接)
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//3、根据config创建出Redisson实例并返回
return Redisson.create(config);
}
}
3、使用Redisson分布式锁
1)基本使用
1、可重入锁
锁A里面调用了锁B,A和B是同一把锁,那么B将直接使用A的锁。
2、不可重入锁
锁A里面调用了锁B,A和B是同一把锁,那么B就不会执行,直接死锁。
redisson直接获取的锁,默认就是可重入锁。
基于Redis的Redisson分布式可重入锁RLock
Java对象实现了java.util.concurrent.locks.Lock
接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
@GetMapping(value = "/hello")
public String hello(){
//1、获取一把锁,只要锁名字相同,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock();
try{
//3、执行业务
System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
}catch (Exception e){
}finally {
//4、解锁,假设解锁代码没有运行,redisson会不会出现死锁。
//在程序运行过程中,终止程序。
//即使没有解锁成功,三十秒后也会自动过期
myLock.unlock();
System.out.println("释放锁");
}
return "123";
}
Redisson中有一个看门狗机制,可以对锁进行续期。默认情况下,看门狗检查所的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
2)看门狗原理,redisson解决死锁
//1、获取一把锁,只要锁名字相同,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁,十秒钟自动解锁。业务超时时,无法自动续期。
//会出现删错锁的情况。
//自动解锁时间一定要大于业务的执行时间。
myLock.lock(10,TimeUnit.SECONDS);
- 如果我们传递了锁的超时时间,就会发送给redis执行脚本,进行占锁,默认超时时间就是我们指定的时间。
- 如果我们未指定锁的超时时间,就使用30 * 1000【LockWatchdogTimeout看门狗的默认时间】每隔十秒就会再次自动续期,就会续成30s。
- internalLockLeaseTime:看门狗时间,30/3=10,10s后续期一次。
看门狗续期源码
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
//重新调用方法,自旋
RedissonLock.this.renewExpiration();
}
}
});
}
}
}//定时任务延时为 默认时间/3
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
3)最佳实战
直接给定三十秒,如果一个业务 超过三十秒,就说明这个业务完蛋了。
所以我们只需要手动加锁,手动解锁就行。
lock.lock(30,TimeUnit.SECONDS);
4)读写锁
读锁不阻塞。
写锁会阻塞读操作。写锁释放之后,才可以读。写锁没释放,读就一直等待。
写锁:排他锁(互斥锁)
读锁:共享锁
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
读写锁细节:
5)分布式闭锁(CountDownLatch)
模拟学校,全部同学来完之后,才允许关门。
全部任务执行完成之后,才允许放行。
RCountDownLatch latch = redisson.getCountDownLatch("door");
//设置闭锁所需要down的次数。
latch.trySetCount(5);
//闭锁在这里等待,直到闭锁值为0
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch door = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();//计数减一
6)分布式信号量(可用于分布式限流)
假如当前服务最多承受每秒一万。只要所有服务一上来就先去获取一个信号量,这些信号量一上来就是一万。
模拟车库停车,只有三个车位,来一辆车占用一个车位
普通信号量
//1、注入
@Autowired
RedissonClient redisson;
@Autowired
StringRedisTemplate redisTemplate;
//2、设置初始值代码:
redisTemplate.opsForValue().set("locka", String.valueOf(10));
//3、占车位代码:
RSemaphore park = redisson.getSemaphore("park");
//获取一个信号,无返回值。如果没有信号量,则一直等待。
park.acquire();
//尝试获取一个信号量,如果没有信号量,则返回false,如果获取到了信号量,则返回true。
park.tryAcquire();
//尝试获取一个信号量,最多等待23s
park.tryAcquire(23, TimeUnit.SECONDS);
//尝试23s异步获取一个信号量
park.tryAcquireAsync(23, TimeUnit.SECONDS);
//4、释放车位代码
RSemaphore park = redisson.getSemaphore("park");
Boolean b = park.release();//释放一个信号,释放一个车位。
park.release(10);//释放10个信号量
可过期性信号量
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);
五、分布式下的缓存一致性问题
1、设置锁的细节
锁的粒度,越细越快。
比如product-11-lock、product-12-lock:每一个商品单独加锁
2、缓存里面的数据如何和数据库保持一致
缓存数据一致性
- 双写模式
- 失效模式
两种模式在大并发中都容易出现错误。
1)双写模式
改数据库的同时写缓存。很容易出bug。
在一个数据没写完之前,又有一个新数据比之前的线程更快,那么就会产生脏数据。
解决方案:
- 在一个线程写数据前,直接加锁,直到写入缓存完成。
- 如果允许暂时的数据不一致,则允许这种情况发生,一般都是一天或者几个小时后自动失效的缓存,且对数据一致性要求不高的数据,才允许这种情况。
2)失效模式(简单实用)
- 把数据库改完之后直接删掉缓存中的数据。
- 等待下一次主动查询进行更新。
- 经常修改的数据,实时性比较高,是否需要加缓存是需要考量的。
- 如果一个数据,经常修改,经常加锁,导致更慢,这种情况就不加锁
3)最好的设计
对每一个缓存添加一个过期时间,哪怕暂时的脏数据,只要过期时间一到,数据一清空,用户再次请求就可以得到正确的最新的数据。
3、缓存数据一致性-解决方案
- 无论是双写模式还是失效模式,都会导致缓存的不一致问题,即多个实例同时更新会出事。怎么办?
- 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
- 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁保证并发读写,写+写的时候按顺序排好队。读+读无所谓。所以适合用读写锁。(业务不关心脏数据,允许临时脏数据可忽略)
- 总结:
- 我们能放入缓存的数据本就不该是实时性、一致性要求超高的。
- 所有缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。数据过期下一次查询出发主动更新。
- 我们不应该过度设计,增加系统的复杂性。
- 遇到实时性、一致性要求高的数据,就应该查询数据库,即使慢点。
- 读写数据的时候,加上分布式的读写锁。
4、Canal
阿里开源的一个中间件,可以模拟,它是数据库的一个从服务器,比如mysql有一个库,里面装了canal,canal就把自己伪装成一个从服务器,从服务器的特点就是mysql里面有什么变化,它就会同步过来,正好利用canal的这个特性,只要业务代码更新了数据库,mysql的这个数据库肯定要开启binlog这个二进制日志,日志里面有每一次的mysql更新的记录,canal就假装成mysql的从库,把mysql的每一个更新都拿过来,相当于只要有更新,canal就知道了。
好处就是编码期间,只需要该数据库就行了,不用管缓存的任何操作,canal在后台,只要相应的数据库改了,相应的缓存也会都被改掉。屏蔽掉了缓存的操作。
缺点就是又加了一个中间件,还要额外开发一些自定义的功能。
好处就是一次开发成型,以后就不管这些事情了。
- canal在大数据情况下通常用来解决数据异构问题。
- 比如每一个人去京东,每一个人的首页都不一样,这都是基于爱好的。
- 可以实时订阅访问记录表的更新和商品信息表的更新。通过这些东西的分析计算,生成另外一张用户商品推荐表。
- 上大数据相关的系统的时候,可以使用这些方案。
五、springCache
1、简介
- Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并使用JCache(JSR-107)注解简化开发。
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
- 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有,就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
- 使用Spring缓存抽象时,我们需要关注以下两点;
- 确定方法需要被缓存以及他们的缓存策略。
- 从缓存中读取之前缓存存储的数据。
缓存管理器可以存放无数个各种各样的缓存。
2、整合SpringCache简化缓存的开发
1)引入依赖
<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>
2)写配置
-
自动配置了哪些
- CacheAutoConfiguration 会导入 RedisCacheConfiguration;
- 自动配好了缓存管理器 RedisCacheManager
-
写一个配置文件application.properties
-
spring.cache.type=redis
-
-
测试使用缓存
@Cacheable
:触发将数据保存到缓存的操作。@CacheEvict
:触发将数据从缓存中删除的操作@CachePut
:更新缓存,不影响方法执行。@Caching
:组合以上多个缓存操作。@CacheConfig
:在类级别共享缓存的相同配置。
- 开启缓存功能
//在启动类添加注解
@EnableCaching
2)存缓存
-
只需要注解就能完成缓存操作。
- 在service的实现层,给某个方法添加 @Cacheable注解。表示当前方法的结果需要缓存,如果缓存中有,则方法不调用。如果缓存中没有,会调用方法,最后将方法的结果存入缓存中。
- 每一个需要缓存的数据,我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】
- @Cacheable({“category”,“product”}),给两个分区存储该方法的返回数据。
-
默认行为
- 如果缓存中有,方法不用调用;
- key默认自动生成,缓存的名字::SimpleKey{}(自主生成的key值);
- 缓存的value值,默认使用JDK序列化机制,将序列化后的数据存到redis。
- 默认ttl时间-1;
-
自定义操作:
-
指定生成的缓存使用的key;
- key属性指定,接受一个SpEl表达式,可以得到方法名,root名,参数名等。
- 官网的SpEl表达式语法
- key的值为一个表达式,所以如果需要设置字符串,就需要用单引号。
- @Cacheable(value={“category”},key="‘level1Category’")
- @Cacheable(value={“category”},key="#root.method.name")
-
指定缓存的数据的存活时间;
-
在application.properties配置文件中设置存活一个小时
-
下面的配置需要在自定义缓存管理器中手动配置才能生效。
-
spring.cache.redis.time-to-live=3600000 #毫秒为单位
-
-
将数据保存为json格式。
-
CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置 -> 如果redisCacheConfiguration有就用已有的,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration 即可 -> 就会应用到当前RedisCacheManager 管理的所有缓存分区中。
-
自定义缓存管理器
-
新建一个配置类,MyCacheConfig.java
-
把主类的@EnableCache剪切到MyCacheConfig.java中
-
package pers.tangxz.learn.configs; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import org.springframework.cache.annotation.EnableCaching; 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.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @Info: * @Author: 唐小尊 * @Date: 2020/09/26 13:36 */ @Configuration @EnableCaching public class MyCacheConfig { @Bean RedisCacheConfiguration redisCacheConfiguration(){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); //GenericFastJsonRedisSerializer兼容各种泛型的Json序列化 config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); return config; } }
-
加上在配置文件中配置的ttl过期时间之后
package pers.tangxz.learn.configs; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import org.springframework.boot.autoconfigure.cache.CacheProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; 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.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @Info: * @Author: 唐小尊 * @Date: 2020/09/26 13:36 */ @Configuration @EnableCaching @EnableConfigurationProperties(CacheProperties.class) public class MyCacheConfig { // @Autowired // CacheProperties cacheProperties; @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); //GenericFastJsonRedisSerializer兼容各种泛型的Json序列化 config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer())); //将配置文件中的所有配置都生效 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.isCacheNullValues()){ config = config.disableKeyPrefix(); } return config; } }
-
完整配置文件
#SpringCache设置为redis缓存 spring.cache.type=redis #加一个过期时间一个小时 spring.cache.redis.time-to-live=3600000 #如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀,就让默认的分区名作为前缀。 #spring.cache.redis.key-prefix=CACHE_ #是否使用前缀 spring.cache.redis.use-key-prefix=true #是否缓存空值,防止缓存穿透 spring.cache.redis.cache-null-values=true
-
-
-
3)缓存一致性的操作
- 支持双写模式的注解:@CachePut,可以帮助我们更新缓存。
- 支持失效模式的注解@CacheEvict从缓存中删除数据
@CacheEvict
的使用(失效模式)- 删除@Cacheable(value={“category”},key="#root.method.name")的缓存,其key为获取数据的方法的名字:getLevel1Categorys;
- 在修改的Service方法上面添加注解
@CacheEvict(value="category",key="'getLevel1Categorys'")
; - 删掉该缓存,然后下一次获取数据的时候,再把新数据放入缓存。
@Caching
同时进行多种缓存操作- 修改一个关联两个缓存的数据,就需要同时删除这两个数据,使用@Caching(组合多个操作)
- 在修改方法上添加注解
@Caching(evict{
@CacheEvict(value="category",key="'getLevel1Categorys'"),
@CacheEvict(value="category",key="'getCatalogJson'")
})
-
直接清除某个分区的全部缓存
- @CacheEvict(value=“category”,allEntries=true)
- 存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀。
-
@CachePut(双写模式)
- 如果某个修改方法,返回的是修改后的最新的对象,那么改完之后,直接存入数据。
3、SpringCache的原理与不足
-
读模式
-
缓存穿透
-
查询一个永不存在的数据
-
解决:缓存空数据
-
#是否缓存空值,防止缓存穿透 spring.cache.redis.cache-null-values=true
-
-
缓存击穿
-
大量并发进来同时查询一个正好过期的数据
-
解决:加锁
-
@Cacheable(value={"category"},key="#root.method.name",sync=true)
-
-
缓存雪崩
-
大量的key同时过期(超大型系统里面可能会存在)
-
解决:加随机过期时间
-
#加一个过期时间 spring.cache.redis.time-to-live=1000
-
-
-
写模式(缓存与数据库一致)
- 读写加锁;
- 引入Canal,感知到MySQL的更新去更新数据库;
- 读多写多,直接去数据库查询就行。
-
总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据),完全可以使用Spring-Cache。写模式(只要缓存的数据有过期时间就足够了)
- 特殊数据:特殊设计
-
原理:
- CacheManager(RedisCacheManager) -> Cache(RedisCache) -> Cache 负责缓存的读写。