十一、缓存和分布式锁

【摘要】缓存和分布式锁

前言

151、缓存-缓存使用-本地缓存与分布式缓存

缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而db承担数据落盘工作。

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的。
  • 访问量大且更新频率不高的数据(读多,写少)。

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要5分钟才能看到新的商品一般还是可以接受的。

1
2
3
4
5
6
data = cache.load(id);//从缓存中加载数据
if(data==null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到cache中
}
return data;

注意:在开发中,凡是放入缓存中的数据我们应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务奔溃导致我们的数据永久不一致问题。

这种本地缓存每个实例都有自己的缓存,可能会出现数据不一致的情况。同时本地缓存还会占用堆内存,影响垃圾回收、影响系统性能。

所以我们需要使用分布式缓存,不应该把缓存放在每一个微服务的进程中。常用的缓存中间件是Redis。使用缓存中间件还可以无限扩容。

152、缓存-缓存使用-整合redis测试

引入Reids。(pom.xml)

1
2
3
4
5
<!-- 引入Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置Redis。(application.yml)

1
2
3
4
5
spring:
redis:
host: 127.0.0.1
port: 6379
password: redis

ZheliProductApplicationTests.java

1
2
3
4
5
6
7
8
9
10
11
12
13
@Autowired
private StringRedisTemplate stringRedisTemplate;

@Test
public void testStringRedisTemplate(){
//hello world
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
//保存
ops.set("hello","world_"+ UUID.randomUUID().toString());
//查询
String hello = ops.get("hello");
System.out.println("之前保存的数据是:"+hello);
}

控制台打印

1
之前保存的数据是:world_0a678d76-d227-4c10-807e-00d6320b01ce

153、缓存-缓存使用-改造三级分类业务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Autowired
private StringRedisTemplate redisTemplate;

@Override
public 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>> catelogJsonFromDb = getCatalogJsonFromDb();
//3.查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(catelogJsonFromDb);
redisTemplate.opsForValue().set("catalogJson",s);
}
//转为我们指定的对象
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}

154、缓存-缓存使用-压力测试出的内存泄露及解决

  • 1.springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。

  • 2.lettuce的bug导致netty堆外内存溢出-Xmx300m:netty如果没有指定堆外内存,默认使用-Xmx300m

  • 解决方案:可以通过-Dio.netty.maxDirectMemory只去调大堆外内存。
    (1).升级lettuce客户端。 (2).切换使用jedis。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <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>
  • redisTemplate:
    lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate。

缓存-缓存使用-缓存击穿、穿透、雪崩

缓存穿透: 指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是 数据库也无此记录,我们没有将这次查询的null写入缓存,这将导致这个不 存在的数据每次请求都要到存储层去查询,失去了缓存的意义

风险: 利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

解决: null结果缓存,并加入短暂过期时间

缓存雪崩: 缓存雪崩是指在我们设置缓存时key采用了相同的过期时间, 导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时 压力过重雪崩。
解决: 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这 样每一个缓存的过期时间的重复率就会降低,就很难引发集体 失效的事件。

缓存穿透: 对于一些设置了过期时间的key,如果这些key可能会在某些 时间点被超高并发地访问,是一种非常“热点”的数据。如果这个key在大量请求同时进来前正好失效,那么所有对 这个key的数据查询都落到db,我们称为缓存击穿。
解决: 加锁大量并发只让一个去查,其他人等待,查到以后释放锁,其他 人获取到锁,先查缓存,就会有数据,不用去db

数据穿透:查询一个不存在的数据。缓存null。

数据雪崩:大面积数据同时失效。设置随机过期时间。

数据击穿:大量请求时正好失效。加锁。

缓存-缓存使用-加锁解决缓存击穿问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Override
public Map<String, List<Catalog2Vo>> getCatalogJson(){
/**
* 1.空结果缓存:解决缓存穿透。
* 2.设置过期时间(加随机值):解决缓存雪崩。
* 3.加锁:解决缓存击穿。
*/
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if(StringUtils.isEmpty(catalogJson)){
System.out.println("缓存不命中,将要查询数据库......");
Map<String, List<Catalog2Vo>> catelogJsonFromDb = getCatalogJsonFromDb();
return catelogJsonFromDb;
}
System.out.println("缓存命中,直接返回......");
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() {
//只要是同一把锁,就能锁住需要这个锁的所有线程。
//1.synchronized (this):SpringBoot所有的组件在容器中都是单例的。
//TODO 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁。
synchronized (this){
//得到锁以后,我们应该再去缓存中确认一次,如果没有才需要继续查询。
String catalogJson = redisTemplate.opsForValue().get("catalogJson");
if(!StringUtils.isEmpty(catalogJson)){
//缓存不为空,之间返回
Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJson,new TypeReference<Map<String, List<Catalog2Vo>>>(){});
return result;
}
System.out.println("查询了数据库......");
/**
* 1.将数据库的多次查询变成一次
*/
List<CategoryEntity> selectList = baseMapper.selectList(null);

//1.查出所有一级分类
List<CategoryEntity> leve1Categorys = getParent_cid(selectList,0L);//getLeve1Categorys();
//2.封装数据
Map<String, List<Catalog2Vo>> parent_cid = leve1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
//1.每一个的一级分类。查到这个一级分类的二级分类
List<CategoryEntity> categoryEntities = getParent_cid(selectList,v.getCatId());
//2.封装上面的结果
List<Catalog2Vo> catalog2Vos = null;
if (categoryEntities != null) {
catalog2Vos = categoryEntities.stream().map(l2 -> {
Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
//1.找当前二级分类的三级分类封装成vo
List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
if(level3Catelog!=null){
List<Catalog2Vo.Catalog3Vo> collect = level3Catelog.stream().map(l3 -> {
Catalog2Vo.Catalog3Vo catalog3Vo = new Catalog2Vo.Catalog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
return catalog3Vo;
}).collect(Collectors.toList());
catalog2Vo.setCatalog3List(collect);
}
return catalog2Vo;
}).collect(Collectors.toList());
}

return catalog2Vos;
}));
//3.查到的数据再放入缓存,将对象转为json放在缓存中
String s = JSON.toJSONString(parent_cid);
redisTemplate.opsForValue().set("catalogJson",s,1, TimeUnit.DAYS);
return parent_cid;
}

}

157、缓存-缓存使用-本地锁在分布式下的问题

首先需要在idea中启动多个springboot实例。右击Services中的ZheliProductApplication,然后选择Copy Configuration。然后设置参数,Name修改为可以分辨出的就可以,Product arguments修改为--server.port=8004

缓存-分布式锁-分布式锁原理与使用

我们这边的分布式锁使用redis的setnx实现;http://www.redis.cn/commands/set.html

缓存-分布式锁-Redisson简介&整合

https://redis.io/topics/distlock

https://github.com/redisson/redisson/wiki/Table-of-Content

https://mvnrepository.com/artifact/org.redisson/redisson

整合redisson作为分布式锁等功能框架

(一).引入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>

(二).配置redisson

https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class MyRedissonConfig {

/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
* @throws IOException
*/
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
//1.创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//2.根据Config创建出RedissonClient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@SpringBootTest
class ZheliProductApplicationTests {
@Autowired
private RedissonClient redissonClient;

@Test
public void testRedisson(){
System.out.println(redissonClient);//org.redisson.Redisson@cfacf0
}
}

160、缓存-分布式锁-Redisson-lock锁测试

161、缓存-分布式锁-Redisson-lock看门狗原理-redisson如何解决死锁

可重入锁(Reentrant Lock)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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看门狗默认时间】
// 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期满30s
// internalLockleaseTime【看门狗时间】/3,10s

// 最佳实战
//(1).lock.lock(30, TimeUnit.SECONDS);//省掉了整个续期操作,手动解锁
try{
System.out.println("加锁成功,执行业务......"+Thread.currentThread().getId());
Thread.sleep(30*1000);
} catch (InterruptedException e) {
} finally {
//3.解锁。假设解锁代码没有运行,redisson会不会出现死锁?不会!
System.out.println("释放锁......"+Thread.currentThread().getId());
lock.unlock();
}
return "Hello";
}

打开浏览器两个窗口同时访问http://localhost:8000/hello。会发现同时只有一个线线程会获取锁对象。控制台信息打印如下。

1
2
3
4
加锁成功,执行业务......424
释放锁......424
加锁成功,执行业务......426
释放锁......426

运行过程中,Redis中的状态。当运行完毕后或者超时my-lock数据会过期。

162、缓存-分布式锁-Redisson-读写锁测试

163、缓存-分布式锁-Redisson-读写锁补充

读写锁(ReadWriteLock)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@ResponseBody
@GetMapping("/write")
public String writeValue(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
RLock rLock = lock.writeLock();
String s = "";
try{
//改数据加写锁,读数据加读锁。
rLock.lock();
System.out.println("写锁加锁成功..."+Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30*1000);
redisTemplate.opsForValue().set("writeValue",s);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
rLock.unlock();
System.out.println("写锁释放..."+Thread.currentThread().getId());
}
return s;
}

//保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁,独享),读锁使一个共享锁。
//写锁没释放读就必须等待
//读+读 相当于无锁,并发读只会在redis中记录好所有当前的读锁,他们都会同时加锁成功
//写+读 等待写锁释放
//写+写 阻塞方式
//读+写 有读锁,写也需要等待
//只要有写的存在,都必须等待。
@ResponseBody
@GetMapping("/read")
public String readValue(){
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
System.out.println("读锁加锁成功..."+Thread.currentThread().getId());
String s = "";
//加读锁
RLock rLock = lock.readLock();
rLock.lock();
try{
s = redisTemplate.opsForValue().get("writeValue");
} catch (Exception e) {
e.printStackTrace();
}finally {
rLock.unlock();
System.out.println("读锁释放..."+Thread.currentThread().getId());
}
return s;
}

164、缓存-分布式锁-Redisson-信号量测试

信号量(Semaphore)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 车位停车
* 3车位
* 信号量也可以用作分布式限流
*/
@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
//park.acquire();//获取一个信号,获取一个值,占一个车位
boolean b = park.tryAcquire();
if(b){
//执行业务
}else{
return "error";
}
return "ok=>"+b;
}

@ResponseBody
@GetMapping("/go")
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();//释放一个车位
return "ok";
}

165、缓存-分布式锁-Redisson-闭锁测试

闭锁(CountDownLatch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 放假,锁门
* 1班没人了,2班没人了...
* 5个班级全部走完,我们可以锁大门
*/
@ResponseBody
@GetMapping("/lockDoor")
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.await();//等待闭锁都完成

return "放假了...";
}

@ResponseBody
@GetMapping("/gogogo/{id}")
public String gogogog(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计数减--

return id+"班的人都走了...";
}

166、缓存-分布式锁-缓存一致性解决

  • 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
    1. 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
    2. 如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。
    3. 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
    4. 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);
  • 总结:
    1. 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保 证每天拿到当前最新数据即可。
    2. 我们不应该过度设计,增加系统的复杂性。
    3. 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

167、缓存-SpringCache-简介

168、缓存-SpringCache-整合&体验@Cacheable

整合SpringCache简化缓存开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(一).引入依赖
spring-boot-starter-cache、spring-boot-starter-data-redis
(二).写配置
(1).自动配置了哪些?
CacheAutoConfiguration会导入RedisCacheConfiguration
自动配置好了缓存管理器RedisCacheManager
(2).配置使用redis作为缓存
在application.properties中配置。
(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).只需要使用注解就能完成缓存操作
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
1
2
3
4
5
6
7
8
//CategoryServiceImpl.java
//每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】
@Cacheable({"category"}) //代表当前方法的结果需要缓存,如果缓存中有,方法不有调用;如果缓存中没有,那就会调用方法,将方法的结果放入缓存
@Override
public List<CategoryEntity> getLeve1Categorys() {
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}

运行加了@Cacheable注解的方法,可以在redis中查看缓存数据。

169、缓存-SpringCache-@Cacheable细节设置

1
2
#单位是毫秒
spring.cache.redis.time-to-live=3600000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 1.每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】
* 2.@Cacheable({"category"}) //代表当前方法的结果需要缓存,如果缓存中有,方法不有调用;如果缓存中没有,那就会调用方法,将方法的结果放入缓存
* 3.默认行为
* (1).如果缓存命中,方法不用调用。
* (2).key默认自动生成:缓存名字::SimpleKey[](自主生成的key值)
* (3).缓存的value的值。默认使用jdk序列化机制,将序列化后的数据存到redis。
* (4).默认ttl时间:-1。
* 自定义
* (1).指定生成的缓存使用的key:key属性指定,接受一个SpEL。
* (2).指定缓存的数据的存活时间:配置文件中修改ttl
* (3).将数据保存为json格式:
* @return
*/
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLeve1Categorys() {
System.out.println("getLeve1Categorys......");
long l = System.currentTimeMillis();
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
System.out.println("消耗时间"+(System.currentTimeMillis()-l));
return categoryEntities;
}

SpEL语法:https://docs.spring.io/spring-framework/docs/5.3.0-SNAPSHOT/reference/html/integration.html#cache-spel-context

170、缓存-SpringCache-自定义缓存配置

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//config = config.entryTtl();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return config;
}
}

application.properties里面配置了过期时间,为了使我们的配置生效。

重新启动后,查看redis中的缓存数据。

配置其他内容。

1
2
3
4
5
6
7
8
9
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=false
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true

171、缓存-SpringCache-@CacheEvict

缓存失效模式:修改的时候使缓存消失,下次重新查询的时候,再加入缓存。

修改过后就可以在 RedisDeskTopManager客户端查看,可以看到缓存删除了。

之后修改我们原本获取三级分类的方法。

重启服务,刷新首页。

第一次访问之后缓存就存到redis中了,再次刷新首页就不会去访问数据库了。

但是现在修改菜单数据之后删除键为getLeve1Categorys的缓存数据,我们想要修改菜单之后两个缓存数据都删除掉。有以下两种方式:

1.同时进行多种缓存:@Caching
2.指定删除某个分区下的所有数据:@CacheEvict(value = “category”,allEntries = true)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 级联更新所有关联的数据
* @CacheEvict:失效模式
* 1.同时进行多种缓存:@Caching
* 2.指定删除某个分区下的所有数据:@CacheEvict(value = "category",allEntries = true)
* 存储同一类型的数据,都可以指定成同一分区。分区名默认就是缓存的前缀。
* @param category
*/
@Caching(evict = {
@CacheEvict(value = "category" ,key = "'getLeve1Categorys'"),
@CacheEvict(value = "category" ,key = "'getCatalogJson'"),
})
//@CacheEvict(value = "category",allEntries = true)
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
}

推荐配置

1
2
//@CachePut//双写模式
@CacheEvict(value = "category",allEntries = true)//失效模式

172、缓存-SpringCache-原理与不足

Spring-Cache的不足

  • 读模式:

    • 缓存击穿:查询一个null数据。解决:缓存空数据:cache-null-values。
    • 缓存穿透:大量并发进来同时查找一个正好过期的数据。解决:加锁:?默认是无加锁的。sync = true(加锁解决击穿)
    • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。
  • 写模式:(缓存和数据库不一致)

    • 1).读写加锁。
    • 2).引入Canal,感知MySQL的更新去更新数据库。
    • 3).读少写多,直接去数据库查询就行。
  • 原理

    • CacheManager(RedisCacheManager) -> Cache(RedisCache) -> Cache负责缓存的读写
  • 总结:

    • 常规数据(读多写少。即时性,一致性要求不高的数据):完全可以使用spring-cache:写模式(只要缓存的数据又过期时间就足够了)
    • 特殊数据:特殊设计。

评论