缓存与分布式锁
缓存与分布式锁
一、缓存
1、缓存使用
为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。
哪些数据适合放入缓存?
- 即时性、数据一致性要求不高的
- 访问量大且更新频率不高的数据(读多,写少)
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。
2、整合 redis 作为缓存
- 引入 redis-starter
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
-
配置 redis
spring: redis: host: 192.168.163.131 port: 6379
-
使用 RedisTemplate 操作 redis
@Autowired StringRedisTemplate stringRedisTemplate; @Test public void teststringRedisTemplate(){ ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); ops.set("1", "2" + UUID.randomUUID().toString()); System.out.println(ops.get("1")); }
-
给业务中加入缓存
@Autowired private StringRedisTemplate redisTemplate; //TODO 产生堆外内存溢出:OutOfDirectMemoryError //1).sprignboot2.0以后默认是使用lettuce作为操作redis的客户端,它使用netty进行网络通信 //2).lettuce的bug导致netty堆外内存溢出,netty如果没有指定堆外内存,默认使用-xmx默认的内存,没有及时释放内存 // 可以通过-Dio.netty.maxDirectMemory进行设置 //解决方案:不能通过-Dio.netty.maxDirectMemory只去调大内存 // 1)升级lettuce客户端 // 2)切换使用jedis //缓存中拿数据 @Override public Map<String, List<Catelog2Vo>> getCatelogJson() { //1.加入缓存逻辑,缓存中存的数据是json字符串,还用逆转为能用的对象类型【序列号与反序列化】 //json跨语言,跨平台兼容 String catelogJson = redisTemplate.opsForValue().get("catelogJson"); if (StringUtils.isEmpty(catelogJson)) { //2.缓存中没有,查询数据库 Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb(); //3.查到的数据再放入缓存 String string = JSON.toJSONString(catelogJsonFromDb); redisTemplate.opsForValue().set("catelogJson", string); return catelogJsonFromDb; } //转为我们指定的对象 Map<String, List<Catelog2Vo>> parseObject = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() { }); return parseObject; } //加入缓存前从数据库查询并封装分类数据 // @Override public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() { List<CategoryEntity> entityList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1 = getCategoryEntities(entityList, 0L); Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 拿到每一个一级分类 然后查询他们的二级分类 List<CategoryEntity> entities = getCategoryEntities(entityList, v.getCatId()); List<Catelog2Vo> catelog2Vos = null; if (entities != null) { catelog2Vos = entities.stream().map(l2 -> { Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null); // 找当前二级分类的三级分类 List<CategoryEntity> level3 = getCategoryEntities(entityList, l2.getCatId()); // 三级分类有数据的情况下 if (level3 != null) { List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList()); catelog2Vo.setCatalog3List(catalog3Vos); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); return parent_cid; }
3、堆外内存溢出异常
这里可能会产生堆外内存溢出异常:OutOfDirectMemoryError。
下面进行分析:
-
SpringBoot 2.0 以后默认使用 lettuce 作为操作 redis 的客户端,它使用 netty 进行网络通信;
-
lettuce 的 bug 导致 netty 堆外内存溢出;
-
netty 如果没有指定堆外内存,默认使用 -Xmx 参数指定的内存;
-
可以通过 -Dio.netty.maxDirectMemory 进行设置;
解决方案:不能只使用 -Dio.netty.maxDirectMemory 去调大堆外内存,这样只会延缓异常出现的时间。
-
升级 lettuce 客户端,或使用 jedis 客户端
<!-- redis --> <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>
二、缓存失效问题
先来解决大并发读情况下的缓存失效问题;
1、缓存穿透
缓存穿透是指 查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方法:缓存空结果、并且设置短的过期时间。
2、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
解决方法:原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
3、缓存击穿
对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿。
解决方法:加锁。大量并发只让一个人去查,其他人等待,查到之后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去查数据库。
如:单体应用中加锁
//缓存中拿数据
@Override
public Map<String, List<Catelog2Vo>> getCatelogJson() {
//1.加入缓存逻辑,缓存中存的数据是json字符串,还用逆转为能用的对象类型【序列号与反序列化】
//json跨语言,跨平台兼容
/**
* 1.空结果缓存,解决缓存穿透
* 2.设置过期时间,加随机值,解决缓存雪崩
* 3.加锁,解决缓存击穿
**/
String catelogJson = redisTemplate.opsForValue().get("catelogJson");
if (StringUtils.isEmpty(catelogJson)) {
//2.缓存中没有,查询数据库
Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDb();
//3.查到的数据再放入缓存 这个操作再getCatelogJsonFromDb()完成
return catelogJsonFromDb;
}
//转为我们指定的对象
Map<String, List<Catelog2Vo>> parseObject = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return parseObject;
}
//加入缓存前从数据库查询并封装分类数据
// @Override
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程
//1).synchronized (this),SpringBoot所有的组件在容器中都是单例的
//TODO 本地锁,synchronized (this),JUC(lock),在分布式情况下,想要锁住所有,我们必须使用分布式锁
synchronized (this) {
//得到锁以后,再去缓存中确定一次,如果有了就不用查数据库了
String catelogJson = redisTemplate.opsForValue().get("catelogJson");
if (!StringUtils.isEmpty(catelogJson)) {
Map<String, List<Catelog2Vo>> parseObject = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return parseObject;
}
List<CategoryEntity> entityList = baseMapper.selectList(null);
// 查询所有一级分类
List<CategoryEntity> level1 = getCategoryEntities(entityList, 0L);
Map<String, List<Catelog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
// 拿到每一个一级分类 然后查询他们的二级分类
List<CategoryEntity> entities = getCategoryEntities(entityList, v.getCatId());
List<Catelog2Vo> catelog2Vos = null;
if (entities != null) {
catelog2Vos = entities.stream().map(l2 -> {
Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null);
// 找当前二级分类的三级分类
List<CategoryEntity> level3 = getCategoryEntities(entityList, l2.getCatId());
// 三级分类有数据的情况下
if (level3 != null) {
List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList());
catelog2Vo.setCatalog3List(catalog3Vos);
}
return catelog2Vo;
}).collect(Collectors.toList());
}
return catelog2Vos;
}));
String string = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catelogJson", string,1, TimeUnit.DAYS);
return parent_cid;
}
}
本地锁,上述中查数据库和设置缓存应是原子操作
三、分布式锁
本地锁只能锁住当前服务的进程,每一个单独的服务都会有一个进程读取数据库,不能达到只读取依次数据库的效果,所以需要分布式锁。
分布式锁的实现
使用 Redis 作为分布式锁
redis 中有一个 SETNX 命令,该命令会向 redis 中保存一条数据,如果不存在则保存成功,存在则返回失败。
我们约定保存成功即为加锁成功,之后加锁成功的线程才能执行真正的业务操作。
//加入缓存前从数据库查询并封装分类数据,使用redis占坑实现分布式锁
public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock() {
//1.占分布式锁, 去redis占坑
//2.设置过期时间(必须和加锁是同步的,院子的),避免删除锁的时候宕机造成死锁
String uuid = UUID.randomUUID().toString();//使用随机避免删了别人的锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if (lock) {
// 加锁成功...执行业务
Map<String, List<Catelog2Vo>> fromDb;
try {
fromDb = getFromDb();
} finally {
//获取值对比和对比成功删除要是原子操作
// String lock1 = redisTemplate.opsForValue().get("lock");
/* ①原方式 if (uuid.equals(lock1)) {
//删除我自己的锁
redisTemplate.delete("lock");//删除锁
}*/
//lua 脚本解锁
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
// ②改进方式,删除锁
Long lock2 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return fromDb;
} else {
//加锁失败。。。重试 synchronize()
//休眠100ms重试
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatelogJsonFromDbWithRedisLock();//自旋的方式
}
}
Redisson 作为分布式锁
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-MemoryDataGrid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
官方文档:https://github.com/redisson/redisson/wiki
- 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
- 配置 redisson
/**
* @author chenfl
* @create 2022-02-28-19:33
*/
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;
/**
* @author chenfl
* @description redisson配置
* @date 2022/2/28 19:33
*/
@Configuration
public class MyRedissonConfig {
/**
* @Author chenfl
* @Description //所有对redisson的使用都要通过 RedissonClient 对象
* @Date 19:36 2022/2/28
* @Param []
* @return org.redisson.api.RedissonClient
**/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//1.创建配置
//Redis url should start with redis:// or rediss://
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
//2.根据Config创建出RedissonClient
return Redisson.create(config);
}
}
- 使用
public String hello() {
// 1. 获取一把锁,只要锁的名字是一样,就是同一把锁
RLock mylock = redisson.getLock("my-lock");
// 2. 加锁,阻塞式等待
/**
* 方式一加锁
* 1).锁的自动续期,如果业务超长,运行期间自动给锁续上30s,不用担心业务时间长,锁自动过期被删除
* 2).加锁的业务只要运行完成,就不会给当前的锁续期,及时不手动解锁
**/
// mylock.lock();
/**
* 方式二加锁
* 10s自动解锁后,自动解锁时间一定要大于业务的执行时间。
* 问题:在锁时间到了以后,不会自动续期
* 1.如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
* 2.如果我们未指定锁的超时时间,就使用30*1000【lockWatchdogTimeout看门狗的默认时间】
* 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】 每隔10s都会再次续期,续成满时间如当前是30s
* internalLockLeaseTime【看门狗时间】/3......10s
* 、、
* //最佳实现
* 1)mylock.lock(10, TimeUnit.SECONDS);省掉了整个续期的操作,手动解锁
**/
mylock.lock(10, TimeUnit.SECONDS);
try {
System.out.println("加锁成功,执行业务。。。" + Thread.currentThread().getId());
//休眠方便测试
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 解锁,假设解锁代码没有运行,Redisson 会出现死锁吗?(不会)
System.out.println("释放锁。。。" + Thread.currentThread().getId());
mylock.unlock();
}
return "hello";
}
- 锁的自动续期,如果业务时间很长,运行期间自动给锁续期 30 s,不用担心业务时间过长,锁自动过期被删掉;
- 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动续期,默认也会在 30 s 后解锁;
读写锁
//保证一定能读到最新的数据。修改期间,写锁是一个排它锁(互斥锁,独享锁),读锁是一个共享锁
//读+读 相当于无所,并发读,只会在redis中记录好,所有当前的读锁,他们都会同事加锁成功
//写+读 写锁没释放,读就必须等待
//写+写 阻塞方式
//读+写 有读锁,写也需要等待
//只要有写的存在,都必须等待
@ResponseBody
@GetMapping("/write")
public String write() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String s = "";
RLock rLock = readWriteLock.writeLock();
try {
System.out.println("写锁加锁成功" + Thread.currentThread().getId());
//1.改数据加写锁,读数据加读锁
rLock.lock();
s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("writeValue", s);
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rLock.unlock();
System.out.println("写锁释放" + Thread.currentThread().getId());
}
return s;
}
@ResponseBody
@GetMapping("/read")
public String read() {
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
String writeValue = null;
//加读锁
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
System.out.println("读锁加锁成功" + Thread.currentThread().getId());
writeValue = redisTemplate.opsForValue().get("writeValue");
TimeUnit.SECONDS.sleep(30);
} catch (Exception e) {
e.printStackTrace();
} finally {
rLock.unlock();
System.out.println("读锁释放" + Thread.currentThread().getId());
}
return writeValue;
}
闭锁
/**
* @Author chenfl
* @Description //放假,锁门
* 1班没人了。。。2班。。。5个班全部走完,我们可以锁大门
* @Date 9:49 2022/3/1
* @Param []
* @return java.lang.String
**/
@ResponseBody
@GetMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await();//等待闭锁都完成
return "放假了。。。";
}
@ResponseBody
@GetMapping("/gogogo/{id}")
public String gogogo(@PathVariable("id") long id) {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计数减一
return id + "班的人都走了";
}
信号量
/**
* @return
* @Author chenfl
* @Description //车库停车
* 3个车位
* 信号量也可以用作分布式限流
* @Date 9:58 2022/3/1
* @Param
**/
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
// park.acquire();//获取一个信号,获取一个值,占一个车位
boolean b = park.tryAcquire();//尝试获取
if (b) {
//执行业务
} else {
return "当前流量过大,请稍等";
}
return "ok=>" + b;
}
@ResponseBody
@GetMapping("/go")
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();//释放一个信号
return "ok";
}
修改代码
/**
* 缓存里的数据如何和数据库的数据保持一致??
* 缓存数据一致性
* 1)、双写模式
* 2)、失效模式
*
* @return
*/
public Map<String, List<Catalogs2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//1、占分布式锁。去redis占坑
//(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
//RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
//创建读锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
RLock rLock = readWriteLock.readLock();
Map<String, List<Catalogs2Vo>> dataFromDb = null;
try {
rLock.lock();
//加锁成功...执行业务
dataFromDb = getCatalogJsonFromDB();
} finally {
rLock.unlock();
}
return dataFromDb;
}
缓存数据一致性-解决方案
-
无论是双写模式还是失效模式,都会导致缓存的不一致问题,即多个实例同时更新会出事。怎么办?
- 如果是用户纬度数据(订单数据,用户数据),这种并发率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求
- 通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓,所以适合使用读写锁、(业务不关心脏数据,允许临时脏数据可忽略)
-
总结
- 我们能放入缓存的数据本就不应该是实时性,一致性要求超高的,所有缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可
- 我们不应该福过度设计,增加系统的复杂度
- 遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点
缓存 - SpringCache
简介
- Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化我们开发;
- Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
- 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
- 使用Spring缓存抽象时我们需要关注以下两点;
- 确定方法需要被缓存以及他们的缓存策略
- 从缓存中读取之前缓存存储的数据
基础概念
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
添加配置
自动配置了:
- CacheAutoConfiguration 会导入RedisCacheConfiguration;
- 会自动装配缓存管理器 RedisCacheManager;
手动配置:
#配置使用redis作为缓存
spring.cache.type=redis
#spring.cache.cache-names=qq,
#毫秒为单位
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
常用注解
-
@Cacheable :触发将数据保存到缓存的操作;
-
@CacheEvict : 触发将数据从缓存删除的操作;失效模式
-
//指定删除某个分区下的所有缓存 //存储同一类型的数据,都可以指定成同一个分区,分区名默认就是存储的前缀(配置文件不指定的话) @CacheEvict(value="category",allEntries = true) @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); }
-
//指定分区下哪个key的缓存 @CacheEvict(value = "category",key = "'getLevel1Categorys'") @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); }
-
-
@CachePut :不影响方法执行更新缓存;双写模式
-
@Cacheing:组合以上多个操作;
-
@Caching(evict = { @CacheEvict(value = "category",key = "'getLevel1Categorys'"), @CacheEvict(value = "category",key = "'getCatelogJson'")}) @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); }
-
-
@CacheConfig:在类级别共享缓存的相同配置;
业务实现
-
开启缓存功能 @EnableCaching
-
只需要使用注解就能完成缓存操作
-
每一个需要缓存的数据我们都来指定要放到哪个名字的缓存【缓存的分区(按照业务类型分)】 @Cacheable({"category"})
-
@Cacheable 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
-
默认行为
-
如果缓存中有,方法不再调用
-
缓存的value值,默认使用jdk序列化机制,将序列化后的数据存到redis中
-
默认ttl时间是 -1:
-
自定义操作:key的生成
-
指定生成缓存的key:key属性指定,接收一个 SpEl
-
@Cacheable(value = {"category"},key = "'level1Categorys'")或者@Cacheable(value = {"category"},key = "#root.method.name")
-
-
指定缓存的数据的存活时间:配置文档中修改存活时间 ttl,毫秒为单位
spring.cache.redis.time-to-live=60000spring.cache.redis.time-to-live=60000
-
将数据保存为json格式: 自定义配置类MyCacheManager
-
package com.atguigu.gulimall.product.config;/** * @author chenfl * @create 2022-03-01-15:48 */ 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.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author chenfl * @description 缓存配置 * @date 2022/3/1 15:48 */ @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { /** * 配置文件的配置没有用上 * 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache") * public class CacheProperties * <p> * 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class) */ @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // config = config.entryTtl(); 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; } }
-
-
-
-
原理:CacheAutoConfiguration -> RedisCacheConfiguration-> RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置-> 如果redisCacheConfiguration有就用自己的,没有就用默认配置 ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可->就会应用到当前RedisCacheManager管理的所有缓存分区中
-
Spring-Cache的不足之处:
- 读模式
- 缓存穿透:查询一个null数据。解决方案:缓存空数据
- 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用@Cacheable(sync = true)来解决击穿问题
- 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间(spring.cache.redis.time-to-live=3600000)
- 写模式:(缓存与数据库一致)
- 读写加锁。
- 引入Canal,感知到MySQL的更新去更新Redis
- 读多写多,直接去数据库查询就行
- 总结
-
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
config = config.disableKeyPrefix();
}
return config;
}
}
-
- 读模式
-
原理:CacheAutoConfiguration -> RedisCacheConfiguration-> RedisCacheManager -> 初始化所有的缓存 -> 每个缓存决定使用什么配置-> 如果redisCacheConfiguration有就用自己的,没有就用默认配置 ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可->就会应用到当前RedisCacheManager管理的所有缓存分区中
-
Spring-Cache的不足之处:
- 读模式
- 缓存穿透:查询一个null数据。解决方案:缓存空数据
- 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用@Cacheable(sync = true)来解决击穿问题
- 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间(spring.cache.redis.time-to-live=3600000)
- 写模式:(缓存与数据库一致)
- 读写加锁。
- 引入Canal,感知到MySQL的更新去更新Redis
- 读多写多,直接去数据库查询就行
- 总结
- 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
- 特殊数据:特殊设计
- 读模式