首先需要安装Redis,如何安装可以看我的这篇文章
接下来说明如何使用,以及一些Redis的相关知识。
哪些数据适合放入缓存?
本地缓存:和微服务同一个进程。缺点:分布式时本地缓存不能共享
分布式缓存:缓存中间件
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
配置redis主机地址
spring:
redis:
host: xxx(你的ip)
port: 6379
public class RedisTests{
@Autowired
StringRedisTemplate stringRedisTemplate;
public void testStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("hello", "world_" + UUID.randomUUID().toString());
// 查询
String hello = ops.get("hello");
System.out.println(hello);
}
}
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
private Map<String, List<Catalog2Vo>> getCatalogJson(){
// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON )){
// 2. 缓存中没有,查询数据库
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// 3. 查到的数据再放入缓存,将对象转为json放入缓存中
// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON", s);
return catalogJsonFromDb;
}
// 转为指定的对象
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
// 从数据库查询并封装分类数据
private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
//遍历查找出二级分类
List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
List<Catalog2Vo> catalog2Vos=null;
if (level2Categories!=null){
//封装二级分类到vo并且查出其中的三级分类
catalog2Vos = level2Categories.stream().map(cat -> {
//遍历查出三级分类并封装
List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (level3Catagories != null) {
catalog3Vos = level3Catagories.stream()
.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
.collect(Collectors.toList());
}
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
return listMap;
}
}
缓存穿透:查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
风险:利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃。
解决:缓存空对象、布隆过滤器、mvc拦截器
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案:
规避雪崩:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中。
设置热点数据永远不过期。
出现雪崩:降级 熔断
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
缓存雪崩和缓存击穿不同的是:
缓存击穿 指 并发查同一条数据。缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
设置热点数据永远不过期。
加互斥锁:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db去数据库加载,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
缓存击穿:加锁
不好的方法是synchronized(this),肯定不能这么写 ,不具体写了。
锁时序问题:之前的逻辑是查缓存没有,然后取竞争锁查数据库,这样就造成多次查数据库。
解决方法:竞争到锁后,再次确认缓存中没有,再去查数据库。
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
private Map<String, List<Catalog2Vo>> getCatalogJson(){
// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
/*
1. 空结果缓存:解决缓存穿透
2. 设置过期时间(加随机值):解决缓存雪崩
3. 加锁:解决缓存击穿
*/
// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON )){
// 2. 缓存中没有,查询数据库
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
return catalogJsonFromDb;
}
// 转为指定的对象
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
// 从数据库查询并封装分类数据
private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){
// 在非分布式的场景下,只要是同一把锁,就能锁住需要这个锁的所有线程
// synchronized(this): SpringBoot所有的组件在容器中都是单例的。
synchronized(this){
// 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(!StringUtils.isEmpty(catalogJSON)){
// 如果不为null,直接返回
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
//遍历查找出二级分类
List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
List<Catalog2Vo> catalog2Vos=null;
if (level2Categories!=null){
//封装二级分类到vo并且查出其中的三级分类
catalog2Vos = level2Categories.stream().map(cat -> {
//遍历查出三级分类并封装
List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (level3Catagories != null) {
catalog3Vos = level3Catagories.stream()
.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
.collect(Collectors.toList());
}
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
// 3. 查到的数据再放入缓存,将对象转为json放入缓存中
// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
String s = JSON.toJSONString(listMap);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return listMap;
}
}
}
分布式锁
分布式项目时,但本地锁只能锁住当前服务,需要分布式锁。
redis分布式锁的原理:setnx,同一时刻只能设置成功一个。
前提:锁的key是一定的,value可以变。
没获取到锁阻塞或者sleep一会,设置好了锁,玩意服务出现宕机,没有执行删除锁逻辑,这就造成了死锁
解决:设置过期时间业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁。
解决:锁续期(redisson有看门狗)。删锁的时候明确是自己的锁。如uuid判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁。
解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的。
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
private Map<String, List<Catalog2Vo>> getCatalogJson(){
// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
/*
1. 空结果缓存:解决缓存穿透
2. 设置过期时间(加随机值):解决缓存雪崩
3. 加锁:解决缓存击穿
*/
// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(StringUtils.isEmpty(catalogJSON )){
// 2. 缓存中没有,查询数据库
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
return catalogJsonFromDb;
}
// 转为指定的对象
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
// 从数据库查询并封装分类数据
private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock(){
// 1) 占分布式锁
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
if(lock){
// 加锁成功
// 2) 设置过期时间,必须和加锁是同步的,原子性的
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally{
// get与delete的原子操作
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";
Long lockValue = stringRedisTemplate.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{
// 加锁失败
// 休眠 100ms
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式
}
return getDataFromDb();
}
private Map<String, List<Catalog2Vo>> getDataFromDb(){
// 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(!StringUtils.isEmpty(catalogJSON)){
// 如果不为null,直接返回
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> {
//遍历查找出二级分类
List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
List<Catalog2Vo> catalog2Vos=null;
if (level2Categories!=null){
//封装二级分类到vo并且查出其中的三级分类
catalog2Vos = level2Categories.stream().map(cat -> {
//遍历查出三级分类并封装
List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (level3Catagories != null) {
catalog3Vos = level3Catagories.stream()
.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
.collect(Collectors.toList());
}
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
// 3. 查到的数据再放入缓存,将对象转为json放入缓存中
// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
String s = JSON.toJSONString(listMap);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return listMap;
}
}
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>redisson-spring-boot-starterartifactId>
<version>3.15.6version>
dependency>
application.yml
spring:
application:
name: springboot-redisson
redis:
redisson:
file: classpath:redisson.yml
redisson.yml
# 单节点配置
singleServerConfig:
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 10000
# 连接超时,单位:毫秒
connectTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
# 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒
retryInterval: 1500
# # 重新连接时间间隔,单位:毫秒
# reconnectionTimeout: 3000
# # 执行失败最大次数
# failedAttempts: 3
# 密码
password: 1234
# 单个连接最大订阅数量
subscriptionsPerConnection: 5
# 客户端名称
clientName: null
# # 节点地址
address: "redis://127.0.0.1:6379"
# 发布和订阅连接的最小空闲连接数
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
# 最小空闲连接数
connectionMinimumIdleSize: 500
# 连接池大小
connectionPoolSize: 1000
# 数据库编号
database: 0
# DNS监测时间间隔,单位:毫秒
dnsMonitoringInterval: 5000
# 线程池数量,默认值: 当前处理核数量 * 2
threads: 16
# Netty线程池数量,默认值: 当前处理核数量 * 2
nettyThreads: 32
# 编码,不使用默认编码,因为set进去之后是乱码
#codec: ! {}
# 传输模式
transportMode : "NIO"
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootRedissonApplicationTests {
@Autowired
private RedissonClient redissonClient;
@Test
void contextLoads() {
redissonClient.getBucket("hello").set("bug");
String test = (String) redissonClient.getBucket("hello").get();
System.out.println(test);
}
}
具体使用方式可以查看官网,有详细的说明
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Autowired
CategoryBrandRelationService categoryBrandRelationService;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
RedissonClient redisson;
@Override
private Map<String, List<Catalog2Vo>> getCatalogJson() {
// 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。
/*
1. 空结果缓存:解决缓存穿透
2. 设置过期时间(加随机值):解决缓存雪崩
3. 加锁:解决缓存击穿
*/
// 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
// 2. 缓存中没有,查询数据库
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();
return catalogJsonFromDb;
}
// 转为指定的对象
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
/*
* 缓存里面的数据如何和数据库保持一致
* 缓存数据的一致性
* 1)双写模式:写完数据库后,马上写到缓存里。
* 缺点:脏数据问题,这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
* 2)失效模式:写完数据库后,马上删掉缓存的数据。
* 缺点:也会产生脏数据问题。
* 解决办法:1. 对于实时性要求高的数据,直接读数据库。
* 2. 使用读写锁(业务不关心脏数据,允许临时脏数据可忽略)
* 3. 缓存数据一致性-解决-Canal
*/
private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
// 1. 锁的名字。(锁的粒度,越细越快)
// 锁的粒度:具体缓存的是某个数据,例11号商品:product-11-lock
redisson.getLock("catalogJson-lock");
lock.lock();
Map<String, List<Catalog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
} finally {
lock.unlock();
}
return dataFromDb;
}
private Map<String, List<Catalog2Vo>> getDataFromDb() {
// 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (!StringUtils.isEmpty(catalogJSON)) {
// 如果不为null,直接返回
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() {
});
return result;
}
List<CategoryEntity> categoryEntities = this.list();
//查出所有一级分类
List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L);
Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//遍历查找出二级分类
List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId());
List<Catalog2Vo> catalog2Vos = null;
if (level2Categories != null) {
//封装二级分类到vo并且查出其中的三级分类
catalog2Vos = level2Categories.stream().map(cat -> {
//遍历查出三级分类并封装
List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId());
List<Catalog2Vo.Catalog3Vo> catalog3Vos = null;
if (level3Catagories != null) {
catalog3Vos = level3Catagories.stream()
.map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName()))
.collect(Collectors.toList());
}
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos);
return catalog2Vo;
}).collect(Collectors.toList());
}
return catalog2Vos;
}));
// 3. 查到的数据再放入缓存,将对象转为json放入缓存中
// 使用alibaba的fastjson包,可以将任意对象转换为json字符串
String s = JSON.toJSONString(listMap);
redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
return listMap;
}
}
首先理一下:
每次都那样写缓存太麻烦了,spring从3.1开始定义了org.springframework.cache.Cache org.springframework.cache.CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发。
Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等。
每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
在主类上加上注解
@EnableCaching
在方法上注解:代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。反之则调用方法,最后将方法的结果放入缓存。
每一个需要缓存的数据都来指定要放到哪个名字的缓存。(缓存的分区 [按照业务类型分] )
默认行为:
@Cacheable({"category"})
@Override
public List<CategoryEntity> getLevel1Categorys(){
List<CategoryEntity> categoryEntities = baseMapper.selectList();
return categoryEntities;
}
自定义:
@Cacheable({"category"}, key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys(){
List<CategoryEntity> categoryEntities = baseMapper.selectList();
return categoryEntities;
}
@Configuration
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
//指定缓存序列化方式为json
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
//设置配置文件中的各项配置,如过期时间
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;
}
}
第一个方法存放缓存,第二个方法清空缓存
// 调用该方法时会将结果缓存,缓存名为category,key为方法名
// sync表示该方法的缓存被读取时会加锁 // value等同于cacheNames // key如果是字符串"''"
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
return getCategoriesDb();
}
//调用该方法会删除缓存category下的所有cache,如果要删除某个具体,用key="''"
@Override
@CacheEvict(value = {"category"},allEntries = true)
public void updateCascade(CategoryEntity category) {
this.updateById(category);
if (!StringUtils.isEmpty(category.getName())) {
categoryBrandRelationService.updateCategory(category);
}
}
如果要清空多个缓存,用@Caching(evict={@CacheEvict(value=“”)})
1)读模式
缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
2) 写模式:(缓存与数据库一致)
读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行
3)总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)