为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。
哪些数据适合放入缓存?
第一步、引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
第二步、配置redis主机地址
spring:
redis:
host: 124.222.223.222
port: 6379
@Autowired
StringRedisTemplate redisTemplate;
@Test
public void testStringRedisTemplate() {
// 存入一个 hello world
ValueOperations<String, String> ops = redisTemplate.opsForValue();
// 保存数据
ops.set("hello", "world_"+ UUID.randomUUID().toString());
// 查询数据
System.out.println("之前保存的数据:" + ops.get("hello"));
}
优化菜单获取业务getCatalogJson,使用 从缓存中查询并封装分类数据
在 CategoryServiceImpl 实现类中 将原来的从数据库中查询并封装分类数据 的方法名改为 getCatalogJsonFromDb()
供调用!,并编写 从缓存中查询并封装分类数据的方法
/**
* 从缓存中查询并封装分类数据
* 给换从中放JSON字符串,拿出来的JSON字符串,还要逆转为能用的对象类型【序列化与反序列化】
* @return
*/
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
// 1、加入缓存逻辑,缓存中存放的数据是json字符串
// JSON跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 2、缓存中没有数据,则查询数据库并保存
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// 3、查到的数据放入缓存,将查出的对象转为json放在缓存中
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogJsonFromDb));
return catalogJsonFromDb;
}
// 4、将读到的JSON字符串转为我们想要的
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference< Map<String, List<Catelog2Vo>>>(){});
return result;
}
测试爆出 lettuce堆外内存溢出bug
当进行压力测试时后期后出现堆外内存溢出OutOfDirectMemoryError
产生原因:
springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信
lettuce的bug导致netty堆外内存溢出。netty如果没有指定堆外内存,默认使用Xms的值,可以使用-Dio.netty.maxDirectMemory进行设置
解决方案:由于是lettuce的bug造成,不要直接使用-Dio.netty.maxDirectMemory去调大虚拟机堆外内存,治标不治本。
lettuce和jedis是操作redis的底层客户端,RedisTemplate是再次封装
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
lettuce 和 jedis 都是操作Redis的底层客户端。Spring 再次封装 redisTemplate ;
缓存穿透
缓存失效 :缓存没有命中到,没有使用
缓存击穿 (量太大)
缓存击穿:
缓存击穿 , 是指一个key非常热点 , 在不停的扛着大并发 , 大并发中对这一个点进行访问 , 当这个key在失效的瞬间 , 持续的大并发就穿破缓存 , 直接请求数据库 , 就像在一个屏幕上凿开了一个洞 .
当某个key在过期的瞬间 , 有大量的请求并发访问 , 这类数据一般是热点数据 , 由于缓存过期 , 会同时访问数据库来查询最新数据 , 并且会写缓存 , 会导致数据库瞬间压力过大 .
比如热搜排行上,一个热点新闻被同时大量访问就可能导致缓存击穿。
解决:
缓存雪崩
1、空结果缓存:解决缓存穿透
2、设置过期事件(加随机值):解决缓存雪崩
3、加锁:解决缓存击穿
方法一:本地锁 在代码块上加synchronized(this)
,SpringBoot所有的组件在容器中都是单例的。
方法二:分布式锁
方法一:本地锁 在代码块上加synchronized(this)
,SpringBoot所有的组件在容器中都是单例的。
锁-时序问题
这里进行压力测试的时候出现了查询多次数据库没锁住,即锁-时序问题,因为我们将结果放入缓存这一步写在了锁外面,比如1号请求在数据库中查到了数据之后就释放了锁,此时还没将结果放入缓存,此时2号请求竞争到锁由于此时1号请求在数据库中查到的数据还没有放入到缓存,故缓存中没有再次查询数据库
解决:
将结果放入缓存这一步写到锁里面去,不要放在释放锁之后做
本地锁在分布式下的问题
复制配置创建多个进程服务
和我们预想的一样在分布式情况下,每个服务都要查询一次数据库。虽然没有完全锁住,但确实优化了。
CategoryServiceImpl 实现类:
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
/*
* 1、空结果缓存:解决缓存穿透
* 2、设置过期事件(加随机值):解决缓存雪崩
* 3、加锁:解决缓存击穿
* */
// 1、加入缓存逻辑,缓存中存放的数据是json字符串
// JSON跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
System.out.println("缓存不命中__查询数据库");
// 2、缓存中没有数据,则查询数据库并保存(保存缓存的代码写在了getCatalogJsonFromDb()方法里面)
Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
}
System.out.println("缓存命中__直接返回");
// 4、将读到的JSON字符串转为我们想要的
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference< Map<String, List<Catelog2Vo>>>(){});
return result;
}
/**
* 从数据库中查询并封装分类数据
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
// 方法一、用线程安全方法
// 只要是同一把锁,就能锁住这个锁的所有线程
// 1、synchronized (this):SpringBoot所有的组件在容器中都是单例的
// TODO 本地锁:synchronized,JUC(Lock),在分布式情况下想要锁住所有,必须使用分布式锁
synchronized (this) {
// 得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
// 如果缓存不为空,直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference< Map<String, List<Catelog2Vo>>>(){});
return result;
}
System.out.println("查询数据库");
/**
* 1、将数据库的多次查询变为一次
*/
List<CategoryEntity> selectList = baseMapper.selectList(null);
// 1、查出所有分类
List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);
// 2、封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 2.1、每一个一集分类,查到这个一集分类的所有二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
// 2.2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 2.3、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
if (level3Catelog!=null) {
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
// 封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
// 3、查到的数据放入缓存,将查出的对象转为json放在缓存中
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(parent_cid),1, TimeUnit.DAYS);
return parent_cid;
}
}
分布式锁原理与使用
问题:
setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决:
设置锁的自动过程,即使没有删除,会自动删除
问题:
setnx设置好,正要去设置过期时间,宕机,又成死锁了
解决:
设置过期时间和占位必须是原子的。redis支持使用 setnx ex命令
最终代码:
/**
* 从数据库中查询并封装分类数据(Redis分布式锁)
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
// 1、占分布式锁,去Redis占坑 设置过期时间,必须和加锁是同步的、原子的
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
// 2、加锁成功 ...执行业务
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
// 3、删锁
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),
Arrays.asList("lock"),uuid);
}
return dataFromDb;
} else {
// 加锁失败...重试
// 休眠100ms重试
System.out.println("获取分布式锁失败...等待重试");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式
}
}
https://github.com/redisson/redisson
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
本文我们仅关注分布式锁的实现,更多请参考官方文档
第一步、导入依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
第二步、编写配置类 RedissonClient
在 com/atguigu/gulimall/product/config
路径下
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
// 1、创建配置
Config config = new Config();
// 可以使用 "rediss://"来启用SSL安全连接
config.useSingleServer().setAddress("redis://124.222.223.222:6379");
// 2、根据Config创建出实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
分布式锁:github.com/redisson/redisson/wiki/8.-分布式锁和同步器
A调用B。AB都需要同一锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();
锁的续期:大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个**监控锁的看门狗,**它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
RLock
对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException
错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore
对象.
测试代码附上:
// 测试接口
@ResponseBody
@GetMapping("/hello")
public String hello() {
// 1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
// 2、加锁
//lock.lock(); // 阻塞式等待,默认加的锁都是30s时间
// 1)、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务长锁自动过期被删掉
// 2)、加锁的业务只要运行完成就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
lock.lock(10, TimeUnit.SECONDS); // 10s自动解锁,自动解锁时间一定要大于业务的执行时间
// lock.lock(10, TimeUnit.SECONDS); 锁时间到了以后不会自动续期
// 1、如果我们传递了锁的超时时间,就发送给redis执行脚本 进行占锁,默认超时就是我们指定的时间
// 2、如果我们未指定锁的超时时间,就是使用 30 * 1000【看门狗的超时间 lockWatchdogTimeout】
// 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是开门狗的默认时间】,每隔1/3的看门狗时间10s再次续期
// internallockLeaseTime【看门狗时间】/3,10s
// 最佳实战
// 指定锁的超时时间,省掉了整个续期操作。手动解锁
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
Thread.sleep(30000);
} catch (Exception e){
} finally {
// 3、解锁 假设解锁代码没有运行,redisson会不会出现死锁
lock.unlock();
System.out.println("释放锁..." + Thread.currentThread().getId());
}
return "hello";
}
java.util.concurrent.locks.Lock
接口的一种RLock
对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
使用方法都同上可重入锁一样,可重入锁是不公平锁!
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
另外Redisson还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();
测试代码:
@ResponseBody
@GetMapping("/write")
public String writeValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock rLock = lock.writeLock();
String s = "";
try {
// 1、该数据加写锁,读数据加读锁
rLock.lock();
System.out.println("写锁加锁成功..."+Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue", s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
@ResponseBody
@GetMapping("/read")
public String readValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
// 加读锁
RLock rLock = lock.readLock();
String s = "";
try {
rLock.lock();
System.out.println("读锁加锁成功..."+Thread.currentThread().getId());
Thread.sleep(30000);
s = (String) redisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
}
return s;
}
}
信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于0的话方法就会阻塞,直到数字大于0
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
测试代码:
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
//park.acquire(); // 获取一个信号,获取一个值, 占一个车位
boolean b = park.tryAcquire(); // 尝试获取一下,有就执行没就不执行
return "ok=>"+b;
}
@GetMapping("/go")
@ResponseBody
public String gp() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release(); // 释放一个车位
return "ok";
}
等待其他都完成之后,才关闭
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch
采用了与java.util.concurrent.CountDownLatch
相似的接口和用法。
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
测试代码:
/**
* 放假、锁门
* 1班没人了
* 5个班全部走完,我们才可以锁大门
*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await(); // 等待闭锁都完成
return "放假啦...";
}
@GetMapping("/gogo/{id}")
public String goGo(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown(); // 计数减1
return id+"班的人都走了...";
}
/**
* Redisson优化分布式锁
* @return
*/
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
// 1、锁的名字。锁的粒度,越细越快。
// 锁的粒度:具体缓存的是某个数据。比如:11号商品;product-11-lock
RLock lock = redisson.getLock("catalogJson-lock");
lock.lock();
Map<String, List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally {
lock.unlock();
}
return dataFromDb;
}
这个时候来解决我们一直思考的问题:缓存里面的数据如何和数据库保持一致
问题:缓存里面的数据如何和数据库保持一致
缓存数据一致性
双写模式 :修改数据库的时候同时修改缓存中的数据
失效模式 :袖该数据库的时候删除缓存中的数据,等待下次主动查询进行更新
缓存数据一致性——解决方案
本系统的一致性解决方案:
1、缓存的所有数据都有过期时间,数据过期下次查询触发主动更新
2、读写数据的时候,加上分布式的读写锁。(经常读写会有影响、偶尔写经常读没多大影响)
SpringCache简介
org.springframework.cache.Cache
和 org.springframework.cache.CacheManager
接口来统一不同的缓存技术;并支持使用 JCache(JSR-107)注解简化我们开发;第一步、引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
第二步、加入配置
spring:
cache:
type: redis #指定缓存类型为redis
redis:
time-to-live: 3600000 # 指定缓存的数据的存活时间(毫秒为单位)
key-prefix: CACHE_ # 设置key的前缀,用来区分和reids其他键不同的.如果制定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
use-key-prefix: true # 设置是否使用前缀
cache-null-values: true # 是否缓存空值,防止缓存穿透
2.1、自动配置了哪些
CacheAutoCOnfiguration 会导入 RedisCacheConfiguration
RedisCacheConfiguration :自动配好了缓存管理器(RedisCacheManager)
2.2、我们需要配置哪些
配置使用redis作为缓存
第三步、给主类加上开启缓存注解
@EnableCaching
@EnableCaching
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@EnableDiscoveryClient
@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
@Cacheable
: 触发缓存填充(将数据保存到缓存的操作) `@CacheEvict` : 触发缓存逐出(将数据从缓存中删除的操作)
`@CachePut` : 在不干扰方法执行的情况下更新缓存(不影响方法执行更新缓存)
`@Caching` : 重新组合要应用于一个方法的多个缓存操作(组合以上多个操作)
`@CacheConfig` : 在类级别共享一些常见的缓存相关设置(在类级别共享缓存的相同配置)
给查找所有的一级分类方法加上@Cacheable,将该数据保存到缓存中去
@Cacheable
查找所有的一级分类
3、默认行为
4、自定义
指定生成的缓存使用的key key属性指定,接收一个SpEL SpEL的详细语法
指定缓存的数据的存活时间 在配置文件中指定(spring.cache.redis.time-to-live=num毫秒)
将数据保存为json格式 需要自定义缓存配置
@Cacheable(value = {"category"}, key = "#root.methodName")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys...");
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}
测试,之后每次刷新首页都不再进入getLevel1Categorys()方法了!
原理:
CacheConfigurations 缓存的自动配置帮我们导入了 -> RedisCacheConfiguration
-> 自动配置了RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置
->如果 redisCacheConfiguration 有就有已有的,没有就用默认配置
-> 想改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration 即可
-> 就会应用到当前缓存 RedisCacheManager 管理的所有缓存分区中
编写自定义的缓存机制
默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类
com.atguigu.gulimall.product.config
包下
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@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;
}
}
触发缓存逐出(将数据从缓存中删除的操作)
@CacheEvict
:缓存失效模式(当修改数据库的时候删除指定key的缓存)@Caching
:同时进行多个缓存操作
@CacheEvict(value = "category", allEntries = true)
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
// 双写
// 失效:redis.del("catalogJSON"); 等待下次主动查询进行更新
}
/**
* 修改(从数据库中查询并封装分类数据)的方法【SpringCache】
* @return
*/
@Cacheable(value = "category",key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
System.out.println("进行查询数据库");
List<CategoryEntity> selectList = baseMapper.selectList(null);
List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
// 2、封装数据
Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 2.1、每一个一集分类,查到这个一集分类的所有二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
// 2.2、封装上面的结果
List<Catelog2Vo> catelog2Vos = null;
if (categoryEntities != null) {
catelog2Vos = categoryEntities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
// 2.3、找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
if (level3Catelog != null) {
List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
// 封装成指定格式
Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catelog3Vo;
}).collect(Collectors.toList());
catelog2Vo.setCatalog3List(collect);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
return parent_cid;
}
1)、读模式
2)、写模式:(缓存与数据库一致)
原理:
总结: